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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://sentry-test-site.example'],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import type { EventAndTraceHeader } from '../../../../utils/helpers';
import {
eventAndTraceHeaderRequestParser,
getFirstSentryEnvelopeRequest,
shouldSkipTracingTest,
waitForErrorRequest,
waitForTransactionRequest,
} from '../../../../utils/helpers';

const SAMPLED_TRACE_ID = '12345678901234567890123456789012';
const SAMPLED_SPAN_ID = '1234567890123456';
const UNSAMPLED_TRACE_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const DEFERRED_TRACE_ID = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';

sentryTest(
'continueTrace continues a sampled incoming trace into span and outgoing request',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

// Discard the initial pageload transaction.
await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(page, url, eventAndTraceHeaderRequestParser);

const transactionPromise = waitForTransactionRequest(
page,
event => event.contexts?.trace?.trace_id === SAMPLED_TRACE_ID,
);

await page.locator('#sampled').click();

const req = await transactionPromise;
const transaction = eventAndTraceHeaderRequestParser(req);
const traceContext = transaction[0].contexts?.trace;

expect(traceContext?.trace_id).toBe(SAMPLED_TRACE_ID);
expect(traceContext?.parent_span_id).toBe(SAMPLED_SPAN_ID);
expect(transaction[0].transaction).toBe('continued-sampled');

// The incoming positive sampling decision is honored regardless of local config.
expect(transaction[1]?.sampled).toBe('true');

// Outgoing request carries the continued trace.
const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${SAMPLED_TRACE_ID}-[a-f0-9]{16}-1$`));
expect(headers['baggage']).toContain(`sentry-trace_id=${SAMPLED_TRACE_ID}`);
},
);

sentryTest(
'continueTrace continues an unsampled incoming trace without emitting a transaction',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(page, url, eventAndTraceHeaderRequestParser);

// The captured error carries the continued (unsampled) trace even though no transaction is sent.
const errorPromise = waitForErrorRequest(page);

await page.locator('#unsampled').click();

const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise);
expect(errorEvent.contexts?.trace?.trace_id).toBe(UNSAMPLED_TRACE_ID);

// Outgoing request carries the continued trace with the negative sampling decision.
const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${UNSAMPLED_TRACE_ID}-[a-f0-9]{16}-0$`));
},
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing no-transaction assertions

Medium Severity

Several cases claim a transaction is not emitted (unsampled, deferred with tracesSampleRate=0, TwP / negative parent decisions) but only assert on the error event and outgoing headers. Nothing fails if a transaction is still sent. Flagged because the review rules require an explicit assertion when a payload is expected to be absent.

Additional Locations (2)
Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit 9a9c4f5. Configure here.

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.

Valid, but this feels pretty minor to me.


sentryTest(
'continueTrace continues a deferred-sampling incoming trace and applies the local sample rate',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(page, url, eventAndTraceHeaderRequestParser);

// With tracesSampleRate=1 the deferred decision resolves to sampled, so a transaction is emitted
// on the continued trace id.
const transactionPromise = waitForTransactionRequest(
page,
event => event.contexts?.trace?.trace_id === DEFERRED_TRACE_ID,
);

await page.locator('#deferred').click();

const transaction = eventAndTraceHeaderRequestParser(await transactionPromise);
expect(transaction[0].contexts?.trace?.trace_id).toBe(DEFERRED_TRACE_ID);
expect(transaction[0].transaction).toBe('continued-deferred');
},
);

sentryTest('continueTrace with no incoming trace starts a fresh trace', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

const [pageloadEvent] = await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
page,
url,
eventAndTraceHeaderRequestParser,
);
const pageloadTraceId = pageloadEvent.contexts?.trace?.trace_id;

const transactionPromise = waitForTransactionRequest(page, event => event.transaction === 'continued-noTrace');

await page.locator('#noTrace').click();

const transaction = eventAndTraceHeaderRequestParser(await transactionPromise);
const traceId = transaction[0].contexts?.trace?.trace_id;

expect(traceId).toMatch(/^[a-f0-9]{32}$/);
expect(traceId).not.toBe(pageloadTraceId);
expect(transaction[0].contexts?.trace).not.toHaveProperty('parent_span_id');
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const TRACES = {
sampled: {
sentryTrace: '12345678901234567890123456789012-1234567890123456-1',
baggage:
'sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=1,sentry-sampled=true,sentry-public_key=public,sentry-sample_rand=0.42',
},
unsampled: {
sentryTrace: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111111111-0',
baggage: undefined,
},
// No trailing sampling flag -> deferred sampling decision.
deferred: {
sentryTrace: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2222222222222222',
baggage: undefined,
},
noTrace: {
sentryTrace: undefined,
baggage: undefined,
},
};

function continueAndRun(variant) {
const { sentryTrace, baggage } = TRACES[variant];
Sentry.continueTrace({ sentryTrace, baggage }, () => {
// Keep the span callback synchronous: the browser ACS is stack-based, so an `await` here would pop
// the continued span/scope before `fetch` and `captureException` run. Firing `fetch` without
// awaiting still attaches the propagation headers (they are read synchronously at call time).
Sentry.startSpan({ op: 'ui.interaction.click', name: `continued-${variant}` }, () => {
fetch('http://sentry-test-site.example');
Sentry.captureException(new Error(`continued-${variant}-error`));
});
});
Comment thread
cursor[bot] marked this conversation as resolved.
}

