Skip to content
Open
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
19 changes: 15 additions & 4 deletions dev-packages/cloudflare-integration-tests/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,17 @@ export function expectedEvent(event: Event, { sdk }: { sdk: 'cloudflare' | 'hono

export function eventEnvelope(
event: Event,
{ includeSampleRand = false, sdk = 'cloudflare' }: { includeSampleRand?: boolean; sdk?: 'cloudflare' | 'hono' } = {},
{
includeSamplingFields = false,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-l: Not a fan of this tbh as it still limits what we're expecting in the specific values. Why not directly assert on the envelope headers?

This is fine for the PR though, so no need to change it :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it would be better to assert actual values.

I had a go at this but it snowballs quite a bit so I'll extract that out into a separate PR later.

includeSampleRand = false,
includeTransaction = true,
sdk = 'cloudflare',
}: {
includeSamplingFields?: boolean;
includeSampleRand?: boolean;
includeTransaction?: boolean;
sdk?: 'cloudflare' | 'hono';
} = {},
): Envelope {
return [
{
Expand All @@ -72,10 +82,11 @@ export function eventEnvelope(
environment: event.environment || 'production',
public_key: 'public',
trace_id: UUID_MATCHER,
sample_rate: expect.any(String),
...(includeSamplingFields && { sample_rate: expect.any(String), sampled: expect.any(String) }),
...(includeSampleRand && { sample_rand: expect.stringMatching(/^[01](\.\d+)?$/) }),
sampled: expect.any(String),
transaction: expect.any(String),
// In TwP mode the span name is omitted from the DSC when the span source is `url`
// (raw URLs may contain PII), mirroring `getDynamicSamplingContextFromSpan`.
...(includeTransaction && { transaction: expect.any(String) }),
},
},
[[{ type: 'event' }, expectedEvent(event, { sdk })]],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,30 @@ import { createRunner } from '../../runner';
it('Only sends one error event when withSentry is called twice', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(
eventEnvelope({
level: 'error',
exception: {
values: [
{
type: 'Error',
value: 'Test error from double-instrumented worker',
stacktrace: {
frames: expect.any(Array),
eventEnvelope(
{
level: 'error',
exception: {
values: [
{
type: 'Error',
value: 'Test error from double-instrumented worker',
stacktrace: {
frames: expect.any(Array),
},
mechanism: { type: 'auto.http.cloudflare', handled: false },
},
mechanism: { type: 'auto.http.cloudflare', handled: false },
},
],
],
},
request: {
headers: expect.any(Object),
method: 'GET',
url: expect.any(String),
},
},
request: {
headers: expect.any(Object),
method: 'GET',
url: expect.any(String),
},
}),
// `/error` resolves to a raw URL span (source `url`), so the TwP DSC omits the span name.
{ includeTransaction: false },
),
)
// The http.server span produces a transaction envelope that is sent in parallel with the
// error event. Either can arrive first at the mock server, so ignore it here to keep the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ it('Hono app captures errors', async ({ signal }) => {
url: expect.any(String),
},
},
{ includeSampleRand: true },
{ includeSamplingFields: true, includeSampleRand: true },
),
)
// Second envelope: transaction event
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ it('Hono app captures parametrized errors (Hono SDK)', async ({ signal }) => {
},
],
},
{ includeSampleRand: true, sdk: 'hono' },
{ includeSamplingFields: true, includeSampleRand: true, sdk: 'hono' },
),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import { createRunner } from '../../../runner';
it('Captures JSON request body', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(
eventEnvelope({
level: 'info',
message: 'POST JSON request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-json'),
data: '{"username":"test","action":"login"}',
eventEnvelope(
{
level: 'info',
message: 'POST JSON request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-json'),
data: '{"username":"test","action":"login"}',
},
},
}),
// Raw URL span (source `url`), so the TwP DSC omits the span name.
{ includeTransaction: false },
),
)
.start(signal);

