diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index d151772439a1..00fa6042934c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -67,7 +67,6 @@ sentryTest('should capture an INP click event span after pageload', async ({ bro 'sentry.exclusive_time': inpValue, 'sentry.op': 'ui.interaction.click', 'sentry.origin': 'auto.http.browser.inp', - 'sentry.sample_rate': 1, 'sentry.source': 'custom', transaction: 'test-url', 'user_agent.original': expect.stringContaining('Chrome'), diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts index 961f98518049..ece59d8fd281 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts @@ -70,7 +70,6 @@ sentryTest( 'sentry.exclusive_time': inpValue, 'sentry.op': 'ui.interaction.click', 'sentry.origin': 'auto.http.browser.inp', - 'sentry.sample_rate': 1, 'sentry.source': 'custom', transaction: 'test-route', 'user_agent.original': expect.stringContaining('Chrome'), diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts index a123099107d2..ca6e9723ce65 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/navigation/test.ts @@ -250,7 +250,7 @@ sentryTest( const navigationTraceId = navigationTraceContext?.trace_id; expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand}`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); @@ -313,7 +313,7 @@ sentryTest( const navigationTraceId = navigationTraceContext?.trace_id; expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sample_rate=1,sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand}`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${navigationTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); diff --git a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts index a4439840da7d..4af462e26aca 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload/test.ts @@ -240,7 +240,7 @@ sentryTest( // sampling decision is propagated from active span sampling decision expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand}`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); @@ -297,7 +297,7 @@ sentryTest( // sampling decision is propagated from active span sampling decision expect(headers['sentry-trace']).toMatch(new RegExp(`^${pageloadTraceId}-[0-9a-f]{16}-1$`)); expect(headers['baggage']).toBe( - `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sample_rate=1,sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand}`, + `sentry-environment=production,sentry-public_key=public,sentry-trace_id=${pageloadTraceId},sentry-sampled=true,sentry-sample_rand=${pageloadTraceHeader?.sample_rand},sentry-sample_rate=1`, ); }, ); diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts index 9a295f677d96..644afc377545 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts @@ -31,7 +31,6 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts index 8817b2b22aa7..c04bbb568f2e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.static.test.ts @@ -36,7 +36,6 @@ test.describe('tracing in static/pre-rendered routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts index 8c0e2c0c8850..2bcf6cbf2362 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts @@ -31,7 +31,6 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts index a6b288f4de71..fc396999d76e 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts @@ -33,7 +33,6 @@ test.describe('tracing in static routes with server islands', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', @@ -75,7 +74,6 @@ test.describe('tracing in static routes with server islands', () => { data: expect.objectContaining({ 'sentry.op': 'http.server', 'sentry.origin': 'auto.http.astro', - 'sentry.sample_rate': 1, 'sentry.source': 'route', }), op: 'http.server', diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts index 9c202da53542..9db35c72a47d 100644 --- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts +++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.static.test.ts @@ -36,7 +36,6 @@ test.describe('tracing in static/pre-rendered routes', () => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.browser', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), op: 'pageload', diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts index 227851d458cf..ed4a36303efa 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/events.test.ts @@ -32,7 +32,6 @@ test('Event emitter', async () => { trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.op': 'event.nestjs', 'sentry.origin': 'auto.event.nestjs', }, diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts index ecc67e26ab16..040d5d326809 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts @@ -89,7 +89,6 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, @@ -204,7 +203,6 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index fa5cd062d862..5656ce0e5e57 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -36,7 +36,6 @@ test('Sends a pageload transaction', async ({ page }) => { data: expect.objectContaining({ 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.app_router_instrumentation', - 'sentry.sample_rate': 1, 'sentry.source': 'url', }), }, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts index 7e059b99354b..1b4e0f92a97a 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts @@ -89,7 +89,6 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, @@ -204,7 +203,6 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, diff --git a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts index 7e4aeee0f220..af2cfddded9a 100644 --- a/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-fastify/tests/propagation.test.ts @@ -89,7 +89,6 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, @@ -204,7 +203,6 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts index 0ff946d07d3d..00fa3ecfca9d 100644 --- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts +++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts @@ -89,7 +89,6 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, @@ -204,7 +203,6 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => { 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', - 'sentry.sample_rate': 1, url: `http://localhost:3030/test-inbound-headers/${id}`, 'otel.kind': 'SERVER', 'http.response.status_code': 200, diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts index b873e4a5c0aa..513cf6146d0f 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-assign/test.ts @@ -1,3 +1,4 @@ +import { parseBaggageHeader } from '@sentry/core'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; import type { TestAPIResponse } from '../server'; @@ -102,14 +103,20 @@ test('Should populate and propagate sentry baggage if sentry-trace header does n const response = await runner.makeRequest('get', '/test/express'); expect(response).toBeDefined(); - expect(response).toMatchObject({ - test_data: { - host: 'somewhere.not.sentry', - // TraceId changes, hence we only expect that the string contains the traceid key - baggage: expect.stringMatching( - /sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/, - ), - }, + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', }); }); @@ -123,13 +130,18 @@ test('Should populate Sentry and ignore 3rd party content if sentry-trace header }); expect(response).toBeDefined(); - expect(response).toMatchObject({ - test_data: { - host: 'somewhere.not.sentry', - // TraceId changes, hence we only expect that the string contains the traceid key - baggage: expect.stringMatching( - /sentry-environment=prod,sentry-release=1.0,sentry-public_key=public,sentry-trace_id=[\S]*,sentry-sample_rate=1,sentry-transaction=GET%20%2Ftest%2Fexpress/, - ), - }, + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', }); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts index 6ba4aaa74b32..105722a43239 100644 --- a/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts @@ -10,6 +10,7 @@ test('envelope header for error event during active unsampled span is correct', public_key: 'public', environment: 'production', release: '1.0', + sample_rate: '0', sampled: 'false', sample_rand: expect.any(String), }, diff --git a/dev-packages/node-integration-tests/suites/sample-rand-propagation/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js similarity index 100% rename from dev-packages/node-integration-tests/suites/sample-rand-propagation/server.js rename to dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/server.js diff --git a/dev-packages/node-integration-tests/suites/sample-rand-propagation/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts similarity index 97% rename from dev-packages/node-integration-tests/suites/sample-rand-propagation/test.ts rename to dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts index 42e6a0a5e555..7c566c1d8eeb 100644 --- a/dev-packages/node-integration-tests/suites/sample-rand-propagation/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rand-propagation/test.ts @@ -1,4 +1,4 @@ -import { cleanupChildProcesses, createRunner } from '../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; describe('sample_rand propagation', () => { afterAll(() => { diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js new file mode 100644 index 000000000000..dbc6b9009c49 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js @@ -0,0 +1,39 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts new file mode 100644 index 000000000000..c6800055c84b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts @@ -0,0 +1,25 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with no tracing enabled', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate an incoming sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should not propagate a sample rate for root traces', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js new file mode 100644 index 000000000000..dc2f49b081cf --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts new file mode 100644 index 000000000000..c12d2920dd9f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts @@ -0,0 +1,61 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate=0', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision and sample rate', 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/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js new file mode 100644 index 000000000000..512681043d4d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0.69, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts new file mode 100644 index 000000000000..27afa03e9045 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts @@ -0,0 +1,61 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision and sample rate', 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/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); +}); 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 new file mode 100644 index 000000000000..5dc1d17588e5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/server.js @@ -0,0 +1,46 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +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; + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); 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 new file mode 100644 index 000000000000..304725268f03 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler/test.ts @@ -0,0 +1,37 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampler', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + 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 () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate sample_rate equivalent to incoming sample_rate (because tracesSampler is configured that way)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); +}); diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index 5bbda349b25d..b32cab7db3f5 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -317,6 +317,9 @@ export const browserTracingIntegration = ((_options: Partial): Partial { + if (typeof rootSpanSampleRate === 'number' || typeof rootSpanSampleRate === 'string') { + dsc.sample_rate = `${rootSpanSampleRate}`; + } + return dsc; + } // For core implementation, we freeze the DSC onto the span as a non-enumerable property const frozenDsc = (rootSpan as SpanWithMaybeDsc)[FROZEN_DSC_FIELD]; if (frozenDsc) { - return frozenDsc; + return applyLocalSampleRateToDsc(frozenDsc); } // For OpenTelemetry, we freeze the DSC on the trace state - const traceState = rootSpan.spanContext().traceState; const traceStateDsc = traceState?.get('sentry.dsc'); // If the span has a DSC, we want it to take precedence const dscOnTraceState = traceStateDsc && baggageHeaderToDynamicSamplingContext(traceStateDsc); if (dscOnTraceState) { - return dscOnTraceState; + return applyLocalSampleRateToDsc(dscOnTraceState); } // Else, we generate it from the span const dsc = getDynamicSamplingContextFromClient(span.spanContext().traceId, client); - const jsonSpan = spanToJSON(rootSpan); - const attributes = jsonSpan.data; - const maybeSampleRate = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]; - - if (maybeSampleRate != null) { - dsc.sample_rate = `${maybeSampleRate}`; - } // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII - const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + const source = rootSpanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // after JSON conversion, txn.name becomes jsonSpan.description - const name = jsonSpan.description; + const name = rootSpanJson.description; if (source !== 'url' && name) { dsc.transaction = name; } @@ -127,6 +133,8 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly, samplingContext: SamplingContext, sampleRand: number, -): [sampled: boolean, sampleRate?: number] { +): [sampled: boolean, sampleRate?: number, localSampleRateWasApplied?: boolean] { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { return [false]; } + let localSampleRateWasApplied = undefined; + // we would have bailed already if neither `tracesSampler` nor `tracesSampleRate` were defined, so one of these should // work; prefer the hook if so let sampleRate; if (typeof options.tracesSampler === 'function') { sampleRate = options.tracesSampler(samplingContext); + localSampleRateWasApplied = true; } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; + localSampleRateWasApplied = true; } // Since this is coming from the user (or from a function provided by the user), who knows what we might get. @@ -51,7 +55,7 @@ export function sampleSpan( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); - return [false, parsedSampleRate]; + return [false, parsedSampleRate, localSampleRateWasApplied]; } // We always compare the sample rand for the current execution context against the chosen sample rate. @@ -66,8 +70,7 @@ export function sampleSpan( sampleRate, )})`, ); - return [false, parsedSampleRate]; } - return [true, parsedSampleRate]; + return [shouldSample, parsedSampleRate, localSampleRateWasApplied]; } diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index acc14d37bc31..74478f79903f 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -349,7 +349,7 @@ export class SentrySpan implements Span { /* eslint-disable @typescript-eslint/no-dynamic-delete */ delete this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; spans.forEach(span => { - span.data && delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; + delete span.data[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]; }); // eslint-enabled-next-line @typescript-eslint/no-dynamic-delete diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 8e14af7e6c71..64a4c8ba5a4c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -22,6 +22,7 @@ import { generateTraceId } from '../utils-hoist/propagationContext'; import { propagationContextFromHeaders } from '../utils-hoist/tracing'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { parseSampleRate } from '../utils/parseSampleRate'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; @@ -407,8 +408,10 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent const options: Partial = client?.getOptions() || {}; const { name = '', attributes } = spanArguments; - const sampleRand = scope.getPropagationContext().sampleRand; - const [sampled, sampleRate] = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] + const currentPropagationContext = scope.getPropagationContext(); + const [sampled, sampleRate, localSampleRateWasApplied] = scope.getScopeData().sdkProcessingMetadata[ + SUPPRESS_TRACING_KEY + ] ? [false] : sampleSpan( options, @@ -416,15 +419,17 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent name, parentSampled, attributes, - // TODO(v9): provide a parentSampleRate here + parentSampleRate: parseSampleRate(currentPropagationContext.dsc?.sample_rate), }, - sampleRand, + currentPropagationContext.sampleRand, ); const rootSpan = new SentrySpan({ ...spanArguments, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: + sampleRate !== undefined && localSampleRateWasApplied ? sampleRate : undefined, ...spanArguments.attributes, }, sampled, @@ -435,10 +440,6 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent client.recordDroppedEvent('sample_rate', 'transaction'); } - if (sampleRate !== undefined) { - rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); - } - if (client) { client.emit('spanStart', rootSpan); } diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts index 3856b9d35d13..63a6910de06b 100644 --- a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -42,8 +42,12 @@ describe('getDynamicSamplingContextFromSpan', () => { spanId: '12345', traceFlags: 0, traceState: { - get() { - return 'sentry-environment=myEnv2'; + get(key: string) { + if (key === 'sentry.dsc') { + return 'sentry-environment=myEnv2'; + } else { + return undefined; + } }, } as unknown as SpanContextData['traceState'], }; diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index cf2db6cb316a..0eee7338a93d 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -481,7 +481,6 @@ describe('startSpan', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, @@ -986,7 +985,6 @@ describe('startSpanManual', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, @@ -1317,7 +1315,6 @@ describe('startInactiveSpan', () => { trace: { data: { 'sentry.source': 'custom', - 'sentry.sample_rate': 1, 'sentry.origin': 'manual', }, parent_span_id: innerParentSpanId, diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index 4768f3176859..1f396b11c3af 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -492,7 +492,6 @@ describe('Integration | Transactions', () => { 'sentry.op': 'test op', 'sentry.origin': 'auto.test', 'sentry.source': 'task', - 'sentry.sample_rate': 1, }, op: 'test op', span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/opentelemetry/src/constants.ts b/packages/opentelemetry/src/constants.ts index d363945a4403..375e42dfdd00 100644 --- a/packages/opentelemetry/src/constants.ts +++ b/packages/opentelemetry/src/constants.ts @@ -7,6 +7,7 @@ export const SENTRY_TRACE_STATE_DSC = 'sentry.dsc'; export const SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING = 'sentry.sampled_not_recording'; export const SENTRY_TRACE_STATE_URL = 'sentry.url'; export const SENTRY_TRACE_STATE_SAMPLE_RAND = 'sentry.sample_rand'; +export const SENTRY_TRACE_STATE_SAMPLE_RATE = 'sentry.sample_rate'; export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); diff --git a/packages/opentelemetry/src/sampler.ts b/packages/opentelemetry/src/sampler.ts index cdcea1344ef0..9e2f00d565de 100644 --- a/packages/opentelemetry/src/sampler.ts +++ b/packages/opentelemetry/src/sampler.ts @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import type { Attributes, Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; +import type { Context, Span, TraceState as TraceStateInterface } from '@opentelemetry/api'; import { SpanKind, isSpanContextValid, trace } from '@opentelemetry/api'; import { TraceState } from '@opentelemetry/core'; import type { Sampler, SamplingResult } from '@opentelemetry/sdk-trace-base'; @@ -11,16 +11,14 @@ import { SEMATTRS_HTTP_URL, } from '@opentelemetry/semantic-conventions'; import type { Client, SpanAttributes } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '@sentry/core'; +import { baggageHeaderToDynamicSamplingContext } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, hasTracingEnabled, logger, parseSampleRate, sampleSpan } from '@sentry/core'; import { - SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - hasTracingEnabled, - logger, - sampleSpan, -} from '@sentry/core'; -import { + SENTRY_TRACE_STATE_DSC, SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, SENTRY_TRACE_STATE_SAMPLE_RAND, + SENTRY_TRACE_STATE_SAMPLE_RATE, SENTRY_TRACE_STATE_URL, } from './constants'; import { DEBUG_BUILD } from './debug-build'; @@ -106,31 +104,37 @@ export class SentrySampler implements Sampler { // We only sample based on parameters (like tracesSampleRate or tracesSampler) for root spans (which is done in sampleSpan). // Non-root-spans simply inherit the sampling decision from their parent. if (isRootSpan) { - const { isolationScope, scope } = getScopesFromContext(context) ?? {}; - const sampleRand = scope?.getPropagationContext().sampleRand ?? Math.random(); - const [sampled, sampleRate] = sampleSpan( + const { isolationScope } = getScopesFromContext(context) ?? {}; + + const dscString = parentContext?.traceState ? parentContext.traceState.get(SENTRY_TRACE_STATE_DSC) : undefined; + const dsc = dscString ? baggageHeaderToDynamicSamplingContext(dscString) : undefined; + + const sampleRand = parseSampleRate(dsc?.sample_rand) ?? Math.random(); + + const [sampled, sampleRate, localSampleRateWasApplied] = sampleSpan( options, { name: inferredSpanName, attributes: mergedAttributes, normalizedRequest: isolationScope?.getScopeData().sdkProcessingMetadata.normalizedRequest, parentSampled, - // TODO(v9): provide a parentSampleRate here + parentSampleRate: parseSampleRate(dsc?.sample_rate), }, sampleRand, ); - const attributes: Attributes = { - [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: sampleRate, - }; - const method = `${maybeSpanHttpMethod}`.toUpperCase(); if (method === 'OPTIONS' || method === 'HEAD') { DEBUG_BUILD && logger.log(`[Tracing] Not sampling span because HTTP method is '${method}' for ${spanName}`); return { - ...wrapSamplingDecision({ decision: SamplingDecision.NOT_RECORD, context, spanAttributes, sampleRand }), - attributes, + ...wrapSamplingDecision({ + decision: SamplingDecision.NOT_RECORD, + context, + spanAttributes, + sampleRand, + downstreamTraceSampleRate: 0, // we don't want to sample anything in the downstream trace either + }), }; } @@ -149,8 +153,12 @@ export class SentrySampler implements Sampler { context, spanAttributes, sampleRand, + downstreamTraceSampleRate: localSampleRateWasApplied ? sampleRate : undefined, }), - attributes, + attributes: { + // We set the sample rate on the span when a local sample rate was applied to better understand how traces were sampled in Sentry + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: localSampleRateWasApplied ? sampleRate : undefined, + }, }; } else { return { @@ -159,7 +167,6 @@ export class SentrySampler implements Sampler { context, spanAttributes, }), - attributes: {}, }; } } @@ -201,14 +208,24 @@ export function wrapSamplingDecision({ context, spanAttributes, sampleRand, + downstreamTraceSampleRate, }: { decision: SamplingDecision | undefined; context: Context; spanAttributes: SpanAttributes; sampleRand?: number; + downstreamTraceSampleRate?: number; }): SamplingResult { let traceState = getBaseTraceState(context, spanAttributes); + // We will override the propagated sample rate downstream when + // - the tracesSampleRate is applied + // - the tracesSampler is invoked + // Since unsampled OTEL spans (NonRecordingSpans) cannot hold attributes we need to store this on the (trace)context. + if (downstreamTraceSampleRate !== undefined) { + traceState = traceState.set(SENTRY_TRACE_STATE_SAMPLE_RATE, `${downstreamTraceSampleRate}`); + } + if (sampleRand !== undefined) { traceState = traceState.set(SENTRY_TRACE_STATE_SAMPLE_RAND, `${sampleRand}`); } diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 8000c078e3bf..fa2705dec961 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -374,7 +374,6 @@ describe('Integration | Transactions', () => { 'sentry.op': 'test op', 'sentry.origin': 'auto.test', 'sentry.source': 'task', - 'sentry.sample_rate': 1, }, op: 'test op', span_id: expect.stringMatching(/[a-f0-9]{16}/), diff --git a/packages/opentelemetry/test/propagator.test.ts b/packages/opentelemetry/test/propagator.test.ts index 408a151e95ef..48b6372e65a0 100644 --- a/packages/opentelemetry/test/propagator.test.ts +++ b/packages/opentelemetry/test/propagator.test.ts @@ -131,7 +131,6 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', - 'sentry-sample_rate=1', 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', 'sentry-transaction=test', @@ -185,7 +184,6 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', - 'sentry-sample_rate=1', 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', 'sentry-transaction=test', @@ -335,7 +333,6 @@ describe('SentryPropagator', () => { 'sentry-environment=production', 'sentry-release=1.0.0', 'sentry-public_key=abc', - 'sentry-sample_rate=1', 'sentry-sampled=true', 'sentry-trace_id=d4cda95b652f4a1592b449d5929fda1b', 'sentry-transaction=test', diff --git a/packages/opentelemetry/test/sampler.test.ts b/packages/opentelemetry/test/sampler.test.ts index fc38037a7ff2..585e3271a93a 100644 --- a/packages/opentelemetry/test/sampler.test.ts +++ b/packages/opentelemetry/test/sampler.test.ts @@ -60,7 +60,6 @@ describe('SentrySampler', () => { const actual = sampler.shouldSample(ctx, traceId, spanName, spanKind, spanAttributes, links); expect(actual).toEqual({ decision: SamplingDecision.NOT_RECORD, - attributes: {}, traceState: new TraceState().set(SENTRY_TRACE_STATE_SAMPLED_NOT_RECORDING, '1'), }); expect(spyOnDroppedEvent).toHaveBeenCalledTimes(0); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 2c677ded4516..a841bec6ffb6 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -434,7 +434,6 @@ describe('trace', () => { data: { 'sentry.source': 'custom', 'sentry.origin': 'manual', - 'sentry.sample_rate': 1, }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -693,7 +692,6 @@ describe('trace', () => { data: { 'sentry.source': 'custom', 'sentry.origin': 'manual', - 'sentry.sample_rate': 1, }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -990,7 +988,6 @@ describe('trace', () => { data: { 'sentry.source': 'custom', 'sentry.origin': 'manual', - 'sentry.sample_rate': 1, }, parent_span_id: innerParentSpanId, span_id: expect.stringMatching(/[a-f0-9]{16}/),