for (const variant of ['sampled', 'unsampled', 'deferred', 'noTrace']) {
document.getElementById(variant).addEventListener('click', () => continueAndRun(variant));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<button id="sampled">Sampled incoming trace</button>
<button id="unsampled">Unsampled incoming trace</button>
<button id="deferred">Deferred sampling decision</button>
<button id="noTrace">No incoming trace</button>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://sentry-test-site.example'],
tracesSampleRate: 0,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import {
eventAndTraceHeaderRequestParser,
shouldSkipTracingTest,
waitForErrorRequest,
waitForTransactionRequest,
} from '../../../../utils/helpers';

const SAMPLED_TRACE_ID = '12345678901234567890123456789012';
const SAMPLED_SPAN_ID = '1234567890123456';
const UNSAMPLED_TRACE_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const DEFERRED_TRACE_ID = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';

sentryTest(
'continueTrace honors a positive incoming sampling decision over tracesSampleRate=0',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

// With tracesSampleRate=0 there is no pageload transaction, so we only wait for the continued one.
const transactionPromise = waitForTransactionRequest(
page,
event => event.contexts?.trace?.trace_id === SAMPLED_TRACE_ID,
);

await page.goto(url);
await page.locator('#sampled').click();

const transaction = eventAndTraceHeaderRequestParser(await transactionPromise);
expect(transaction[0].contexts?.trace?.trace_id).toBe(SAMPLED_TRACE_ID);
expect(transaction[0].contexts?.trace?.parent_span_id).toBe(SAMPLED_SPAN_ID);

const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${SAMPLED_TRACE_ID}-[a-f0-9]{16}-1$`));
},
);

sentryTest(
'continueTrace does not emit a transaction for a deferred decision with tracesSampleRate=0',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

await page.goto(url);

// The captured error carries the continued trace even though no transaction is sent.
const errorPromise = waitForErrorRequest(page);

await page.locator('#deferred').click();

const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise);
expect(errorEvent.contexts?.trace?.trace_id).toBe(DEFERRED_TRACE_ID);

// The deferred decision resolves negatively against the local tracesSampleRate=0.
const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${DEFERRED_TRACE_ID}-[a-f0-9]{16}-0$`));
},
);

sentryTest(
'continueTrace continues an unsampled incoming trace with tracesSampleRate=0',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

await page.goto(url);

const errorPromise = waitForErrorRequest(page);

await page.locator('#unsampled').click();

const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise);
expect(errorEvent.contexts?.trace?.trace_id).toBe(UNSAMPLED_TRACE_ID);

const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${UNSAMPLED_TRACE_ID}-[a-f0-9]{16}-0$`));
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

// In the browser, "tracing without performance" (TwP) means enabling `browserTracingIntegration`
// but not setting `tracesSampleRate`.
Sentry.init({
traceLifecycle: 'static',
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracePropagationTargets: ['http://sentry-test-site.example'],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { expect } from '@playwright/test';
import { sentryTest } from '../../../../utils/fixtures';
import {
eventAndTraceHeaderRequestParser,
shouldSkipTracingTest,
waitForErrorRequest,
} from '../../../../utils/helpers';

const SAMPLED_TRACE_ID = '12345678901234567890123456789012';
const UNSAMPLED_TRACE_ID = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
const DEFERRED_TRACE_ID = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb';

// In TwP mode no transactions are emitted, so each variant is observed via the captured error event
// (which carries the continued trace on its `contexts.trace`) and the outgoing request headers.
const VARIANTS = [
{ button: 'sampled', traceId: SAMPLED_TRACE_ID, sampledFlag: '-1' },
{ button: 'unsampled', traceId: UNSAMPLED_TRACE_ID, sampledFlag: '-0' },
{ button: 'deferred', traceId: DEFERRED_TRACE_ID, sampledFlag: '' },
] as const;

for (const variant of VARIANTS) {
sentryTest(
`continueTrace continues the ${variant.button} incoming trace in TwP mode`,
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const url = await getLocalTestUrl({ testDir: __dirname });

const outgoingRequestPromise = page.waitForRequest('http://sentry-test-site.example/**');
await page.route('http://sentry-test-site.example/**', route => {
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({}) });
});

await page.goto(url);

const errorPromise = waitForErrorRequest(page);

await page.locator(`#${variant.button}`).click();

const [errorEvent] = eventAndTraceHeaderRequestParser(await errorPromise);
expect(errorEvent.contexts?.trace?.trace_id).toBe(variant.traceId);

const outgoingRequest = await outgoingRequestPromise;
const headers = await outgoingRequest.allHeaders();
expect(headers['sentry-trace']).toMatch(new RegExp(`^${variant.traceId}-[a-f0-9]{16}${variant.sampledFlag}$`));
},
);
}
Loading
Loading