From b41d58686f20fad7f3dc91cf169f6b0af0dbe7c6 Mon Sep 17 00:00:00 2001 From: JPeer264 Date: Tue, 1 Sep 2026 12:25:00 +0200 Subject: [PATCH] test(e2e): Port cloudflare-orchestrion-mysql to span streaming Co-Authored-By: Claude Fable 5 --- .../cloudflare-orchestrion-mysql/src/index.ts | 1 - .../tests/mysql.test.ts | 73 ++++++++++--------- 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/src/index.ts b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/src/index.ts index 0fe3d1af327a..2cccb3c26c64 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/src/index.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/src/index.ts @@ -20,7 +20,6 @@ interface MysqlModule { export default Sentry.withSentry( (env: Env) => ({ - traceLifecycle: 'static', dsn: env.E2E_TEST_DSN, environment: 'qa', tunnel: 'http://localhost:3031/', diff --git a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts index 60c2a1b44704..0d024121865d 100644 --- a/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts +++ b/dev-packages/e2e-tests/test-applications/cloudflare-orchestrion-mysql/tests/mysql.test.ts @@ -1,57 +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'; + +function isMysqlRequestSegment(span: SerializedStreamedSpan): boolean { + return getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/test-mysql'; +} + +function getDbSpansOfTrace(spans: SerializedStreamedSpan[], traceId: string): SerializedStreamedSpan[] { + return spans.filter(span => getSpanOp(span) === 'db' && span.trace_id === traceId); +} test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ baseURL }) => { - // Each incoming request gets a Sentry http.server transaction; the mysql - // queries run inside it, so their db spans attach to it. The + // Each incoming request gets a Sentry http.server segment span; the mysql + // queries run inside it, so their db spans share its trace. The // `orchestrion:mysql:query` channel was injected into the bundled `mysql` // package at build time by `@sentry/cloudflare/vite`, and the Cloudflare SDK // subscribes to it once it detects the injection. - const transactionPromise = waitForTransaction('cloudflare-orchestrion-mysql', event => { - return ( - event?.contexts?.trace?.op === 'http.server' && - (event.request?.url ?? '').includes('/test-mysql') && - (event.spans?.some(span => span.op === 'db') ?? false) - ); + const spansPromise = collectStreamedSpans('cloudflare-orchestrion-mysql', spans => { + const segment = spans.find(isMysqlRequestSegment); + return !!segment && getDbSpansOfTrace(spans, segment.trace_id).length >= 1; }); 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 segment = spans.find(isMysqlRequestSegment)!; + const dbSpans = getDbSpansOfTrace(spans, segment.trace_id); - 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.address']).toBe('127.0.0.1'); - expect(firstQuery!.data?.['server.port']).toBe(3306); - expect(firstQuery!.data?.['db.user']).toBe('root'); + // With span streaming the span name is the low-cardinality query summary; the statement stays in `db.query.text`. + expect(firstQuery!.name).toBe('SELECT'); + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); + expect(firstQuery!.attributes['db.query.text']?.value).toBe('SELECT 1 + 1 AS solution'); + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); }); -test('a nested query lands on the same transaction (async context restored)', async ({ baseURL }) => { +test('a nested query lands on the same trace (async 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 the channel subscriber 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('cloudflare-orchestrion-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 - ); + // mysql's async socket-callback dispatch. Both spans sharing the SAME + // http.server segment's trace proves the channel subscriber restored the + // parent span across that async boundary (otherwise the nested query would + // start its own trace and never join this one). + const spansPromise = collectStreamedSpans('cloudflare-orchestrion-mysql', spans => { + const segment = spans.find(isMysqlRequestSegment); + return !!segment && getDbSpansOfTrace(spans, segment.trace_id).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(isMysqlRequestSegment)!; + const queryTexts = getDbSpansOfTrace(spans, segment.trace_id).map(span => span.attributes['db.query.text']?.value); + expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); + expect(queryTexts).toContain('SELECT NOW()'); });