|
1 | 1 | import { expect, test } from '@playwright/test'; |
2 | | -import { waitForTransaction } from '@sentry-internal/test-utils'; |
| 2 | +import { collectStreamedSpans, getSpanOp } from '@sentry-internal/test-utils'; |
3 | 3 |
|
4 | 4 | test('a real mysql query emits a db span with orchestrion-channel attributes', async ({ baseURL }) => { |
5 | | - // Each incoming request gets a Sentry http.server transaction; the mysql |
6 | | - // queries run inside it, so their db spans attach to it. The |
| 5 | + // Each incoming request gets a Sentry http.server segment span; the mysql |
| 6 | + // queries run inside it, so their db spans share its trace. The |
7 | 7 | // `orchestrion:mysql:query` channel was injected into the bundled `mysql` |
8 | 8 | // package at build time by `@sentry/cloudflare/vite`, and the Cloudflare SDK |
9 | 9 | // subscribes to it once it detects the injection. |
10 | | - const transactionPromise = waitForTransaction('cloudflare-orchestrion-mysql', event => { |
11 | | - return ( |
12 | | - event?.contexts?.trace?.op === 'http.server' && |
13 | | - (event.request?.url ?? '').includes('/test-mysql') && |
14 | | - (event.spans?.some(span => span.op === 'db') ?? false) |
15 | | - ); |
16 | | - }); |
| 10 | + const spansPromise = collectStreamedSpans( |
| 11 | + 'cloudflare-orchestrion-mysql', |
| 12 | + spans => |
| 13 | + spans.some( |
| 14 | + span => |
| 15 | + getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/test-mysql', |
| 16 | + ) && spans.some(span => getSpanOp(span) === 'db'), |
| 17 | + ); |
17 | 18 |
|
18 | 19 | const res = await fetch(`${baseURL}/test-mysql`); |
19 | 20 | expect(res.status).toBe(200); |
20 | 21 | await res.json(); |
21 | 22 |
|
22 | | - const transaction = await transactionPromise; |
23 | | - const dbSpans = transaction.spans!.filter(span => span.op === 'db'); |
| 23 | + const spans = await spansPromise; |
| 24 | + const dbSpans = spans.filter(span => getSpanOp(span) === 'db'); |
24 | 25 |
|
25 | | - const firstQuery = dbSpans.find(span => span.description === 'SELECT 1 + 1 AS solution'); |
| 26 | + const firstQuery = dbSpans.find(span => span.attributes['db.query.text']?.value === 'SELECT 1 + 1 AS solution'); |
26 | 27 | expect(firstQuery).toBeDefined(); |
27 | | - expect(firstQuery!.data?.['sentry.origin']).toBe('auto.db.mysql'); |
28 | | - expect(firstQuery!.data?.['db.system.name']).toBe('mysql'); |
29 | | - expect(firstQuery!.data?.['db.query.text']).toBe('SELECT 1 + 1 AS solution'); |
30 | | - expect(firstQuery!.data?.['server.address']).toBe('127.0.0.1'); |
31 | | - expect(firstQuery!.data?.['server.port']).toBe(3306); |
32 | | - expect(firstQuery!.data?.['db.user']).toBe('root'); |
| 28 | + // With span streaming the span name is the low-cardinality query summary; the statement stays in `db.query.text`. |
| 29 | + expect(firstQuery!.name).toBe('SELECT'); |
| 30 | + expect(firstQuery!.attributes['sentry.origin']?.value).toBe('auto.db.mysql'); |
| 31 | + expect(firstQuery!.attributes['db.system.name']?.value).toBe('mysql'); |
| 32 | + expect(firstQuery!.attributes['db.query.text']?.value).toBe('SELECT 1 + 1 AS solution'); |
| 33 | + expect(firstQuery!.attributes['server.address']?.value).toBe('127.0.0.1'); |
| 34 | + expect(firstQuery!.attributes['server.port']?.value).toBe(3306); |
| 35 | + expect(firstQuery!.attributes['db.user']?.value).toBe('root'); |
33 | 36 | }); |
34 | 37 |
|
35 | | -test('a nested query lands on the same transaction (async context restored)', async ({ baseURL }) => { |
| 38 | +test('a nested query lands on the same trace (async context restored)', async ({ baseURL }) => { |
36 | 39 | // The second query runs inside the first query's callback — i.e. across |
37 | | - // mysql's async socket-callback dispatch. Both spans appearing on the SAME |
38 | | - // http.server transaction proves the channel subscriber restored the parent |
39 | | - // span across that async boundary (otherwise the nested query would start its |
40 | | - // own trace and never join this transaction). |
41 | | - const transactionPromise = waitForTransaction('cloudflare-orchestrion-mysql', event => { |
42 | | - return ( |
43 | | - event?.contexts?.trace?.op === 'http.server' && |
44 | | - (event.request?.url ?? '').includes('/test-mysql') && |
45 | | - (event.spans?.filter(span => span.op === 'db').length ?? 0) >= 2 |
46 | | - ); |
47 | | - }); |
| 40 | + // mysql's async socket-callback dispatch. Both spans sharing the SAME |
| 41 | + // http.server segment's trace proves the channel subscriber restored the |
| 42 | + // parent span across that async boundary (otherwise the nested query would |
| 43 | + // start its own trace and never join this one). |
| 44 | + const spansPromise = collectStreamedSpans( |
| 45 | + 'cloudflare-orchestrion-mysql', |
| 46 | + spans => |
| 47 | + spans.some( |
| 48 | + span => |
| 49 | + getSpanOp(span) === 'http.server' && span.is_segment && span.attributes['url.path']?.value === '/test-mysql', |
| 50 | + ) && spans.filter(span => getSpanOp(span) === 'db').length >= 2, |
| 51 | + ); |
48 | 52 |
|
49 | 53 | const res = await fetch(`${baseURL}/test-mysql`); |
50 | 54 | expect(res.status).toBe(200); |
51 | 55 | await res.json(); |
52 | 56 |
|
53 | | - const transaction = await transactionPromise; |
54 | | - const descriptions = transaction.spans!.filter(span => span.op === 'db').map(span => span.description); |
55 | | - expect(descriptions).toContain('SELECT 1 + 1 AS solution'); |
56 | | - expect(descriptions).toContain('SELECT NOW()'); |
| 57 | + const spans = await spansPromise; |
| 58 | + const queryTexts = spans |
| 59 | + .filter(span => getSpanOp(span) === 'db') |
| 60 | + .map(span => span.attributes['db.query.text']?.value); |
| 61 | + expect(queryTexts).toContain('SELECT 1 + 1 AS solution'); |
| 62 | + expect(queryTexts).toContain('SELECT NOW()'); |
57 | 63 | }); |
0 commit comments