From ee058e6af44b8f6ffc5367f6ce28841e92653973 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Apr 2024 10:04:31 +0200 Subject: [PATCH 1/4] fix(node): Ensure tracing is suppressed for all generated requests This exports a new `suppressTracing` utility from `@sentry/opentelemetry` which can be used to wrap a callback to avoid tracing inside of it. We use this for bun transport, and for nextjs error symbolication. --- packages/bun/package.json | 1 + packages/bun/src/transports/index.ts | 19 ++++++++------ .../devErrorSymbolicationEventProcessor.ts | 25 +++++++++++-------- packages/node/src/transports/http.ts | 5 ++-- packages/opentelemetry/src/index.ts | 2 ++ .../src/utils/suppressTracing.ts | 8 ++++++ 6 files changed, 38 insertions(+), 22 deletions(-) create mode 100644 packages/opentelemetry/src/utils/suppressTracing.ts diff --git a/packages/bun/package.json b/packages/bun/package.json index d5454c17c88c..9bad7751c615 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -44,6 +44,7 @@ "dependencies": { "@sentry/core": "8.0.0-alpha.9", "@sentry/node": "8.0.0-alpha.9", + "@sentry/opentelemetry": "8.0.0-alpha.9", "@sentry/types": "8.0.0-alpha.9", "@sentry/utils": "8.0.0-alpha.9" }, diff --git a/packages/bun/src/transports/index.ts b/packages/bun/src/transports/index.ts index ad9832795fc4..15468a6a7a39 100644 --- a/packages/bun/src/transports/index.ts +++ b/packages/bun/src/transports/index.ts @@ -1,4 +1,5 @@ import { createTransport } from '@sentry/core'; +import { suppressTracing } from '@sentry/opentelemetry'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { rejectedSyncPromise } from '@sentry/utils'; @@ -19,14 +20,16 @@ export function makeFetchTransport(options: BunTransportOptions): Transport { }; try { - return fetch(options.url, requestOptions).then(response => { - return { - statusCode: response.status, - headers: { - 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), - 'retry-after': response.headers.get('Retry-After'), - }, - }; + return suppressTracing(() => { + return fetch(options.url, requestOptions).then(response => { + return { + statusCode: response.status, + headers: { + 'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'), + 'retry-after': response.headers.get('Retry-After'), + }, + }; + }); }); } catch (e) { return rejectedSyncPromise(e); diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts index 525322e3784c..b3795d566525 100644 --- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts +++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts @@ -1,3 +1,4 @@ +import { suppressTracing } from '@sentry/opentelemetry'; import type { Event, EventHint } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; import type { StackFrame } from 'stacktrace-parser'; @@ -40,17 +41,19 @@ async function resolveStackFrame( const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 3000); - const res = await fetch( - `${ - // eslint-disable-next-line no-restricted-globals - typeof window === 'undefined' ? 'http://localhost:3000' : '' // TODO: handle the case where users define a different port - }${basePath}/__nextjs_original-stack-frame?${params.toString()}`, - { - signal: controller.signal, - }, - ).finally(() => { - clearTimeout(timer); - }); + const res = await suppressTracing(() => + fetch( + `${ + // eslint-disable-next-line no-restricted-globals + typeof window === 'undefined' ? 'http://localhost:3000' : '' // TODO: handle the case where users define a different port + }${basePath}/__nextjs_original-stack-frame?${params.toString()}`, + { + signal: controller.signal, + }, + ).finally(() => { + clearTimeout(timer); + }), + ); if (!res.ok || res.status === 204) { return null; diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index 4cbe7ece1f60..d1ff9ef70813 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -2,9 +2,8 @@ import * as http from 'node:http'; import * as https from 'node:https'; import { Readable } from 'stream'; import { createGzip } from 'zlib'; -import { context } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; import { createTransport } from '@sentry/core'; +import { suppressTracing } from '@sentry/opentelemetry'; import type { BaseTransportOptions, Transport, @@ -82,7 +81,7 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); // This ensures we do not generate any spans in OpenTelemetry for the transport - return context.with(suppressTracing(context.active()), () => { + return suppressTracing(() => { const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); return createTransport(options, requestExecutor); }); diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index 43f183be2139..3d727c707897 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -23,6 +23,8 @@ export { isSentryRequestSpan } from './utils/isSentryRequest'; export { getActiveSpan } from './utils/getActiveSpan'; export { startSpan, startSpanManual, startInactiveSpan, withActiveSpan, continueTrace } from './trace'; +export { suppressTracing } from './utils/suppressTracing'; + // eslint-disable-next-line deprecation/deprecation export { setupGlobalHub } from './custom/hub'; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/opentelemetry/src/utils/suppressTracing.ts b/packages/opentelemetry/src/utils/suppressTracing.ts new file mode 100644 index 000000000000..38813fd3239c --- /dev/null +++ b/packages/opentelemetry/src/utils/suppressTracing.ts @@ -0,0 +1,8 @@ +import { context } from '@opentelemetry/api'; +import { suppressTracing as suppressTracingImpl } from '@opentelemetry/core'; + +/** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */ +export function suppressTracing(callback: () => T): T { + const ctx = suppressTracingImpl(context.active()); + return context.with(ctx, callback); +} From 34f79d7e67e666edf81f1de26c3bcd614277da66 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Apr 2024 15:21:56 +0200 Subject: [PATCH 2/4] WIP --- packages/core/src/asyncContext.ts | 5 +++- packages/core/src/constants.ts | 2 ++ packages/core/src/tracing/trace.ts | 39 ++++++++++++++++++++++-------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/packages/core/src/asyncContext.ts b/packages/core/src/asyncContext.ts index 854b03ea9600..4627d4c2453d 100644 --- a/packages/core/src/asyncContext.ts +++ b/packages/core/src/asyncContext.ts @@ -1,7 +1,7 @@ import type { Hub, Integration } from '@sentry/types'; import type { Scope } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; -import type { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from './tracing/trace'; +import type { startInactiveSpan, startSpan, startSpanManual, suppressTracing, withActiveSpan } from './tracing/trace'; import type { getActiveSpan } from './utils/spanUtils'; /** @@ -62,6 +62,9 @@ export interface AsyncContextStrategy { /** Make a span the active span in the context of the callback. */ withActiveSpan?: typeof withActiveSpan; + + /** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */ + suppressTracing?: typeof suppressTracing; } /** diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 38475b857ace..90a7ad701d1b 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1 +1,3 @@ export const DEFAULT_ENVIRONMENT = 'production'; + +export const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d21a567cecc5..0d9d1dcabb8e 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -3,6 +3,7 @@ import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, St import { propagationContextFromHeaders } from '@sentry/utils'; import type { AsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../asyncContext'; +import { SUPPRESS_TRACING_KEY } from '../constants'; import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { getAsyncContextStrategy } from '../hub'; @@ -204,6 +205,20 @@ export function withActiveSpan(span: Span | null, callback: (scope: Scope) => }); } +/** Suppress tracing in the given callback, ensuring no spans are generated inside of it. */ +export function suppressTracing(callback: () => T): T { + const acs = getAcs(); + + if (acs.suppressTracing) { + return acs.suppressTracing(callback); + } + + return withScope(scope => { + scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); + return callback(); + }); +} + function createChildSpanOrTransaction({ parentSpan, spanContext, @@ -237,6 +252,7 @@ function createChildSpanOrTransaction({ parentSpanId, ...spanContext, }, + scope, parentSampled, ); @@ -258,6 +274,7 @@ function createChildSpanOrTransaction({ parentSpanId, ...spanContext, }, + scope, parentSampled, ); @@ -296,20 +313,22 @@ function getAcs(): AsyncContextStrategy { return getAsyncContextStrategy(carrier); } -function _startRootSpan(spanArguments: SentrySpanArguments, parentSampled?: boolean): SentrySpan { +function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parentSampled?: boolean): SentrySpan { const client = getClient(); const options: Partial = (client && client.getOptions()) || {}; const { name = '', attributes } = spanArguments; - const [sampled, sampleRate] = sampleSpan(options, { - name, - parentSampled, - attributes, - transactionContext: { - name, - parentSampled, - }, - }); + const [sampled, sampleRate] = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] + ? [false] + : sampleSpan(options, { + name, + parentSampled, + attributes, + transactionContext: { + name, + parentSampled, + }, + }); const transaction = new SentrySpan({ ...spanArguments, From 65d4c30fa64e72d820f707b86c032f3809806798 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Apr 2024 16:08:53 +0200 Subject: [PATCH 3/4] properly implement suppressTracing --- packages/bun/src/transports/index.ts | 3 +- packages/core/src/constants.ts | 2 - packages/core/src/tracing/index.ts | 1 + packages/core/src/tracing/trace.ts | 32 ++++--- packages/core/test/lib/tracing/trace.test.ts | 86 ++++++++++++++++--- .../devErrorSymbolicationEventProcessor.ts | 2 +- packages/node/src/transports/http.ts | 3 +- .../opentelemetry/src/asyncContextStrategy.ts | 2 + packages/opentelemetry/test/trace.test.ts | 53 ++++++++++++ 9 files changed, 152 insertions(+), 32 deletions(-) diff --git a/packages/bun/src/transports/index.ts b/packages/bun/src/transports/index.ts index 15468a6a7a39..b4968f2001c0 100644 --- a/packages/bun/src/transports/index.ts +++ b/packages/bun/src/transports/index.ts @@ -1,5 +1,4 @@ -import { createTransport } from '@sentry/core'; -import { suppressTracing } from '@sentry/opentelemetry'; +import { createTransport, suppressTracing } from '@sentry/core'; import type { BaseTransportOptions, Transport, TransportMakeRequestResponse, TransportRequest } from '@sentry/types'; import { rejectedSyncPromise } from '@sentry/utils'; diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index 90a7ad701d1b..38475b857ace 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -1,3 +1 @@ export const DEFAULT_ENVIRONMENT = 'production'; - -export const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 7cc86ae76531..a01fca60316a 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -14,6 +14,7 @@ export { startSpanManual, continueTrace, withActiveSpan, + suppressTracing, } from './trace'; export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; export { setMeasurement, timedEventsToMeasurements } from './measurement'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 0d9d1dcabb8e..c9e82163fef1 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -3,7 +3,7 @@ import type { ClientOptions, Scope, SentrySpanArguments, Span, SpanTimeInput, St import { propagationContextFromHeaders } from '@sentry/utils'; import type { AsyncContextStrategy } from '../asyncContext'; import { getMainCarrier } from '../asyncContext'; -import { SUPPRESS_TRACING_KEY } from '../constants'; + import { getClient, getCurrentScope, getIsolationScope, withScope } from '../currentScopes'; import { getAsyncContextStrategy } from '../hub'; @@ -26,6 +26,8 @@ import { SentrySpan } from './sentrySpan'; import { SPAN_STATUS_ERROR } from './spanstatus'; import { setCapturedScopesOnSpan } from './utils'; +const SUPPRESS_TRACING_KEY = '__SENTRY_SUPPRESS_TRACING__'; + /** * Wraps a function with a transaction/span and finishes the span after the function is done. * The created span is the active span and will be used as parent by other spans created inside the function @@ -238,7 +240,7 @@ function createChildSpanOrTransaction({ let span: Span; if (parentSpan && !forceTransaction) { - span = _startChildSpan(parentSpan, spanContext); + span = _startChildSpan(parentSpan, scope, spanContext); addChildSpanToSpan(parentSpan, span); } else if (parentSpan) { // If we forced a transaction but have a parent span, make sure to continue from the parent span, not the scope @@ -330,7 +332,7 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent }, }); - const transaction = new SentrySpan({ + const rootSpan = new SentrySpan({ ...spanArguments, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', @@ -339,30 +341,32 @@ function _startRootSpan(spanArguments: SentrySpanArguments, scope: Scope, parent sampled, }); if (sampleRate !== undefined) { - transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } if (client) { - client.emit('spanStart', transaction); + client.emit('spanStart', rootSpan); } - return transaction; + return rootSpan; } /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * This inherits the sampling decision from the parent span. */ -function _startChildSpan(parentSpan: Span, spanArguments: SentrySpanArguments): SentrySpan { +function _startChildSpan(parentSpan: Span, scope: Scope, spanArguments: SentrySpanArguments): Span { const { spanId, traceId } = parentSpan.spanContext(); - const sampled = spanIsSampled(parentSpan); + const sampled = scope.getScopeData().sdkProcessingMetadata[SUPPRESS_TRACING_KEY] ? false : spanIsSampled(parentSpan); - const childSpan = new SentrySpan({ - ...spanArguments, - parentSpanId: spanId, - traceId, - sampled, - }); + const childSpan = sampled + ? new SentrySpan({ + ...spanArguments, + parentSpanId: spanId, + traceId, + sampled, + }) + : new SentryNonRecordingSpan(); addChildSpanToSpan(parentSpan, childSpan); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 02144174ef1f..7c3962021b2f 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -20,11 +20,12 @@ import { startInactiveSpan, startSpan, startSpanManual, + suppressTracing, withActiveSpan, } from '../../../src/tracing'; import { SentryNonRecordingSpan } from '../../../src/tracing/sentryNonRecordingSpan'; import { _setSpanForScope } from '../../../src/utils/spanOnScope'; -import { getActiveSpan, getRootSpan, getSpanDescendants } from '../../../src/utils/spanUtils'; +import { getActiveSpan, getRootSpan, getSpanDescendants, spanIsSampled } from '../../../src/utils/spanUtils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -259,7 +260,7 @@ describe('startSpan', () => { const initialScope = getCurrentScope(); const manualScope = initialScope.clone(); - const parentSpan = new SentrySpan({ spanId: 'parent-span-id' }); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); _setSpanForScope(manualScope, parentSpan); startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { @@ -490,7 +491,7 @@ describe('startSpan', () => { }); it('uses implementation from ACS, if it exists', () => { - const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticSpan = new SentrySpan({ spanId: 'aha', sampled: true }); const carrier = getMainCarrier(); @@ -575,7 +576,7 @@ describe('startSpanManual', () => { const initialScope = getCurrentScope(); const manualScope = initialScope.clone(); - const parentSpan = new SentrySpan({ spanId: 'parent-span-id' }); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); _setSpanForScope(manualScope, parentSpan); startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => { @@ -761,7 +762,7 @@ describe('startSpanManual', () => { }); it('uses implementation from ACS, if it exists', () => { - const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticSpan = new SentrySpan({ spanId: 'aha', sampled: true }); const carrier = getMainCarrier(); @@ -840,7 +841,7 @@ describe('startInactiveSpan', () => { const initialScope = getCurrentScope(); const manualScope = initialScope.clone(); - const parentSpan = new SentrySpan({ spanId: 'parent-span-id' }); + const parentSpan = new SentrySpan({ spanId: 'parent-span-id', sampled: true }); _setSpanForScope(manualScope, parentSpan); const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); @@ -994,7 +995,7 @@ describe('startInactiveSpan', () => { }); }); - it('includes the scope at the time the span was started when finished xxx', async () => { + it('includes the scope at the time the span was started when finished', async () => { const beforeSendTransaction = jest.fn(event => event); const client = new TestClient( @@ -1048,7 +1049,7 @@ describe('startInactiveSpan', () => { }); it('uses implementation from ACS, if it exists', () => { - const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticSpan = new SentrySpan({ spanId: 'aha', sampled: true }); const carrier = getMainCarrier(); @@ -1206,7 +1207,7 @@ describe('getActiveSpan', () => { }); it('works with an active span on the scope', () => { - const activeSpan = new SentrySpan({ spanId: 'aha' }); + const activeSpan = new SentrySpan({ spanId: 'aha', sampled: true }); withActiveSpan(activeSpan, () => { const span = getActiveSpan(); @@ -1215,7 +1216,7 @@ describe('getActiveSpan', () => { }); it('uses implementation from ACS, if it exists', () => { - const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticSpan = new SentrySpan({ spanId: 'aha', sampled: true }); const carrier = getMainCarrier(); @@ -1285,7 +1286,7 @@ describe('withActiveSpan()', () => { }); it('uses implementation from ACS, if it exists', () => { - const staticSpan = new SentrySpan({ spanId: 'aha' }); + const staticSpan = new SentrySpan({ spanId: 'aha', sampled: true }); const staticScope = new Scope(); const carrier = getMainCarrier(); @@ -1359,3 +1360,66 @@ describe('span hooks', () => { expect(endedSpans).toEqual(['span5', 'span3', 'span2', 'span1']); }); }); + +describe('suppressTracing', () => { + beforeEach(() => { + addTracingExtensions(); + + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + + setAsyncContextStrategy(undefined); + + const options = getDefaultTestClientOptions({ tracesSampleRate: 1 }); + client = new TestClient(options); + setCurrentClient(client); + client.init(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('works for a root span', () => { + const span = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + expect(span.isRecording()).toBe(false); + expect(spanIsSampled(span)).toBe(false); + }); + + it('works for a child span', () => { + startSpan({ name: 'outer' }, span => { + expect(span.isRecording()).toBe(true); + expect(spanIsSampled(span)).toBe(true); + + const child1 = startInactiveSpan({ name: 'inner1' }); + + expect(child1.isRecording()).toBe(true); + expect(spanIsSampled(child1)).toBe(true); + + const child2 = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + expect(child2.isRecording()).toBe(false); + expect(spanIsSampled(child2)).toBe(false); + }); + }); + + it('works for a child span with forceTransaction=true', () => { + startSpan({ name: 'outer' }, span => { + expect(span.isRecording()).toBe(true); + expect(spanIsSampled(span)).toBe(true); + + const child = suppressTracing(() => { + return startInactiveSpan({ name: 'span', forceTransaction: true }); + }); + + expect(child.isRecording()).toBe(false); + expect(spanIsSampled(child)).toBe(false); + }); + }); +}); diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts index b3795d566525..0af455e8b2f7 100644 --- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts +++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts @@ -1,4 +1,4 @@ -import { suppressTracing } from '@sentry/opentelemetry'; +import { suppressTracing } from '@sentry/core'; import type { Event, EventHint } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; import type { StackFrame } from 'stacktrace-parser'; diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index d1ff9ef70813..d4d7435bdc6d 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -2,8 +2,7 @@ import * as http from 'node:http'; import * as https from 'node:https'; import { Readable } from 'stream'; import { createGzip } from 'zlib'; -import { createTransport } from '@sentry/core'; -import { suppressTracing } from '@sentry/opentelemetry'; +import { createTransport, suppressTracing } from '@sentry/core'; import type { BaseTransportOptions, Transport, diff --git a/packages/opentelemetry/src/asyncContextStrategy.ts b/packages/opentelemetry/src/asyncContextStrategy.ts index 75169ab933ed..b7f66870d07f 100644 --- a/packages/opentelemetry/src/asyncContextStrategy.ts +++ b/packages/opentelemetry/src/asyncContextStrategy.ts @@ -13,6 +13,7 @@ import { startInactiveSpan, startSpan, startSpanManual, withActiveSpan } from '. import type { CurrentScopes } from './types'; import { getScopesFromContext } from './utils/contextData'; import { getActiveSpan } from './utils/getActiveSpan'; +import { suppressTracing } from './utils/suppressTracing'; /** * Sets the async context strategy to use follow the OTEL context under the hood. @@ -122,5 +123,6 @@ export function setOpenTelemetryContextAsyncContextStrategy(): void { // The types here don't fully align, because our own `Span` type is narrower // than the OTEL one - but this is OK for here, as we now we'll only have OTEL spans passed around withActiveSpan: withActiveSpan as typeof defaultWithActiveSpan, + suppressTracing: suppressTracing, }); } diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index a871fe1fbeba..4ef8a8d90f6d 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -15,6 +15,7 @@ import { getRootSpan, spanIsSampled, spanToJSON, + suppressTracing, withScope, } from '@sentry/core'; import type { Event, Scope } from '@sentry/types'; @@ -1575,6 +1576,58 @@ describe('continueTrace', () => { }); }); +describe('suppressTracing', () => { + beforeEach(() => { + mockSdkInit({ enableTracing: true }); + }); + + afterEach(() => { + cleanupOtel(); + }); + + it('works for a root span', () => { + const span = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + expect(span.isRecording()).toBe(false); + expect(spanIsSampled(span)).toBe(false); + }); + + it('works for a child span', () => { + startSpan({ name: 'outer' }, span => { + expect(span.isRecording()).toBe(true); + expect(spanIsSampled(span)).toBe(true); + + const child1 = startInactiveSpan({ name: 'inner1' }); + + expect(child1.isRecording()).toBe(true); + expect(spanIsSampled(child1)).toBe(true); + + const child2 = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + expect(child2.isRecording()).toBe(false); + expect(spanIsSampled(child2)).toBe(false); + }); + }); + + it('works for a child span with forceTransaction=true', () => { + startSpan({ name: 'outer' }, span => { + expect(span.isRecording()).toBe(true); + expect(spanIsSampled(span)).toBe(true); + + const child = suppressTracing(() => { + return startInactiveSpan({ name: 'span', forceTransaction: true }); + }); + + expect(child.isRecording()).toBe(false); + expect(spanIsSampled(child)).toBe(false); + }); + }); +}); + function getSpanName(span: AbstractSpan): string | undefined { return spanHasName(span) ? span.name : undefined; } From a685253d4fc621173c1fa4f25ad1ceeb86e9439d Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Apr 2024 16:46:36 +0200 Subject: [PATCH 4/4] fix tests --- packages/browser-utils/test/browser/metrics/index.test.ts | 4 ++-- packages/browser-utils/test/browser/metrics/utils.test.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-utils/test/browser/metrics/index.test.ts b/packages/browser-utils/test/browser/metrics/index.test.ts index b0015ca0a514..8c4476d4b6b2 100644 --- a/packages/browser-utils/test/browser/metrics/index.test.ts +++ b/packages/browser-utils/test/browser/metrics/index.test.ts @@ -32,7 +32,7 @@ const originalLocation = WINDOW.location; const resourceEntryName = 'https://example.com/assets/to/css'; describe('_addMeasureSpans', () => { - const span = new SentrySpan({ op: 'pageload', name: '/' }); + const span = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); beforeEach(() => { getCurrentScope().clear(); @@ -86,7 +86,7 @@ describe('_addMeasureSpans', () => { }); describe('_addResourceSpans', () => { - const span = new SentrySpan({ op: 'pageload', name: '/' }); + const span = new SentrySpan({ op: 'pageload', name: '/', sampled: true }); beforeAll(() => { setGlobalLocation(mockWindowLocation); diff --git a/packages/browser-utils/test/browser/metrics/utils.test.ts b/packages/browser-utils/test/browser/metrics/utils.test.ts index ad9d0dc801f8..1ec79514af67 100644 --- a/packages/browser-utils/test/browser/metrics/utils.test.ts +++ b/packages/browser-utils/test/browser/metrics/utils.test.ts @@ -26,7 +26,7 @@ describe('startAndEndSpan()', () => { }); it('creates a span with given properties', () => { - const parentSpan = new SentrySpan({ name: 'test' }); + const parentSpan = new SentrySpan({ name: 'test', sampled: true }); const span = startAndEndSpan(parentSpan, 100, 200, { name: 'evaluation', op: 'script', @@ -40,7 +40,7 @@ describe('startAndEndSpan()', () => { }); it('adjusts the start timestamp if child span starts before transaction', () => { - const parentSpan = new SentrySpan({ name: 'test', startTimestamp: 123 }); + const parentSpan = new SentrySpan({ name: 'test', startTimestamp: 123, sampled: true }); const span = startAndEndSpan(parentSpan, 100, 200, { name: 'script.js', op: 'resource', @@ -52,7 +52,7 @@ describe('startAndEndSpan()', () => { }); it('does not adjust start timestamp if child span starts after transaction', () => { - const parentSpan = new SentrySpan({ name: 'test', startTimestamp: 123 }); + const parentSpan = new SentrySpan({ name: 'test', startTimestamp: 123, sampled: true }); const span = startAndEndSpan(parentSpan, 150, 200, { name: 'script.js', op: 'resource',