diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js index 5dc1d17588e5..5f616438fe90 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js @@ -4,12 +4,8 @@ const Sentry = require('@sentry/node'); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', transport: loggingTransport, - tracesSampler: ({ parentSampleRate }) => { - if (parentSampleRate) { - return parentSampleRate; - } - - return 0.69; + tracesSampler: ({ inheritOrSampleWith }) => { + return inheritOrSampleWith(0.69); }, }); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts index 304725268f03..f97773711941 100644 --- a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts @@ -11,7 +11,7 @@ describe('parentSampleRate propagation with tracesSampler', () => { expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); }); - test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate', async () => { + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (1 -> because there is a positive sampling decision and inheritOrSampleWith was used)', async () => { const runner = createRunner(__dirname, 'server.js').start(); const response = await runner.makeRequest('get', '/check', { headers: { @@ -20,6 +20,30 @@ describe('parentSampleRate propagation with tracesSampler', () => { }, }); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=1/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (0 -> because there is a negative sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (the fallback value -> because there is no sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); }); diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 70c62cd20992..0820b7be2cf0 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -27,7 +27,24 @@ export function sampleSpan( // work; prefer the hook if so let sampleRate; if (typeof options.tracesSampler === 'function') { - sampleRate = options.tracesSampler(samplingContext); + sampleRate = options.tracesSampler({ + ...samplingContext, + inheritOrSampleWith: fallbackSampleRate => { + // If we have an incoming parent sample rate, we'll just use that one. + // The sampling decision will be inherited because of the sample_rand that was generated when the trace reached the incoming boundaries of the SDK. + if (typeof samplingContext.parentSampleRate === 'number') { + return samplingContext.parentSampleRate; + } + + // Fallback if parent sample rate is not on the incoming trace (e.g. if there is no baggage) + // This is to provide backwards compatibility if there are incoming traces from older SDKs that don't send a parent sample rate or a sample rand. In these cases we just want to force either a sampling decision on the downstream traces via the sample rate. + if (typeof samplingContext.parentSampled === 'boolean') { + return Number(samplingContext.parentSampled); + } + + return fallbackSampleRate; + }, + }); localSampleRateWasApplied = true; } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 58634930c993..8e52b32eacf7 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -2,7 +2,7 @@ import type { CaptureContext } from '../scope'; import type { Breadcrumb, BreadcrumbHint } from './breadcrumb'; import type { ErrorEvent, EventHint, TransactionEvent } from './event'; import type { Integration } from './integration'; -import type { SamplingContext } from './samplingcontext'; +import type { TracesSamplerSamplingContext } from './samplingcontext'; import type { SdkMetadata } from './sdkmetadata'; import type { SpanJSON } from './span'; import type { StackLineParser, StackParser } from './stacktrace'; @@ -243,7 +243,7 @@ export interface ClientOptions number | boolean; + tracesSampler?: (samplingContext: TracesSamplerSamplingContext) => number | boolean; /** * An event-processing callback for error and message events, guaranteed to be invoked after all other event diff --git a/packages/core/src/types-hoist/samplingcontext.ts b/packages/core/src/types-hoist/samplingcontext.ts index 6f0d2a0800cf..b0a52862870c 100644 --- a/packages/core/src/types-hoist/samplingcontext.ts +++ b/packages/core/src/types-hoist/samplingcontext.ts @@ -10,9 +10,7 @@ export interface CustomSamplingContext { } /** - * Data passed to the `tracesSampler` function, which forms the basis for whatever decisions it might make. - * - * Adds default data to data provided by the user. + * Auxiliary data for various sampling mechanisms in the Sentry SDK. */ export interface SamplingContext extends CustomSamplingContext { /** @@ -42,3 +40,13 @@ export interface SamplingContext extends CustomSamplingContext { /** Initial attributes that have been passed to the span being sampled. */ attributes?: SpanAttributes; } + +/** + * Auxiliary data passed to the `tracesSampler` function. + */ +export interface TracesSamplerSamplingContext extends SamplingContext { + /** + * Returns a sample rate value that matches the sampling decision from the incoming trace, or falls back to the provided `fallbackSampleRate`. + */ + inheritOrSampleWith: (fallbackSampleRate: number) => number; +} diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 0eee7338a93d..c33b50c01a85 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -608,6 +608,7 @@ describe('startSpan', () => { test2: 'aa', test3: 'bb', }, + inheritOrSampleWith: expect.any(Function), }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index a841bec6ffb6..184b93b1e71b 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1350,6 +1350,7 @@ describe('trace (sampling)', () => { parentSampled: undefined, name: 'outer', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); // Now return `false`, it should not sample @@ -1416,6 +1417,7 @@ describe('trace (sampling)', () => { attr2: 1, 'sentry.op': 'test.op', }, + inheritOrSampleWith: expect.any(Function), }); // Now return `0`, it should not sample @@ -1457,6 +1459,7 @@ describe('trace (sampling)', () => { parentSampled: undefined, name: 'outer3', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); }); @@ -1490,6 +1493,7 @@ describe('trace (sampling)', () => { parentSampled: true, name: 'outer', attributes: {}, + inheritOrSampleWith: expect.any(Function), }); });