diff --git a/.size-limit.js b/.size-limit.js index 13b963bacd8d..a46d905fe89f 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -233,7 +233,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '144 KB', + limit: '146 KB', }, { name: '@sentry/node - without tracing', diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 282e075e81ff..ace67c267fd4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -122,6 +122,7 @@ export { instrumentOpenAiClient } from './utils/openai'; export { OPENAI_INTEGRATION_NAME } from './utils/openai/constants'; export type { OpenAiClient, OpenAiOptions, InstrumentedMethod } from './utils/openai/types'; export type { FeatureFlag } from './utils/featureFlags'; + export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, @@ -219,6 +220,7 @@ export { extractTraceparentData, generateSentryTraceHeader, propagationContextFromHeaders, + shouldContinueTrace, } from './utils/tracing'; export { getSDKSource, isBrowserBundle } from './utils/env'; export type { SdkSource } from './utils/env'; diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index adbcf0ae032a..47d5657a7d87 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -10,7 +10,7 @@ import { import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { Span } from '../types-hoist/span'; import { baggageHeaderToDynamicSamplingContext, dynamicSamplingContextToSentryBaggageHeader } from '../utils/baggage'; -import { extractOrgIdFromDsnHost } from '../utils/dsn'; +import { extractOrgIdFromClient } from '../utils/dsn'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; import { addNonEnumerableProperty } from '../utils/object'; import { getRootSpan, spanIsSampled, spanToJSON } from '../utils/spanUtils'; @@ -42,14 +42,7 @@ export function freezeDscOnSpan(span: Span, dsc: Partial export function getDynamicSamplingContextFromClient(trace_id: string, client: Client): DynamicSamplingContext { const options = client.getOptions(); - const { publicKey: public_key, host } = client.getDsn() || {}; - - let org_id: string | undefined; - if (options.orgId) { - org_id = String(options.orgId); - } else if (host) { - org_id = extractOrgIdFromDsnHost(host); - } + const { publicKey: public_key } = client.getDsn() || {}; // Instead of conditionally adding non-undefined values, we add them and then remove them if needed // otherwise, the order of baggage entries changes, which "breaks" a bunch of tests etc. @@ -58,7 +51,7 @@ export function getDynamicSamplingContextFromClient(trace_id: string, client: Cl release: options.release, public_key, trace_id, - org_id, + org_id: extractOrgIdFromClient(client), }; client.emit('createDsc', dsc); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index ba6df741508c..98c3a33a8a79 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -11,6 +11,7 @@ import type { DynamicSamplingContext } from '../types-hoist/envelope'; import type { ClientOptions } from '../types-hoist/options'; import type { SentrySpanArguments, Span, SpanTimeInput } from '../types-hoist/span'; import type { StartSpanOptions } from '../types-hoist/startSpanOptions'; +import { baggageHeaderToDynamicSamplingContext } from '../utils/baggage'; import { debug } from '../utils/debug-logger'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasSpansEnabled } from '../utils/hasSpansEnabled'; @@ -18,7 +19,7 @@ import { parseSampleRate } from '../utils/parseSampleRate'; import { generateTraceId } from '../utils/propagationContext'; import { _getSpanForScope, _setSpanForScope } from '../utils/spanOnScope'; import { addChildSpanToSpan, getRootSpan, spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../utils/spanUtils'; -import { propagationContextFromHeaders } from '../utils/tracing'; +import { propagationContextFromHeaders, shouldContinueTrace } from '../utils/tracing'; import { freezeDscOnSpan, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { logSpanStart } from './logSpans'; import { sampleSpan } from './sampling'; @@ -216,6 +217,12 @@ export const continueTrace = ( const { sentryTrace, baggage } = options; + const client = getClient(); + const incomingDsc = baggageHeaderToDynamicSamplingContext(baggage); + if (client && !shouldContinueTrace(client, incomingDsc?.org_id)) { + return startNewTrace(callback); + } + return withScope(scope => { const propagationContext = propagationContextFromHeaders(sentryTrace, baggage); scope.setPropagationContext(propagationContext); diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts index 7d542b1cb655..ce0851f940a5 100644 --- a/packages/core/src/types-hoist/options.ts +++ b/packages/core/src/types-hoist/options.ts @@ -302,10 +302,23 @@ export interface ClientOptions { expect(result).toEqual('aha'); }); + + describe('strictTraceContinuation', () => { + const creatOrgIdInDsn = (orgId: number) => { + vi.spyOn(client, 'getDsn').mockReturnValue({ + host: `o${orgId}.ingest.sentry.io`, + protocol: 'https', + projectId: 'projId', + }); + }; + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('continues trace when org IDs match', () => { + creatOrgIdInDsn(123); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + + it('starts new trace when both SDK and baggage org IDs are set and do not match', () => { + creatOrgIdInDsn(123); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=456', + }, + () => { + return getCurrentScope(); + }, + ); + + // Should start a new trace with a different trace ID + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + describe('when strictTraceContinuation is true', () => { + it('starts new trace when baggage org ID is missing', () => { + client.getOptions().strictTraceContinuation = true; + + creatOrgIdInDsn(123); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + // Should start a new trace with a different trace ID + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + it('starts new trace when SDK org ID is missing', () => { + client.getOptions().strictTraceContinuation = true; + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + // Should start a new trace with a different trace ID + expect(scope.getPropagationContext().traceId).not.toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBeUndefined(); + }); + + it('continues trace when both org IDs are missing', () => { + client.getOptions().strictTraceContinuation = true; + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + // Should continue the trace + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + }); + + describe('when strictTraceContinuation is false', () => { + it('continues trace when baggage org ID is missing', () => { + client.getOptions().strictTraceContinuation = false; + + creatOrgIdInDsn(123); + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-environment=production', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + + it('SDK org ID is missing', () => { + client.getOptions().strictTraceContinuation = false; + + const scope = continueTrace( + { + sentryTrace: '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-org_id=123', + }, + () => { + return getCurrentScope(); + }, + ); + + expect(scope.getPropagationContext().traceId).toBe('12312012123120121231201212312012'); + expect(scope.getPropagationContext().parentSpanId).toBe('1121201211212012'); + }); + }); + }); }); describe('getActiveSpan', () => { diff --git a/packages/core/test/lib/utils/dsn.test.ts b/packages/core/test/lib/utils/dsn.test.ts index 5d1cfbe2b538..0555ae583c02 100644 --- a/packages/core/test/lib/utils/dsn.test.ts +++ b/packages/core/test/lib/utils/dsn.test.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, test, vi } from 'vitest'; import { DEBUG_BUILD } from '../../../src/debug-build'; import { debug } from '../../../src/utils/debug-logger'; -import { dsnToString, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; +import { dsnToString, extractOrgIdFromClient, extractOrgIdFromDsnHost, makeDsn } from '../../../src/utils/dsn'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; function testIf(condition: boolean) { return condition ? test : test.skip; @@ -247,3 +248,53 @@ describe('extractOrgIdFromDsnHost', () => { expect(extractOrgIdFromDsnHost('')).toBeUndefined(); }); }); + +describe('extractOrgIdFromClient', () => { + let client: TestClient; + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('returns orgId from client options when available', () => { + client = new TestClient( + getDefaultTestClientOptions({ + orgId: '00222111', + dsn: 'https://public@sentry.example.com/1', + }), + ); + + const result = extractOrgIdFromClient(client); + expect(result).toBe('00222111'); + }); + + test('converts non-string orgId to string', () => { + client = new TestClient( + getDefaultTestClientOptions({ + orgId: 12345, + dsn: 'https://public@sentry.example.com/1', + }), + ); + + const result = extractOrgIdFromClient(client); + expect(result).toBe('12345'); + }); + + test('extracts orgId from DSN host when options.orgId is not available', () => { + client = new TestClient( + getDefaultTestClientOptions({ + dsn: 'https://public@o012300.example.com/1', + }), + ); + + const result = extractOrgIdFromClient(client); + expect(result).toBe('012300'); + }); + + test('returns undefined when neither options.orgId nor DSN host are available', () => { + client = new TestClient(getDefaultTestClientOptions({})); + + const result = extractOrgIdFromClient(client); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/core/test/lib/utils/tracing.test.ts b/packages/core/test/lib/utils/tracing.test.ts index ea99555e70e1..ea41190f3bb3 100644 --- a/packages/core/test/lib/utils/tracing.test.ts +++ b/packages/core/test/lib/utils/tracing.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it, test } from 'vitest'; -import { extractTraceparentData, propagationContextFromHeaders } from '../../../src/utils/tracing'; +import { extractTraceparentData, propagationContextFromHeaders, shouldContinueTrace } from '../../../src/utils/tracing'; +import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; const EXAMPLE_SENTRY_TRACE = '12312012123120121231201212312012-1121201211212012-1'; const EXAMPLE_BAGGAGE = 'sentry-release=1.2.3,sentry-foo=bar,other=baz,sentry-sample_rand=0.42'; @@ -124,3 +125,55 @@ describe('extractTraceparentData', () => { expect(extractTraceparentData('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-bbbbbbbbbbbbbbbb-x')).toBeUndefined(); }); }); + +describe('shouldContinueTrace', () => { + test('returns true when both baggage and SDK org IDs are undefined', () => { + const client = new TestClient(getDefaultTestClientOptions({})); + + const result = shouldContinueTrace(client, undefined); + expect(result).toBe(true); + }); + + test('returns true when org IDs match', () => { + const orgId = '123456'; + const client = new TestClient(getDefaultTestClientOptions({ orgId })); + + const result = shouldContinueTrace(client, orgId); + expect(result).toBe(true); + }); + + test('returns false when org IDs do not match', () => { + const client = new TestClient(getDefaultTestClientOptions({ orgId: '123456' })); + + const result = shouldContinueTrace(client, '654321'); + expect(result).toBe(false); + }); + + test('returns true when baggage org ID is undefined and strictTraceContinuation is false', () => { + const client = new TestClient(getDefaultTestClientOptions({ orgId: '123456', strictTraceContinuation: false })); + + const result = shouldContinueTrace(client, undefined); + expect(result).toBe(true); + }); + + test('returns true when SDK org ID is undefined and strictTraceContinuation is false', () => { + const client = new TestClient(getDefaultTestClientOptions({ strictTraceContinuation: false })); + + const result = shouldContinueTrace(client, '123456'); + expect(result).toBe(true); + }); + + test('returns false when baggage org ID is undefined and strictTraceContinuation is true', () => { + const client = new TestClient(getDefaultTestClientOptions({ orgId: '123456', strictTraceContinuation: true })); + + const result = shouldContinueTrace(client, undefined); + expect(result).toBe(false); + }); + + test('returns false when SDK org ID is undefined and strictTraceContinuation is true', () => { + const client = new TestClient(getDefaultTestClientOptions({ strictTraceContinuation: true })); + + const result = shouldContinueTrace(client, '123456'); + expect(result).toBe(false); + }); +}); diff --git a/packages/core/tsconfig.test.json b/packages/core/tsconfig.test.json index c2b522d04ee1..0db9ad3bf16c 100644 --- a/packages/core/tsconfig.test.json +++ b/packages/core/tsconfig.test.json @@ -5,6 +5,7 @@ "compilerOptions": { "lib": ["DOM", "ES2018"], + "module": "ESNext", // support dynamic import() // should include all types from `./tsconfig.json` plus types for all test frameworks used "types": ["node"] diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 40afc10120df..7dc005521aa7 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -4,6 +4,7 @@ import { isTracingSuppressed, W3CBaggagePropagator } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { Client, continueTrace, DynamicSamplingContext, Options, Scope } from '@sentry/core'; import { + baggageHeaderToDynamicSamplingContext, debug, generateSentryTraceHeader, getClient, @@ -15,6 +16,7 @@ import { parseBaggageHeader, propagationContextFromHeaders, SENTRY_BAGGAGE_KEY_PREFIX, + shouldContinueTrace, spanToJSON, stringMatchesSomePattern, } from '@sentry/core'; @@ -212,9 +214,12 @@ function getContextWithRemoteActiveSpan( const { traceId, parentSpanId, sampled, dsc } = propagationContext; + const client = getClient(); + const incomingDsc = baggageHeaderToDynamicSamplingContext(baggage); + // We only want to set the virtual span if we are continuing a concrete trace // Otherwise, we ignore the incoming trace here, e.g. if we have no trace headers - if (!parentSpanId) { + if (!parentSpanId || (client && !shouldContinueTrace(client, incomingDsc?.org_id))) { return ctx; } diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index e604d83bd4d8..c30377fe7763 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -259,7 +259,7 @@ export function continueTrace(options: Parameters[0 /** * Get the trace context for a given scope. - * We have a custom implemention here because we need an OTEL-specific way to get the span from a scope. + * We have a custom implementation here because we need an OTEL-specific way to get the span from a scope. */ export function getTraceContextForScope( client: Client,