Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
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 numberDiff line numberDiff 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
MemberAuthor

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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
{
"name": "worker-name",
"compatibility_date": "2025-06-17",
"main": "index.ts",
"compatibility_flags": ["nodejs_compat"],
}
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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