diff --git a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js new file mode 100644 index 000000000000..1c46615de295 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/init.js @@ -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', + tracesSampler: ({ attributes }) => { + return attributes?.['sentry.op'] === 'custom.op' ? 1 : 0; + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/subject.js b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/subject.js new file mode 100644 index 000000000000..729e4f2b03c0 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/subject.js @@ -0,0 +1,2 @@ +Sentry.startSpan({ name: 'span-with-unsampled-op', op: 'other.op' }, () => {}); +Sentry.startSpan({ name: 'span-with-sampled-op', op: 'custom.op' }, () => {}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts new file mode 100644 index 000000000000..9d5bc57bc5c2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/tracesSampler/op-attribute/test.ts @@ -0,0 +1,24 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { + envelopeRequestParser, + shouldSkipTracingTest, + waitForTransactionRequestOnUrl, +} from '../../../../utils/helpers'; + +sentryTest('tracesSampler can sample based on the `sentry.op` attribute', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + // The sampler drops `other.op` and keeps `custom.op`, so the only transaction + // that arrives is the one whose op the sampler saw in the attributes. + const req = await waitForTransactionRequestOnUrl(page, url); + const transactionEvent = envelopeRequestParser(req); + + expect(transactionEvent.type).toBe('transaction'); + expect(transactionEvent.transaction).toBe('span-with-sampled-op'); + expect(transactionEvent.contexts?.trace?.op).toBe('custom.op'); +}); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index a370dc296fa7..49c547e45645 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -462,6 +462,15 @@ function parseSentrySpanArguments(options: StartSpanOptions): SentrySpanArgument ...options, }; + // Fold `op` into the attributes up front so samplers see `sentry.op`; the `SentrySpan` + // constructor only adds it after the sampling decision. An explicit `sentry.op` attribute wins. + if (options.op) { + initialCtx.attributes = { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: options.op, + ...options.attributes, + }; + } + if (options.startTime) { const ctx: SentrySpanArguments & { startTime?: SpanTimeInput } = { ...initialCtx }; ctx.startTimestamp = spanTimeInputToSeconds(options.startTime); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 7740da97b1aa..83bfa902cf91 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -785,6 +785,27 @@ describe('startSpan', () => { }); }); + it('passes a `sentry.op` attribute to the tracesSampler when `op` is set', () => { + tracesSampler.mockReturnValueOnce(true); + + const options = getDefaultTestClientOptions({ tracesSampler }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + + startSpan({ name: 'outer', op: 'test.op', attributes: { test1: 'aa' } }, () => {}); + + expect(tracesSampler).toHaveBeenLastCalledWith({ + parentSampled: undefined, + name: 'outer', + attributes: { + 'sentry.op': 'test.op', + test1: 'aa', + }, + inheritOrSampleWith: expect.any(Function), + }); + }); + it.each([false, 0])('returns a negative sampling decision if tracesSampler returns %s', tracesSamplerResult => { tracesSampler.mockReturnValueOnce(tracesSamplerResult);