diff --git a/packages/remix/src/server/integrations/tracing-channel.ts b/packages/remix/src/server/integrations/tracing-channel.ts index 8e93813a19dd..65e6bda2e463 100644 --- a/packages/remix/src/server/integrations/tracing-channel.ts +++ b/packages/remix/src/server/integrations/tracing-channel.ts @@ -192,6 +192,7 @@ function subscribeCallRouteLoader(): void { // With span streaming, a `function` span is named after the function it wraps. The route id // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'loader' : description, + onlyIfParent: true, attributes: { [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, @@ -203,7 +204,6 @@ function subscribeCallRouteLoader(): void { }); }, { - requiresParentSpan: true, beforeSpanEnd: (span, data) => setResponseStatus(span, data.result), }, ); @@ -231,6 +231,7 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined): // With span streaming, a `function` span is named after the function it wraps. The route id // stays on `match.route.id`. name: client && hasSpanStreamingEnabled(client) ? 'action' : description, + onlyIfParent: true, attributes: { [SENTRY_ORIGIN]: ORIGIN, [SENTRY_OP]: FUNCTION, @@ -242,7 +243,6 @@ function subscribeCallRouteAction(formDataCapture: FormDataCapture | undefined): }); }, { - requiresParentSpan: true, beforeSpanEnd: (span, data) => setResponseStatus(span, data.result), // Hold the span end until the (already in-flight) form-data read resolves, then apply the // attributes and end (which sets the response status via `beforeSpanEnd`). On error, or when diff --git a/packages/remix/test/server/tracing-channel.test.ts b/packages/remix/test/server/tracing-channel.test.ts index c11200f643b1..f31f9c693439 100644 --- a/packages/remix/test/server/tracing-channel.test.ts +++ b/packages/remix/test/server/tracing-channel.test.ts @@ -27,7 +27,6 @@ describe('remixIntegration (Orchestrion-based)', () => { beforeEach(() => { span = makeSpan(); startInactiveSpanSpy = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span); - // A truthy active span by default, so the `requiresParentSpan` gate passes. getActiveSpanSpy = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as Span); }); @@ -129,13 +128,13 @@ describe('remixIntegration (Orchestrion-based)', () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it('callRouteLoader: does not create a span without an active parent span', async () => { + it('callRouteLoader: does not create a recording span without an active parent span', async () => { getActiveSpanSpy.mockReturnValue(undefined); const ctx = { arguments: [{ routeId: 'x', request: makeRequest(), params: {} }] }; await tracingChannel(remixChannels.REMIX_CALL_ROUTE_LOADER).tracePromise(async () => ({ status: 200 }), ctx); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); it('callRouteAction: builds an ACTION span and captures the configured form-data keys', async () => { diff --git a/packages/server-utils/src/integrations/dataloader.ts b/packages/server-utils/src/integrations/dataloader.ts index 5fb50a577325..63983e9b32b3 100644 --- a/packages/server-utils/src/integrations/dataloader.ts +++ b/packages/server-utils/src/integrations/dataloader.ts @@ -179,9 +179,7 @@ function subscribeConstruct(): void { function subscribeLoad(): void { const channel = diagnosticsChannel.tracingChannel(CHANNELS.DATALOADER_LOAD); - bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0]), { - requiresParentSpan: true, - }); + bindTracingChannelToSpan(channel, data => startInactiveSpanFor(data.self, 'load', data.arguments[0])); channel.end.subscribe(message => { const data = message as TracingChannelPayloadWithSpan; @@ -194,10 +192,8 @@ function subscribeLoad(): void { } function subscribeSimpleOperation(channelName: ChannelName, operation: Operation): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => startInactiveSpanFor(data.self, operation, data.arguments[0]), - { requiresParentSpan: true }, + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => + startInactiveSpanFor(data.self, operation, data.arguments[0]), ); } diff --git a/packages/server-utils/src/integrations/hono/honoIntegration.ts b/packages/server-utils/src/integrations/hono/honoIntegration.ts index 575462d6d018..d5ef285cdf62 100644 --- a/packages/server-utils/src/integrations/hono/honoIntegration.ts +++ b/packages/server-utils/src/integrations/hono/honoIntegration.ts @@ -111,13 +111,13 @@ function instrumentInternalRequests(): void { ).toUpperCase(); return startInactiveSpan({ name: `${method} ${extractPathname(input)}`, + onlyIfParent: true, attributes: { [SENTRY_OP]: HTTP_SERVER, [SENTRY_ORIGIN]: INTERNAL_REQUEST_ORIGIN, }, }); }, - { requiresParentSpan: true }, ); } diff --git a/packages/server-utils/src/integrations/mongodb/index.ts b/packages/server-utils/src/integrations/mongodb/index.ts index 593638025c6f..b27a3c46c933 100644 --- a/packages/server-utils/src/integrations/mongodb/index.ts +++ b/packages/server-utils/src/integrations/mongodb/index.ts @@ -64,23 +64,17 @@ function instrumentMongodb(): void { * (both publish to this channel). */ function subscribeV4Command(): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.MONGODB_COMMAND), - data => { - const args = data.arguments ?? []; - const ns = args[0] as MongodbNamespace | undefined; - const cmd = args[1] as Record | undefined; - // Skip handshake/heartbeat commands (matches otel). - if (!ns || !cmd || typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { - return undefined; - } - const operation = Object.keys(cmd)[0]; - return startMongoSpan(getV4SpanAttributes(data.self, ns, cmd, operation, ORIGIN)); - }, - // Matches otel's `shouldSkipInstrumentation`: only trace when there is - // an active parent span, to avoid emitting orphaned mongodb spans. - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.MONGODB_COMMAND), data => { + const args = data.arguments ?? []; + const ns = args[0] as MongodbNamespace | undefined; + const cmd = args[1] as Record | undefined; + // Skip handshake/heartbeat commands (matches otel). + if (!ns || !cmd || typeof cmd !== 'object' || cmd.ismaster || cmd.hello) { + return undefined; + } + const operation = Object.keys(cmd)[0]; + return startMongoSpan(getV4SpanAttributes(data.self, ns, cmd, operation, ORIGIN)); + }); } /** @@ -150,21 +144,17 @@ function subscribeV3Wireprotocol(): void { } function bindV3(channelName: string, extract: (args: unknown[]) => V3CallInfo | undefined): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => { - const args = data.arguments; - if (!args) { - return undefined; - } - const info = extract(args); - if (!info || typeof info.ns !== 'string') { - return undefined; - } - return startMongoSpan(getV3SpanAttributes(info.ns, info.topology, info.command, info.operation, ORIGIN)); - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => { + const args = data.arguments; + if (!args) { + return undefined; + } + const info = extract(args); + if (!info || typeof info.ns !== 'string') { + return undefined; + } + return startMongoSpan(getV3SpanAttributes(info.ns, info.topology, info.command, info.operation, ORIGIN)); + }); } /** diff --git a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts index 99d1778e758d..7c243081295c 100644 --- a/packages/server-utils/src/integrations/mongodb/mongodb-span.ts +++ b/packages/server-utils/src/integrations/mongodb/mongodb-span.ts @@ -235,6 +235,7 @@ export function startMongoSpan(attributes: SpanAttributes): Span { return startInactiveSpan({ name, + onlyIfParent: true, attributes: { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', diff --git a/packages/server-utils/src/integrations/mysql2/index.ts b/packages/server-utils/src/integrations/mysql2/index.ts index 5f337d21a41d..f8aa296266f9 100644 --- a/packages/server-utils/src/integrations/mysql2/index.ts +++ b/packages/server-utils/src/integrations/mysql2/index.ts @@ -74,34 +74,31 @@ function instrumentMysql2Orchestrion(): void { } function subscribeQueryChannel(channelName: ChannelName): void { - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(channelName), - data => { - const statement = getQueryText(data.arguments); - const connectionAttributes = getConnectionAttributes(data.self?.config); - const { queryText, querySummary } = sanitizeSqlQueryWithSummary(statement, 'mysql'); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(channelName), data => { + const statement = getQueryText(data.arguments); + const connectionAttributes = getConnectionAttributes(data.self?.config); + const { queryText, querySummary } = sanitizeSqlQueryWithSummary(statement, 'mysql'); - const client = getClient(); - const name = - client && hasSpanStreamingEnabled(client) - ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL - : (queryText ?? 'mysql2.query'); + const client = getClient(); + const name = + client && hasSpanStreamingEnabled(client) + ? querySummary || (connectionAttributes[DB_NAMESPACE] as string | undefined) || DB_SYSTEM_VALUE_MYSQL + : (queryText ?? 'mysql2.query'); - return startInactiveSpan({ - name, - attributes: { - [SENTRY_KIND]: 'client', - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, - [DB_QUERY_TEXT]: queryText || undefined, - [DB_QUERY_SUMMARY]: querySummary, - ...connectionAttributes, - }, - }); - }, - { requiresParentSpan: true }, - ); + return startInactiveSpan({ + name, + onlyIfParent: true, + attributes: { + [SENTRY_KIND]: 'client', + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_VALUE_MYSQL, + [DB_QUERY_TEXT]: queryText || undefined, + [DB_QUERY_SUMMARY]: querySummary, + ...connectionAttributes, + }, + }); + }); } /** diff --git a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts index 72d065956713..2a5cc75fd56c 100644 --- a/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts +++ b/packages/server-utils/src/integrations/mysql2/mysql2-dc-subscriber.ts @@ -92,56 +92,50 @@ export function subscribeMysql2DiagnosticChannels(tracingChannel: MySQL2TracingC } function setupQueryChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string): void { - bindTracingChannelToSpan( - tracingChannel(channelName), - data => { - // mysql2 does not sanitize its channel payload, so the statement may carry - // raw user values (on the `query` channel they are inlined). Strip every - // literal before it leaves the process; `values` is never attached. - const { queryText, querySummary } = sanitizeSqlQueryWithSummary(data.query, 'mysql'); - const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); + bindTracingChannelToSpan(tracingChannel(channelName), data => { + // mysql2 does not sanitize its channel payload, so the statement may carry + // raw user values (on the `query` channel they are inlined). Strip every + // literal before it leaves the process; `values` is never attached. + const { queryText, querySummary } = sanitizeSqlQueryWithSummary(data.query, 'mysql'); + const operation = queryText?.match(SQL_OPERATION_RE)?.[1]?.toUpperCase(); - const client = getClient(); - const name = - client && hasSpanStreamingEnabled(client) - ? querySummary || data.database || DB_SYSTEM_NAME_VALUE_MYSQL - : queryText || 'mysql2.query'; + const client = getClient(); + const name = + client && hasSpanStreamingEnabled(client) + ? querySummary || data.database || DB_SYSTEM_NAME_VALUE_MYSQL + : queryText || 'mysql2.query'; - return startInactiveSpan({ - name, - attributes: { - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [DB_QUERY_TEXT]: queryText, - [DB_QUERY_SUMMARY]: querySummary, - [DB_OPERATION_NAME]: operation, - [DB_NAMESPACE]: data.database || undefined, - [SERVER_ADDRESS]: data.serverAddress, - [SERVER_PORT]: data.serverPort, - }, - }); - }, - { requiresParentSpan: true }, - ); + return startInactiveSpan({ + name, + onlyIfParent: true, + attributes: { + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_QUERY_TEXT]: queryText, + [DB_QUERY_SUMMARY]: querySummary, + [DB_OPERATION_NAME]: operation, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }); } function setupConnectChannel(tracingChannel: MySQL2TracingChannelFactory, channelName: string, spanName: string): void { - bindTracingChannelToSpan( - tracingChannel(channelName), - data => { - return startInactiveSpan({ - name: spanName, - attributes: { - [SENTRY_ORIGIN]: ORIGIN, - [SENTRY_OP]: DB, - [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, - [DB_NAMESPACE]: data.database || undefined, - [SERVER_ADDRESS]: data.serverAddress, - [SERVER_PORT]: data.serverPort, - }, - }); - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(tracingChannel(channelName), data => { + return startInactiveSpan({ + name: spanName, + onlyIfParent: true, + attributes: { + [SENTRY_ORIGIN]: ORIGIN, + [SENTRY_OP]: DB, + [DB_SYSTEM_NAME]: DB_SYSTEM_NAME_VALUE_MYSQL, + [DB_NAMESPACE]: data.database || undefined, + [SERVER_ADDRESS]: data.serverAddress, + [SERVER_PORT]: data.serverPort, + }, + }); + }); } diff --git a/packages/server-utils/src/integrations/neon.ts b/packages/server-utils/src/integrations/neon.ts index f7e66ff953a2..e4130d14942c 100644 --- a/packages/server-utils/src/integrations/neon.ts +++ b/packages/server-utils/src/integrations/neon.ts @@ -60,15 +60,11 @@ function instrumentNeon(): void { // the pg span shape. bindPgQueryChannel(CHANNELS.NEON_QUERY, { origin: ORIGIN }); - bindTracingChannelToSpan( - diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), - data => { - const span = startInactiveSpan(httpQuerySpanOptions(data.arguments)); - httpQuerySpans.add(span); - return span; - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(diagnosticsChannel.tracingChannel(CHANNELS.NEON_HTTP_QUERY), data => { + const span = startInactiveSpan(httpQuerySpanOptions(data.arguments)); + httpQuerySpans.add(span); + return span; + }); // The connection string is resolved inside the HTTP executor, so the query span is active // when this settles. The executor's own context never sees the string. @@ -109,6 +105,7 @@ function httpQuerySpanOptions(args: unknown[]): Parameters)[QUERY_FROM_INSTRUMENTED_SQL]) { return undefined; } @@ -292,6 +291,7 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { // `sentry.kind: client` matches the mysql/pg channel subscribers. const span = startInactiveSpan({ name, + onlyIfParent: requireParentSpan !== false, attributes: { [SENTRY_OP]: DB, [SENTRY_KIND]: 'client', @@ -324,7 +324,6 @@ function instrumentPostgresJs(options: PostgresJsIntegrationOptions): void { return span; }, { - requiresParentSpan: requireParentSpan !== false, deferSpanEnd({ data }) { // `handle` is async: its promise settles on dispatch (asyncEnd), long // before the query does. The resolve/reject wrappers own the ending. diff --git a/packages/server-utils/src/integrations/postgres.ts b/packages/server-utils/src/integrations/postgres.ts index 6ef1346a5ee5..3231e0e60a06 100644 --- a/packages/server-utils/src/integrations/postgres.ts +++ b/packages/server-utils/src/integrations/postgres.ts @@ -141,6 +141,7 @@ function subscribeQueryLikeChannel( const spanOptions = getSpanOptions(data); return startInactiveSpan({ ...spanOptions, + onlyIfParent: true, attributes: { ...spanOptions.attributes, [SENTRY_KIND]: 'client' }, }); }, @@ -151,9 +152,6 @@ function subscribeQueryLikeChannel( // `query` can return a streamable `Submittable`, so only it defers. deferStreamedResult ? { - // Only instrument under an active span, leaving the context untouched otherwise - // (e.g. connects issued during app startup). - requiresParentSpan: true, // Streamable `Submittable` (e.g. `client.query(new Query())`) // returns an emitter that orchestrion stores on `ctx.result` while // firing no async events; the query isn't done until the emitter @@ -180,7 +178,7 @@ function subscribeQueryLikeChannel( return true; }, } - : { requiresParentSpan: true }, + : undefined, ); } diff --git a/packages/server-utils/src/integrations/prisma/orchestrion.ts b/packages/server-utils/src/integrations/prisma/orchestrion.ts index de209f694d71..a9c3bea5ac6d 100644 --- a/packages/server-utils/src/integrations/prisma/orchestrion.ts +++ b/packages/server-utils/src/integrations/prisma/orchestrion.ts @@ -52,11 +52,7 @@ export function instrumentPrismaV8(options: PrismaV8InstrumentationOptions): voi bindTracingChannelToSpan( diagnosticsChannel.tracingChannel(channelName), data => startOperationSpan(method, data), - { - // A stray query outside a request must not become a segment of its own. - requiresParentSpan: true, - ...(isLazy ? { deferSpanEnd: deferSpanEndUntilConsumed } : {}), - }, + isLazy ? { deferSpanEnd: deferSpanEndUntilConsumed } : undefined, ); } } @@ -72,6 +68,7 @@ function startOperationSpan(method: string, data: PrismaTerminalChannelContext): const span = startInactiveSpan({ name: OPERATION_SPAN_NAME, + onlyIfParent: true, parentSpan, attributes: { [SENTRY_ORIGIN]: PRISMA_ORIGIN, diff --git a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts index d918d080f749..9426e26c6add 100644 --- a/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts +++ b/packages/server-utils/src/integrations/redis/ioredis-channel-subscriber.ts @@ -93,6 +93,7 @@ export function startIORedisCommandSpan( return startInactiveSpan({ name: cacheProperties?.name ?? streamedName ?? statement, + onlyIfParent: true, attributes: { ...attributes, ...cacheProperties?.attributes }, }); } @@ -112,8 +113,6 @@ export function instrumentIoredis(options: RedisCacheOptions): void { ); bindTracingChannelToSpan(commandChannel, data => startIORedisCommandSpan(data, options), { - // ioredis' `requireParentSpan` default: only create a span under an active span. - requiresParentSpan: true, beforeSpanEnd(span, data) { if (!('error' in data)) { applyCacheResponseAttributes(span, data.result); @@ -121,19 +120,16 @@ export function instrumentIoredis(options: RedisCacheOptions): void { }, }); - bindTracingChannelToSpan( - connectChannel, - data => { - const { host, port } = getConnectionOptions(data.self); - return startInactiveSpan({ - name: 'connect', - attributes: { - [SENTRY_KIND]: 'client', - ...connectionAttributes(host, port), - [SENTRY_OP]: DB, - }, - }); - }, - { requiresParentSpan: true }, - ); + bindTracingChannelToSpan(connectChannel, data => { + const { host, port } = getConnectionOptions(data.self); + return startInactiveSpan({ + name: 'connect', + onlyIfParent: true, + attributes: { + [SENTRY_KIND]: 'client', + ...connectionAttributes(host, port), + [SENTRY_OP]: DB, + }, + }); + }); } diff --git a/packages/server-utils/src/tracing-channel.ts b/packages/server-utils/src/tracing-channel.ts index 075057f47c31..859b80738af7 100644 --- a/packages/server-utils/src/tracing-channel.ts +++ b/packages/server-utils/src/tracing-channel.ts @@ -8,7 +8,6 @@ import { SPAN_STATUS_ERROR, getAsyncContextStrategy, getMainCarrier, - getActiveSpan, spanIsIgnored, getRootSpan, withActiveSpan, @@ -72,12 +71,6 @@ export interface TracingChannelLifeCycleOptions { /** Ends the span: `end()` on success, `end(error)` on failure (which marks `data` as errored). Idempotent. */ end: (error?: unknown) => void; }) => boolean; - - /** - * Apply span/scope binding only if there is a parent span, similar to returning `undefined` in the `getSpan` callback. - * If you need to perform some conditional checking on the parent span before deciding, do it in the `getSpan` callback. - */ - requiresParentSpan?: boolean; } /** Returned by {@link bindTracingChannelToSpan}: the bound channel plus a teardown handle. */ @@ -120,7 +113,7 @@ export function bindTracingChannelToSpan( getSpan: (data: TracingChannelPayloadWithSpan) => Span | undefined, opts?: TracingChannelLifeCycleOptions, ): TracingChannelBindingHandle { - const handle = bindSpanToChannelStore(channel, getSpan, opts); + const handle = bindSpanToChannelStore(channel, getSpan); const beforeSpanEnd = opts?.beforeSpanEnd; const deferSpanEnd = opts?.deferSpanEnd; @@ -224,7 +217,6 @@ export function bindTracingChannelToSpan( export function bindSpanToChannelStore( channel: TracingChannel, getSpan: (data: TracingChannelPayloadWithSpan) => Span | undefined, - opts?: Pick, ): TracingChannelBindingHandle { // Grabs the tracing channel binding defined by the AsyncContext strategy implementation const binding = getAsyncContextStrategy(getMainCarrier()).getTracingChannelBinding?.(); @@ -253,8 +245,7 @@ export function bindSpanToChannelStore( // callback-style channels (see `_sentryCallerStore`). data._sentryCallerStore = asyncLocalStorage.getStore(); - const shouldGetSpan = !opts?.requiresParentSpan || getActiveSpan(); - const span = shouldGetSpan ? getSpan(data) : undefined; + const span = getSpan(data); if (!span) { // Leave the active context untouched so nested operations keep parenting to the enclosing span. return data._sentryCallerStore as TData; diff --git a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts index f414f8ee1bf0..dec09eb63d28 100644 --- a/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts +++ b/packages/server-utils/test/integrations/mysql2/mysql2-dc-subscriber.test.ts @@ -102,7 +102,7 @@ function installTestAsyncContextStrategy(): void { /** * Drives a channel's `tracePromise` inside an enclosing span and captures the span bound by the - * subscriber. The subscriber only creates a span when there is an enclosing active span, so the + * subscriber. The subscriber only creates a recording span when there is an enclosing active span, so the * helper always establishes one and returns its id for parenting assertions. */ async function traceOperation( @@ -168,7 +168,7 @@ describe('subscribeMysql2DiagnosticChannels', () => { vi.clearAllMocks(); }); - it('does not create a span when there is no enclosing active span', async () => { + it('does not create a recording span when there is no enclosing active span', async () => { const channel = tracingChannel(MYSQL2_DC_CHANNEL_QUERY); let span: Span | undefined; @@ -179,7 +179,8 @@ describe('subscribeMysql2DiagnosticChannels', () => { { query: 'SELECT 1' }, ); - expect(span).toBeUndefined(); + expect(span).toBeDefined(); + expect(span?.isRecording()).toBe(false); }); describe('query channel', () => { diff --git a/packages/server-utils/test/integrations/neon.test.ts b/packages/server-utils/test/integrations/neon.test.ts index 661a65cc7389..6c5ac05fe9d0 100644 --- a/packages/server-utils/test/integrations/neon.test.ts +++ b/packages/server-utils/test/integrations/neon.test.ts @@ -154,13 +154,13 @@ describe('neonIntegration', () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it('does not start a span without a parent span', async () => { + it('does not create a recording span without a parent span', async () => { getActiveSpanSpy.mockReturnValue(undefined); const ctx: ChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.NEON_QUERY).tracePromise(async () => ({ rows: [] }), ctx); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); }); diff --git a/packages/server-utils/test/integrations/postgres/postgres.test.ts b/packages/server-utils/test/integrations/postgres/postgres.test.ts index db64ef9e137c..9af1c1a5c851 100644 --- a/packages/server-utils/test/integrations/postgres/postgres.test.ts +++ b/packages/server-utils/test/integrations/postgres/postgres.test.ts @@ -63,13 +63,6 @@ function installTestAsyncContextStrategy(): void { }); } -// The subscriber builds spans via `startInactiveSpan` and gates on -// `getActiveSpan`. We spy both: `getActiveSpan` to satisfy the -// requireParentSpan gate, and `startInactiveSpan` to capture the span -// options the subscriber builds (name + raw attributes) and to track the -// span's lifecycle. The final `db` op / SQL description come from the -// SDK's `inferDbSpanData` processor, which isn't wired up here. That's -// covered by the integration test. function makeSpan(): Span { return { end: vi.fn(), setStatus: vi.fn(), setAttributes: vi.fn() } as unknown as Span; } @@ -98,7 +91,6 @@ describe('postgresIntegration', () => { beforeEach(() => { span = makeSpan(); startInactiveSpanSpy = vi.spyOn(SentryCore, 'startInactiveSpan').mockReturnValue(span); - // A truthy active span by default, so the requireParentSpan gate passes. getActiveSpanSpy = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({} as Span); }); @@ -251,12 +243,12 @@ describe('postgresIntegration', () => { ); }); - it('requireParentSpan: does not create a span when there is no active span', async () => { + it('does not create a recording span without an active parent span', async () => { getActiveSpanSpy.mockReturnValue(undefined); const ctx: OrchestrionChannelContext = { arguments: ['SELECT 1'], self: { connectionParameters: CONNECTION } }; await tracingChannel(CHANNELS.PG_QUERY).tracePromise(async () => ({ rows: [] }), ctx); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); }); diff --git a/packages/server-utils/test/integrations/prisma-orchestrion.test.ts b/packages/server-utils/test/integrations/prisma-orchestrion.test.ts index 78a1e3bf1988..cbc8cc142c6c 100644 --- a/packages/server-utils/test/integrations/prisma-orchestrion.test.ts +++ b/packages/server-utils/test/integrations/prisma-orchestrion.test.ts @@ -206,13 +206,13 @@ describe('instrumentPrismaV8', () => { expect(span.end).toHaveBeenCalledTimes(1); }); - it('does not open a span without a parent', async () => { + it('does not create a recording span without a parent', async () => { await tracingChannel(CHANNELS.PRISMA_CREATE).tracePromise(async () => ({ id: 1 }), { arguments: [], self: { modelName: 'User' }, }); - expect(startInactiveSpanSpy).not.toHaveBeenCalled(); + expect(startInactiveSpanSpy).toHaveBeenCalledWith(expect.objectContaining({ onlyIfParent: true })); }); it('async terminal: marks the span as errored when the promise rejects', async () => {