Expand All @@ -29,16 +33,20 @@ it('Captures JSON request body', async ({ signal }) => {
it('Captures form-urlencoded request body', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(
eventEnvelope({
level: 'info',
message: 'POST form request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-form'),
data: 'username=test&password=secret',
eventEnvelope(
{
level: 'info',
message: 'POST form request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-form'),
data: 'username=test&password=secret',
},
},
}),
// Raw URL span (source `url`), so the TwP DSC omits the span name.
{ includeTransaction: false },
),
)
.start(signal);

Expand All @@ -53,16 +61,20 @@ it('Captures form-urlencoded request body', async ({ signal }) => {
it('Captures plain text request body', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(
eventEnvelope({
level: 'info',
message: 'POST text request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-text'),
data: 'This is plain text content',
eventEnvelope(
{
level: 'info',
message: 'POST text request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-text'),
data: 'This is plain text content',
},
},
}),
// Raw URL span (source `url`), so the TwP DSC omits the span name.
{ includeTransaction: false },
),
)
.start(signal);

Expand All @@ -77,15 +89,19 @@ it('Captures plain text request body', async ({ signal }) => {
it('Does not capture body for POST without content', async ({ signal }) => {
const runner = createRunner(__dirname)
.expect(
eventEnvelope({
level: 'info',
message: 'POST no body request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-no-body'),
eventEnvelope(
{
level: 'info',
message: 'POST no body request',
request: {
headers: expect.any(Object),
method: 'POST',
url: expect.stringContaining('/post-no-body'),
},
},
}),
// Raw URL span (source `url`), so the TwP DSC omits the span name.
{ includeTransaction: false },
),
)
.start(signal);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as Sentry from '@sentry/cloudflare';

interface Env {
SENTRY_DSN: string;
}

// Tracing is enabled (not TwP), but the route is a raw, non-parametrized URL so the
// http.server span source is `url`. The span name must therefore be omitted from the
// DSC (raw URLs may contain PII), even though a real transaction is recorded.
export default Sentry.withSentry(
(env: Env) => ({
dsn: env.SENTRY_DSN,
tracesSampleRate: 1.0,
}),
{
async fetch(_request, _env, _ctx) {
throw new Error('Test error from URL-source worker');
},
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect, it } from 'vitest';
import { eventEnvelope } from '../../../expect';
import { createRunner } from '../../../runner';

it('omits the span name from the DSC for url-source spans when tracing is enabled', async ({ signal }) => {
const runner = createRunner(__dirname)
// Error event: because tracing is enabled, the DSC carries the sampling fields. But the span
// source is `url`, so the span name is omitted from the DSC (raw URLs may contain PII).
.expect(
eventEnvelope(
{
level: 'error',
exception: {
values: [
{
type: 'Error',
value: 'Test error from URL-source worker',
stacktrace: {
frames: expect.any(Array),
},
mechanism: { type: 'auto.http.cloudflare', handled: false },
},
],
},
request: {
headers: expect.any(Object),
method: 'GET',
url: expect.any(String),
},
},
{ includeSamplingFields: true, includeSampleRand: true, includeTransaction: false },
),
)
// Transaction event: proves we are NOT in TwP — the span is recorded with a `url` source and
// carries the name on the event itself, even though it is intentionally absent from the DSC.
.expect(envelope => {
const transactionEvent = envelope[1]?.[0]?.[1];
expect(transactionEvent).toEqual(
expect.objectContaining({
type: 'transaction',
transaction: 'GET /error',
contexts: expect.objectContaining({
trace: expect.objectContaining({
op: 'http.server',
data: expect.objectContaining({ 'sentry.source': 'url' }),
}),
}),
}),
);
})
.unordered()
.start(signal);
await runner.makeRequest('get', '/error', { expectError: true });
await runner.completed();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ it('Tracing headers', async ({ signal }) => {
const [SERVER_URL, closeTestServer] = await createTestServer()
.get('/', headers => {
expect(headers['baggage']).toEqual(expect.any(String));
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})-0$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0');
expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f\d]{32})-([a-f\d]{16})$/));
expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000');
expect(headers['traceparent']).toEqual(expect.stringMatching(/^00-([a-f\d]{32})-([a-f\d]{16})-00$/));
})
.start();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,19 @@ test.describe('server - redis db spans (instrumentation API)', () => {
expect(transaction.contexts?.trace?.op).toBe('http.server');
expect(transaction.contexts?.trace?.origin).toBe('auto.http.react_router.instrumentation_api');

// Collect every span id in the transaction (root + children) so we can verify nesting.
const rootSpanId = transaction.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...(transaction.spans ?? []).map(span => span.span_id)]);

const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis');

// loader runs SET then GET => at least two redis command spans
expect(redisSpans.length).toBeGreaterThanOrEqual(2);
expect(redisSpans.every(span => span.data?.['db.system'] === 'redis')).toBe(true);
expect(redisSpans.every(span => typeof span.parent_span_id === 'string')).toBe(true);
expect(redisSpans.some(span => span.data?.['net.peer.port'] === 6379)).toBe(true);

const statements = redisSpans.map(span => String(span.data?.['db.statement'] ?? '').toLowerCase());
expect(statements.some(statement => statement.startsWith('set'))).toBe(true);
expect(statements.some(statement => statement.startsWith('get'))).toBe(true);
// every redis span nests under the native instrumentation-API http.server transaction
const allNested = redisSpans.every(
span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id),
);
expect(allNested).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,20 @@ test.describe('server - redis db spans', () => {
const transaction = await txPromise;

expect(transaction.contexts?.trace?.op).toBe('http.server');

// Collect every span id in the transaction (root + children) so we can verify nesting.
const rootSpanId = transaction.contexts?.trace?.span_id;
const spanIds = new Set([rootSpanId, ...(transaction.spans ?? []).map(span => span.span_id)]);

const redisSpans = transaction.spans!.filter(span => span.op === 'db.redis');

// loader runs SET then GET => at least two redis command spans
expect(redisSpans.length).toBeGreaterThanOrEqual(2);

// every redis span is a child span tagged as the redis system
expect(redisSpans.every(span => span.data?.['db.system'] === 'redis')).toBe(true);
expect(redisSpans.every(span => typeof span.parent_span_id === 'string')).toBe(true);
expect(redisSpans.some(span => span.data?.['net.peer.port'] === 6379)).toBe(true);

// db.statement starts with the command name (e.g. "set cache:greeting ...", "get cache:greeting")
const statements = redisSpans.map(span => String(span.data?.['db.statement'] ?? '').toLowerCase());
expect(statements.some(statement => statement.startsWith('set'))).toBe(true);
expect(statements.some(statement => statement.startsWith('get'))).toBe(true);
// every redis span nests under the http.server transaction (its parent is part of the same span tree)
const allNested = redisSpans.every(
span => typeof span.parent_span_id === 'string' && spanIds.has(span.parent_span_id),
);
expect(allNested).toBe(true);
});
});
2 changes: 2 additions & 0 deletions packages/core/src/shared-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export {
spanToStreamedSpanJSON,
spanIsSampled,
spanToTraceContext,
getSamplingDecision,
getSpanDescendants,
getStatusMessage,
getRootSpan,
Expand All @@ -101,6 +102,7 @@ export {
spanTimeInputToSeconds,
updateSpanName,
} from './utils/spanUtils';
export { TraceState, SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING } from './utils/traceState';
export { _setSpanForScope as _INTERNAL_setSpanForScope } from './utils/spanOnScope';
export { parseSampleRate } from './utils/parseSampleRate';
export { applySdkMetadata } from './utils/sdkMetadata';
Expand Down
Loading
Loading