diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js new file mode 100644 index 000000000000..92152554ea57 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + // disable pageload transaction + integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js new file mode 100644 index 000000000000..f62499b1e9c5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/subject.js @@ -0,0 +1 @@ +fetch('http://example.com/0').then(fetch('http://example.com/1').then(fetch('http://example.com/2'))); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts new file mode 100644 index 000000000000..4dc5a0ac4e0a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-with-no-active-span/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'there should be no span created for fetch requests with no active span', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + let requestCount = 0; + page.on('request', request => { + expect(envelopeUrlRegex.test(request.url())).toBe(false); + requestCount++; + }); + + await page.goto(url); + + // Here are the requests that should exist: + // 1. HTML page + // 2. Init JS bundle + // 3. Subject JS bundle + // 4 [OPTIONAl] CDN JS bundle + // and then 3 fetch requests + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + expect(requestCount).toBe(7); + } else { + expect(requestCount).toBe(6); + } + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js new file mode 100644 index 000000000000..92152554ea57 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/init.js @@ -0,0 +1,10 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + // disable pageload transaction + integrations: [Sentry.BrowserTracing({ tracingOrigins: ['http://example.com'], startTransactionOnPageLoad: false })], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js new file mode 100644 index 000000000000..5790c230aa66 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/subject.js @@ -0,0 +1,11 @@ +const xhr_1 = new XMLHttpRequest(); +xhr_1.open('GET', 'http://example.com/0'); +xhr_1.send(); + +const xhr_2 = new XMLHttpRequest(); +xhr_2.open('GET', 'http://example.com/1'); +xhr_2.send(); + +const xhr_3 = new XMLHttpRequest(); +xhr_3.open('GET', 'http://example.com/2'); +xhr_3.send(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts new file mode 100644 index 000000000000..19c1f5891a39 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-with-no-active-span/test.ts @@ -0,0 +1,35 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeUrlRegex, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'there should be no span created for xhr requests with no active span', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + let requestCount = 0; + page.on('request', request => { + expect(envelopeUrlRegex.test(request.url())).toBe(false); + requestCount++; + }); + + await page.goto(url); + + // Here are the requests that should exist: + // 1. HTML page + // 2. Init JS bundle + // 3. Subject JS bundle + // 4 [OPTIONAl] CDN JS bundle + // and then 3 fetch requests + if (process.env.PW_BUNDLE && process.env.PW_BUNDLE.startsWith('bundle_')) { + expect(requestCount).toBe(7); + } else { + expect(requestCount).toBe(6); + } + }, +); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index d00f125a90c1..6027695746e9 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -1,7 +1,7 @@ import type { Page, Request } from '@playwright/test'; import type { EnvelopeItemType, Event, EventEnvelopeHeaders } from '@sentry/types'; -const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; +export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; export const envelopeParser = (request: Request | null): unknown[] => { // https://develop.sentry.dev/sdk/envelopes/ diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index bdeba55ed9e4..308e68a9738c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -80,7 +80,9 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); @@ -128,7 +130,9 @@ export function startSpanManual( // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + const activeSpan = shouldSkipSpan ? undefined : createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); @@ -174,6 +178,12 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { context.scope.getSpan() : getActiveSpan(); + const shouldSkipSpan = context.onlyIfParent && !parentSpan; + + if (shouldSkipSpan) { + return undefined; + } + if (parentSpan) { // eslint-disable-next-line deprecation/deprecation return parentSpan.startChild(ctx); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 6bca44c6b088..fca086f10c94 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -335,6 +335,28 @@ describe('startSpan', () => { }); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('startSpanManual', () => { @@ -415,6 +437,28 @@ describe('startSpanManual', () => { }); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('startInactiveSpan', () => { @@ -479,6 +523,24 @@ describe('startInactiveSpan', () => { span?.end(); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + expect(span).toBeUndefined(); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + return span; + }); + + expect(span).toBeDefined(); + }); + }); }); describe('continueTrace', () => { diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index e2f517900114..1047d77f39fd 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -1,5 +1,7 @@ import type { Span, Tracer } from '@opentelemetry/api'; +import { context } from '@opentelemetry/api'; import { SpanStatusCode, trace } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import { SDK_VERSION, handleCallbackErrors } from '@sentry/core'; import type { Client } from '@sentry/types'; @@ -22,7 +24,11 @@ export function startSpan(spanContext: OpenTelemetrySpanContext, callback: (s const { name } = spanContext; - return tracer.startActiveSpan(name, spanContext, span => { + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + return tracer.startActiveSpan(name, spanContext, ctx, span => { _applySentryAttributesToSpan(span, spanContext); return handleCallbackErrors( @@ -49,7 +55,11 @@ export function startSpanManual(spanContext: OpenTelemetrySpanContext, callba const { name } = spanContext; - return tracer.startActiveSpan(name, spanContext, span => { + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + return tracer.startActiveSpan(name, spanContext, ctx, span => { _applySentryAttributesToSpan(span, spanContext); return handleCallbackErrors( @@ -81,7 +91,11 @@ export function startInactiveSpan(spanContext: OpenTelemetrySpanContext): Span { const { name } = spanContext; - const span = tracer.startSpan(name, spanContext); + const activeCtx = context.active(); + const shouldSkipSpan = spanContext.onlyIfParent && !trace.getSpan(activeCtx); + const ctx = shouldSkipSpan ? suppressTracing(activeCtx) : activeCtx; + + const span = tracer.startSpan(name, spanContext, ctx); _applySentryAttributesToSpan(span, spanContext); diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index 168a9f4893a6..d01d80b64e82 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -14,6 +14,7 @@ export interface OpenTelemetrySpanContext { origin?: SpanOrigin; source?: TransactionSource; scope?: Scope; + onlyIfParent?: boolean; // Base SpanOptions we support attributes?: Attributes; diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index af8625a6534e..34e27c3c62f3 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -2,6 +2,7 @@ import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import { TraceFlags, context, trace } from '@opentelemetry/api'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import { Span as SpanClass } from '@opentelemetry/sdk-trace-base'; import type { PropagationContext } from '@sentry/types'; import { getClient } from '../src/custom/hub'; @@ -260,6 +261,28 @@ describe('trace', () => { }, ); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpan({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('startInactiveSpan', () => { @@ -349,6 +372,24 @@ describe('trace', () => { }); expect(getSpanKind(span)).toEqual(SpanKind.CLIENT); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startInactiveSpan({ name: 'test span', onlyIfParent: true }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('startSpanManual', () => { @@ -419,6 +460,28 @@ describe('trace', () => { ); }); }); + + describe('onlyIfParent', () => { + it('does not create a span if there is no parent', () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + expect(span).not.toBeInstanceOf(SpanClass); + }); + + it('creates a span if there is a parent', () => { + const span = startSpan({ name: 'parent span' }, () => { + const span = startSpanManual({ name: 'test span', onlyIfParent: true }, span => { + return span; + }); + + return span; + }); + + expect(span).toBeInstanceOf(SpanClass); + }); + }); }); describe('trace (tracing disabled)', () => { diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index d4f942e33d64..54ed3fa541c8 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -57,6 +57,7 @@ class Profiler extends React.Component { this._mountSpan = startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': name }, @@ -83,6 +84,7 @@ class Profiler extends React.Component { this._updateSpan = withActiveSpan(this._mountSpan, () => { return startInactiveSpan({ name: `<${this.props.name}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: now, @@ -115,6 +117,7 @@ class Profiler extends React.Component { const startTimestamp = spanToJSON(this._mountSpan).timestamp; withActiveSpan(this._mountSpan, () => { const renderSpan = startInactiveSpan({ + onlyIfParent: true, name: `<${name}>`, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', @@ -187,6 +190,7 @@ function useProfiler( return startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': name }, @@ -205,6 +209,7 @@ function useProfiler( const renderSpan = startInactiveSpan({ name: `<${name}>`, + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp, diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 2cdbae0e9320..5d399f342535 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -74,6 +74,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'unknown' }, @@ -92,6 +93,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(2); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: undefined, @@ -125,6 +127,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -136,6 +139,7 @@ describe('withProfiler', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ attributes: { 'ui.react.changed_props': ['num'], 'ui.component_name': 'unknown' }, name: `<${UNKNOWN_COMPONENT}>`, + onlyIfParent: true, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', startTimestamp: expect.any(Number), @@ -175,6 +179,7 @@ describe('useProfiler()', () => { expect(mockStartInactiveSpan).toHaveBeenCalledTimes(1); expect(mockStartInactiveSpan).toHaveBeenLastCalledWith({ name: '', + onlyIfParent: true, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'Example' }, @@ -199,6 +204,7 @@ describe('useProfiler()', () => { expect(mockStartInactiveSpan).toHaveBeenLastCalledWith( expect.objectContaining({ name: '', + onlyIfParent: true, op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', attributes: { 'ui.component_name': 'Example' }, diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 84e6ec93ff25..084fdbf0238a 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -71,6 +71,7 @@ function wrapMakeRequest( req.on('afterBuild', () => { span = startInactiveSpan({ name: describe(this, operation, params), + onlyIfParent: true, op: 'http.client', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 1423e2b3ad5a..a32ac9dae9ac 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -122,6 +122,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } const span = startInactiveSpan({ name: `${callType} ${methodName}`, + onlyIfParent: true, op: `grpc.${serviceIdentifier}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index ee300e78a010..5e7558f4299d 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -64,6 +64,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { const span = SETUP_CLIENTS.has(getClient() as Client) ? startInactiveSpan({ name: `${httpMethod} ${reqOpts.uri}`, + onlyIfParent: true, op: `http.client.${identifyService(this.apiEndpoint)}`, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index bbfc3240c3ac..3170f9056ec0 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -61,6 +61,7 @@ describe('awsServicesIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.s3.getObject foo', + onlyIfParent: true, }); expect(mockSpanEnd).toHaveBeenCalledTimes(1); @@ -84,6 +85,7 @@ describe('awsServicesIntegration', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client', name: 'aws.s3.getObject foo', + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, @@ -114,6 +116,7 @@ describe('awsServicesIntegration', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, name: 'aws.lambda.invoke foo', + onlyIfParent: true, }); expect(mockSpanEnd).toHaveBeenCalledTimes(1); }); diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index c2cd1b3167db..0ce19ee7db8b 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -149,6 +149,7 @@ describe('GoogleCloudGrpc tracing', () => { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.grpc.serverless', }, name: 'unary call publish', + onlyIfParent: true, }); }); }); diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index 6d64bd68624f..3389130565f9 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -75,6 +75,7 @@ describe('GoogleCloudHttp tracing', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: 'POST /jobs', + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, @@ -82,6 +83,7 @@ describe('GoogleCloudHttp tracing', () => { expect(mockStartInactiveSpan).toBeCalledWith({ op: 'http.client.bigquery', name: expect.stringMatching(/^GET \/queries\/.+/), + onlyIfParent: true, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.serverless', }, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 89d5034a5425..d072188bb2af 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,7 +1,6 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -280,21 +279,19 @@ export function xhrCallback( const scope = getCurrentScope(); const isolationScope = getIsolationScope(); - // only create a child span if there is an active span. This is because - // `startInactiveSpan` can still create a transaction under the hood - const span = - shouldCreateSpanResult && getActiveSpan() - ? startInactiveSpan({ - attributes: { - type: 'xhr', - 'http.method': sentryXhrData.method, - url: sentryXhrData.url, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', - }, - name: `${sentryXhrData.method} ${sentryXhrData.url}`, - op: 'http.client', - }) - : undefined; + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${sentryXhrData.method} ${sentryXhrData.url}`, + onlyIfParent: true, + attributes: { + type: 'xhr', + 'http.method': sentryXhrData.method, + url: sentryXhrData.url, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', + }, + op: 'http.client', + }) + : undefined; if (span) { xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 2177d1939987..e12ca3cf1b97 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,6 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -82,21 +81,19 @@ export function instrumentFetchRequest( const { method, url } = handlerData.fetchData; - // only create a child span if there is an active span. This is because - // `startInactiveSpan` can still create a transaction under the hood - const span = - shouldCreateSpanResult && getActiveSpan() - ? startInactiveSpan({ - attributes: { - url, - type: 'fetch', - 'http.method': method, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, - }, - name: `${method} ${url}`, - op: 'http.client', - }) - : undefined; + const span = shouldCreateSpanResult + ? startInactiveSpan({ + name: `${method} ${url}`, + onlyIfParent: true, + attributes: { + url, + type: 'fetch', + 'http.method': method, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, + }, + op: 'http.client', + }) + : undefined; if (span) { handlerData.fetchData.__span = span.spanContext().spanId; diff --git a/packages/tracing-internal/src/node/integrations/prisma.ts b/packages/tracing-internal/src/node/integrations/prisma.ts index 4399778f1e80..2e2aa7dab17a 100644 --- a/packages/tracing-internal/src/node/integrations/prisma.ts +++ b/packages/tracing-internal/src/node/integrations/prisma.ts @@ -103,6 +103,7 @@ export class Prisma implements Integration { return startSpan( { name: model ? `${model} ${action}` : action, + onlyIfParent: true, op: 'db.prisma', attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.db.prisma', diff --git a/packages/tracing/test/integrations/node/prisma.test.ts b/packages/tracing/test/integrations/node/prisma.test.ts index c86f8aecabe2..2541eebf8f91 100644 --- a/packages/tracing/test/integrations/node/prisma.test.ts +++ b/packages/tracing/test/integrations/node/prisma.test.ts @@ -58,6 +58,7 @@ describe('setupOnce', function () { 'sentry.origin': 'auto.db.prisma', }, name: 'user create', + onlyIfParent: true, op: 'db.prisma', data: { 'db.system': 'postgresql', 'db.prisma.version': '3.1.2', 'db.operation': 'create' }, }, diff --git a/packages/types/src/startSpanOptions.ts b/packages/types/src/startSpanOptions.ts index bde20c2c87bf..57ff96b3169f 100644 --- a/packages/types/src/startSpanOptions.ts +++ b/packages/types/src/startSpanOptions.ts @@ -14,6 +14,9 @@ export interface StartSpanOptions extends TransactionContext { /** The name of the span. */ name: string; + /** If set to true, only start a span if a parent span exists. */ + onlyIfParent?: boolean; + /** An op for the span. This is a categorization for spans. */ op?: string;