Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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/',
Expand Down
Original file line number Diff line number Diff line change
@@ -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()');
});
Loading