diff --git a/dev-packages/e2e-tests/test-applications/deno-mysql/src/app.ts b/dev-packages/e2e-tests/test-applications/deno-mysql/src/app.ts index 8799c7eb1062..585c0788b9a0 100644 --- a/dev-packages/e2e-tests/test-applications/deno-mysql/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/deno-mysql/src/app.ts @@ -6,7 +6,6 @@ import '@sentry/deno/import'; import * as Sentry from '@sentry/deno'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', dsn: Deno.env.get('E2E_TEST_DSN'), debug: !!Deno.env.get('DEBUG'), diff --git a/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts index a38e5a401b44..044336f28ae1 100644 --- a/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-mysql/tests/mysql.test.ts @@ -1,54 +1,63 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry/core'; + +// `Deno.serve` has no route information, so with span streaming the http.server segment is +// named after the method only; the path lives in `url.path`. +function isTestMysqlSegment(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/test-mysql'; +} test('mysql queries emit a db span with orchestrion-channel attributes', async ({ baseURL }) => { - // Each incoming request gets a Sentry http.server transaction (via the + // Each incoming request gets a Sentry http.server segment span (via the // default denoServeIntegration); the mysql queries run inside it, so their - // db spans attach to that transaction. - const transactionPromise = waitForTransaction('deno-mysql', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/test-mysql') && - (event.spans?.some(span => span.op === 'db') ?? false) - ); - }); + // db spans join that trace. + const spansPromise = collectStreamedSpans( + 'deno-mysql', + spans => spans.some(isTestMysqlSegment) && spans.some(span => getSpanOp(span) === 'db'), + ); const res = await fetch(`${baseURL}/test-mysql`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const dbSpans = transaction.spans!.filter(span => span.op === 'db'); + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); expect(firstQuery).toBeDefined(); - expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); - expect(firstQuery!.data?.['db.system.name']).toBe('mysql'); - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); - expect(firstQuery!.data?.['server.port']).toBe(3306); - expect(firstQuery!.data?.['db.user']).toBe('root'); + // With span streaming, db span names are the low-cardinality query summary, not the raw SQL + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.db.mysql', type: 'string' }, + 'db.system.name': { value: 'mysql', type: 'string' }, + 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'server.port': { value: 3306, type: 'integer' }, + 'db.user': { value: 'root', type: 'string' }, + }); }); -test('a nested query lands on the same transaction (AsyncLocalStorage context restored)', async ({ baseURL }) => { +test('a nested query lands on the same trace (AsyncLocalStorage context restored)', async ({ baseURL }) => { // The second query runs inside the first query's callback — i.e. across - // mysql's async socket-callback dispatch. Both spans appearing on the SAME - // http.server transaction proves denoMysqlIntegration's context strategy + // mysql's async socket-callback dispatch. Both db spans being children of the + // SAME http.server segment proves denoMysqlIntegration's context strategy // restored the parent span across that async boundary (otherwise the nested - // query would start its own trace and never join this transaction). - const transactionPromise = waitForTransaction('deno-mysql', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/test-mysql') && - (event.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 - ); - }); + // query would start its own trace and never join this one). + const spansPromise = collectStreamedSpans( + 'deno-mysql', + spans => spans.some(isTestMysqlSegment) && spans.filter(span => getSpanOp(span) === 'db').length >= 2, + ); const res = await fetch(`${baseURL}/test-mysql`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const descriptions = transaction.spans!.filter(span => span.op === 'db').map(span => span.description); - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); - expect(descriptions).toContain('SELECT NOW()'); + const spans = await spansPromise; + const segment = spans.find(isTestMysqlSegment)!; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + + const queries = dbSpans.map(span => span.attributes['db.query.text']?.value); + expect(queries).toContain('SELECT 1 + 1 AS solution'); + expect(queries).toContain('SELECT NOW()'); + expect(dbSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); }); diff --git a/dev-packages/e2e-tests/test-applications/deno-pg/src/app.ts b/dev-packages/e2e-tests/test-applications/deno-pg/src/app.ts index a22b4ec33e4b..2b9e7a432376 100644 --- a/dev-packages/e2e-tests/test-applications/deno-pg/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/deno-pg/src/app.ts @@ -7,7 +7,6 @@ import '@sentry/deno/import'; import * as Sentry from '@sentry/deno'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', dsn: Deno.env.get('E2E_TEST_DSN'), debug: !!Deno.env.get('DEBUG'), diff --git a/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts b/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts index f03ea57becf3..48b07cb5b80e 100644 --- a/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-pg/tests/pg.test.ts @@ -1,55 +1,64 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry/core'; + +// `Deno.serve` has no route information, so with span streaming the http.server segment is +// named after the method only; the path lives in `url.path`. +function isTestPgSegment(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/test-pg'; +} test('pg queries emit a db span with orchestrion-channel attributes', async ({ baseURL }) => { - // Each incoming request gets a Sentry http.server transaction (via the + // Each incoming request gets a Sentry http.server segment span (via the // default denoServeIntegration); the pg queries run inside it, so their - // db spans attach to that transaction. - const transactionPromise = waitForTransaction('deno-pg', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/test-pg') && - (event.spans?.some(span => span.op === 'db') ?? false) - ); - }); + // db spans join that trace. + const spansPromise = collectStreamedSpans( + 'deno-pg', + spans => spans.some(isTestPgSegment) && spans.some(span => getSpanOp(span) === 'db'), + ); const res = await fetch(`${baseURL}/test-pg`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const dbSpans = transaction.spans!.filter(span => span.op === 'db'); + const spans = await spansPromise; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); expect(firstQuery).toBeDefined(); - expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.postgres'); - expect(firstQuery!.data?.['db.system.name']).toBe('postgresql'); - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); - expect(firstQuery!.data?.['server.port']).toBe(5432); - expect(firstQuery!.data?.['db.user']).toBe('postgres'); + // With span streaming, db span names are the low-cardinality query summary, not the raw SQL + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes).toMatchObject({ + 'sentry.origin': { value: 'auto.db.postgres', type: 'string' }, + 'db.system.name': { value: 'postgresql', type: 'string' }, + 'db.query.text': { value: 'SELECT 1 + 1 AS solution', type: 'string' }, + 'server.port': { value: 5432, type: 'integer' }, + 'db.user': { value: 'postgres', type: 'string' }, + }); }); -test('a nested query lands on the same transaction (AsyncLocalStorage context restored)', async ({ baseURL }) => { +test('a nested query lands on the same trace (AsyncLocalStorage context restored)', async ({ baseURL }) => { // The second query runs inside the first query's callback - // i.e. across pg's async socket-callback dispatch. Both spans appearing - // on the SAME http.server transaction proves denoPostgresIntegration's + // i.e. across pg's async socket-callback dispatch. Both db spans being + // children of the SAME http.server segment proves denoPostgresIntegration's // context strategy restored the parent span across that async boundary // (otherwise the nested query would start its own trace and never join - // this transaction). - const transactionPromise = waitForTransaction('deno-pg', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/test-pg') && - (event.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 - ); - }); + // this one). + const spansPromise = collectStreamedSpans( + 'deno-pg', + spans => spans.some(isTestPgSegment) && spans.filter(span => getSpanOp(span) === 'db').length >= 2, + ); const res = await fetch(`${baseURL}/test-pg`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const descriptions = transaction.spans!.filter(span => span.op === 'db').map(span => span.description); - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); - expect(descriptions).toContain('SELECT NOW()'); + const spans = await spansPromise; + const segment = spans.find(isTestPgSegment)!; + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); + + const queries = dbSpans.map(span => span.attributes['db.query.text']?.value); + expect(queries).toContain('SELECT 1 + 1 AS solution'); + expect(queries).toContain('SELECT NOW()'); + expect(dbSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); }); diff --git a/dev-packages/e2e-tests/test-applications/deno-redis/src/app.ts b/dev-packages/e2e-tests/test-applications/deno-redis/src/app.ts index 155aeef31b8a..929e913873c6 100644 --- a/dev-packages/e2e-tests/test-applications/deno-redis/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/deno-redis/src/app.ts @@ -3,7 +3,6 @@ import IORedis from 'ioredis'; import { createClient } from 'redis'; Sentry.init({ - traceLifecycle: 'static', environment: 'qa', dsn: Deno.env.get('E2E_TEST_DSN'), debug: !!Deno.env.get('DEBUG'), diff --git a/dev-packages/e2e-tests/test-applications/deno-redis/tests/ioredis.test.ts b/dev-packages/e2e-tests/test-applications/deno-redis/tests/ioredis.test.ts index 0268bfc8e794..2dd1df6d0f8a 100644 --- a/dev-packages/e2e-tests/test-applications/deno-redis/tests/ioredis.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-redis/tests/ioredis.test.ts @@ -1,94 +1,107 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry/core'; -test('ioredis GET emits an http.server transaction containing a db.query child span', async ({ baseURL }) => { - // Each incoming request gets a Sentry http.server transaction (via the +// `Deno.serve` has no route information, so with span streaming the http.server segment is +// named after the method only; the path lives in `url.path`. +function isSegmentFor(path: string): (span: SerializedStreamedSpan) => boolean { + return span => getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === path; +} + +function isRedisCommand(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'db.query'; +} + +// `db.query.text` carries the key, so with span streaming a redis command span is named +// `{db.operation.name} {server.address}:{server.port}` instead. +function expectedCommandName(span: SerializedStreamedSpan): string { + const { 'db.operation.name': operation, 'server.address': address, 'server.port': port } = span.attributes; + return `${operation?.value} ${address?.value}:${port?.value}`; +} + +test('ioredis GET emits an http.server segment containing a db.query child span', async ({ baseURL }) => { + // Each incoming request gets a Sentry http.server segment span (via the // default denoServeIntegration); the ioredis command runs inside it, so the - // child span attaches to that transaction. - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/ioredis-get') && - (event.spans?.some(span => span.op === 'db.query') ?? false) - ); - }); + // child span joins that trace. + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/ioredis-get')) && spans.some(isRedisCommand), + ); const res = await fetch(`${baseURL}/ioredis-get?key=iocache:user:42`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpan = transaction.spans!.find(span => span.op === 'db.query'); + const spans = await spansPromise; + const segment = spans.find(isSegmentFor('/ioredis-get'))!; + const redisSpan = spans.find(isRedisCommand); expect(redisSpan).toBeDefined(); - // ioredis publishes lowercase command names; node-redis publishes uppercase. - expect(redisSpan!.description).toBe('redis-get'); - expect(redisSpan!.data?.['db.system.name']).toBe('redis'); - expect(redisSpan!.data?.['db.query.text']).toBe('get iocache:user:42'); + expect(redisSpan!.parent_span_id).toBe(segment.span_id); + expect(redisSpan!.name).toBe(expectedCommandName(redisSpan!)); + expect(redisSpan!.attributes).toMatchObject({ + 'db.system.name': { value: 'redis', type: 'string' }, + // ioredis publishes lowercase command names; node-redis publishes uppercase. + 'db.operation.name': { value: 'get', type: 'string' }, + 'db.query.text': { value: 'get iocache:user:42', type: 'string' }, + }); }); -test('ioredis SET then GET emit two db.query child spans on the same transaction', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/ioredis-set-get') && - (event.spans?.filter(span => span.op === 'db.query').length ?? 0) >= 2 - ); - }); +test('ioredis SET then GET emit two db.query child spans on the same trace', async ({ baseURL }) => { + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/ioredis-set-get')) && spans.filter(isRedisCommand).length >= 2, + ); const res = await fetch(`${baseURL}/ioredis-set-get?key=iocache:greeting&value=hello`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + const spans = await spansPromise; + const segment = spans.find(isSegmentFor('/ioredis-set-get'))!; + const redisSpans = spans.filter(isRedisCommand); expect(redisSpans.length).toBeGreaterThanOrEqual(2); - const ops = redisSpans.map(s => s.description); - expect(ops).toContain('redis-set'); - expect(ops).toContain('redis-get'); + expect(redisSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); + const ops = redisSpans.map(span => span.attributes['db.operation.name']?.value); + expect(ops).toContain('set'); + expect(ops).toContain('get'); }); test('ioredis MULTI emits one db.query span per command (no batch channel)', async ({ baseURL }) => { // ioredis does not publish to a batch channel — each command in the // transaction publishes individually with batchMode/batchSize set on its - // own payload. So the transaction should contain multiple `redis-` - // child spans, but no PIPELINE/MULTI batch span. - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/ioredis-multi') && - (event.spans?.filter(span => span.op === 'db.query').length ?? 0) >= 3 - ); - }); + // own payload. So the trace should contain multiple command child spans, + // but no PIPELINE/MULTI batch span. + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/ioredis-multi')) && spans.filter(isRedisCommand).length >= 3, + ); const res = await fetch(`${baseURL}/ioredis-multi`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + const spans = await spansPromise; + const redisSpans = spans.filter(isRedisCommand); expect(redisSpans.length).toBeGreaterThanOrEqual(3); - const descriptions = redisSpans.map(s => s.description); - expect(descriptions).toContain('redis-set'); - expect(descriptions).toContain('redis-get'); + const ops = redisSpans.map(span => span.attributes['db.operation.name']?.value); + expect(ops).toContain('set'); + expect(ops).toContain('get'); // No PIPELINE/MULTI batch wrapper span — ioredis has no separate batch channel. - const batchSpan = transaction.spans!.find(span => span.description === 'MULTI' || span.description === 'PIPELINE'); + const batchSpan = spans.find(span => span.name === 'MULTI' || span.name === 'PIPELINE'); expect(batchSpan).toBeUndefined(); }); test('ioredis PIPELINE emits one db.query span per command', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/ioredis-pipeline') && - (event.spans?.filter(span => span.op === 'db.query').length ?? 0) >= 3 - ); - }); + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/ioredis-pipeline')) && spans.filter(isRedisCommand).length >= 3, + ); const res = await fetch(`${baseURL}/ioredis-pipeline`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + const spans = await spansPromise; + const redisSpans = spans.filter(isRedisCommand); expect(redisSpans.length).toBeGreaterThanOrEqual(3); }); diff --git a/dev-packages/e2e-tests/test-applications/deno-redis/tests/redis.test.ts b/dev-packages/e2e-tests/test-applications/deno-redis/tests/redis.test.ts index 66c3fb72ac43..999444a1d3f6 100644 --- a/dev-packages/e2e-tests/test-applications/deno-redis/tests/redis.test.ts +++ b/dev-packages/e2e-tests/test-applications/deno-redis/tests/redis.test.ts @@ -1,71 +1,89 @@ import { expect, test } from '@playwright/test'; -import { waitForTransaction } from '@sentry-internal/test-utils'; +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; +import type { SerializedStreamedSpan } from '@sentry/core'; -test('GET command emits an http.server transaction containing a db.query child span', async ({ baseURL }) => { - // Each incoming request gets a Sentry http.server transaction (via the +// `Deno.serve` has no route information, so with span streaming the http.server segment is +// named after the method only; the path lives in `url.path`. +function isSegmentFor(path: string): (span: SerializedStreamedSpan) => boolean { + return span => getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === path; +} + +function isRedisCommand(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'db.query'; +} + +// `db.query.text` carries the key, so with span streaming a redis command span is named +// `{db.operation.name} {server.address}:{server.port}` instead. +function expectedCommandName(span: SerializedStreamedSpan): string { + const { 'db.operation.name': operation, 'server.address': address, 'server.port': port } = span.attributes; + return `${operation?.value} ${address?.value}:${port?.value}`; +} + +test('GET command emits an http.server segment containing a db.query child span', async ({ baseURL }) => { + // Each incoming request gets a Sentry http.server segment span (via the // default denoServeIntegration); the redis command runs inside it, so the - // child span attaches to that transaction. - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/redis-get') && - (event.spans?.some(span => span.op === 'db.query') ?? false) - ); - }); + // child span joins that trace. + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/redis-get')) && spans.some(isRedisCommand), + ); const res = await fetch(`${baseURL}/redis-get?key=cache:user:42`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpan = transaction.spans!.find(span => span.op === 'db.query'); + const spans = await spansPromise; + const segment = spans.find(isSegmentFor('/redis-get'))!; + const redisSpan = spans.find(isRedisCommand); expect(redisSpan).toBeDefined(); - expect(redisSpan!.description).toBe('redis-GET'); - expect(redisSpan!.data?.['db.system.name']).toBe('redis'); - // Statement omits the value; for GET the only allowed arg is the key. - expect(redisSpan!.data?.['db.query.text']).toBe('GET cache:user:42'); - expect(redisSpan!.data?.['server.port']).toBe(6379); + expect(redisSpan!.parent_span_id).toBe(segment.span_id); + expect(redisSpan!.name).toBe(expectedCommandName(redisSpan!)); + expect(redisSpan!.attributes).toMatchObject({ + 'db.system.name': { value: 'redis', type: 'string' }, + 'db.operation.name': { value: 'GET', type: 'string' }, + // Statement omits the value; for GET the only allowed arg is the key. + 'db.query.text': { value: 'GET cache:user:42', type: 'string' }, + 'server.port': { value: 6379, type: 'integer' }, + }); }); -test('SET then GET emit two db.query child spans on the same transaction', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/redis-set-get') && - (event.spans?.filter(span => span.op === 'db.query').length ?? 0) >= 2 - ); - }); +test('SET then GET emit two db.query child spans on the same trace', async ({ baseURL }) => { + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/redis-set-get')) && spans.filter(isRedisCommand).length >= 2, + ); const res = await fetch(`${baseURL}/redis-set-get?key=cache:greeting&value=hello`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const redisSpans = transaction.spans!.filter(span => span.op === 'db.query'); + const spans = await spansPromise; + const segment = spans.find(isSegmentFor('/redis-set-get'))!; + const redisSpans = spans.filter(isRedisCommand); expect(redisSpans.length).toBeGreaterThanOrEqual(2); - const ops = redisSpans.map(s => s.description); - expect(ops).toContain('redis-SET'); - expect(ops).toContain('redis-GET'); + expect(redisSpans.every(span => span.parent_span_id === segment.span_id)).toBe(true); + const ops = redisSpans.map(span => span.attributes['db.operation.name']?.value); + expect(ops).toContain('SET'); + expect(ops).toContain('GET'); }); test('MULTI batch emits a PIPELINE/MULTI batch span', async ({ baseURL }) => { - const transactionPromise = waitForTransaction('deno-redis', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/redis-multi') && - (event.spans?.some(span => span.description === 'MULTI' || span.description === 'PIPELINE') ?? false) - ); - }); + const isBatchSpan = (span: SerializedStreamedSpan) => span.name === 'MULTI' || span.name === 'PIPELINE'; + + const spansPromise = collectStreamedSpans( + 'deno-redis', + spans => spans.some(isSegmentFor('/redis-multi')) && spans.some(isBatchSpan), + ); const res = await fetch(`${baseURL}/redis-multi`); expect(res.status).toBe(200); await res.json(); - const transaction = await transactionPromise; - const batchSpan = transaction.spans!.find(span => span.description === 'MULTI' || span.description === 'PIPELINE'); + const spans = await spansPromise; + const batchSpan = spans.find(isBatchSpan); expect(batchSpan).toBeDefined(); - expect(batchSpan!.op).toBe('db.query'); - expect(batchSpan!.data?.['db.system.name']).toBe('redis'); + expect(getSpanOp(batchSpan!)).toBe('db.query'); + expect(batchSpan!.attributes['db.system.name']?.value).toBe('redis'); }); test('shut down redis client', async ({ baseURL }) => {