diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts index 86b3bf6d9d22..86295d06b128 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage/test.ts @@ -1,11 +1,24 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { cleanupChildProcesses(); }); -test('should send a manually started root span', done => { +test('sends a manually started root span with source set to custom', done => { createRunner(__dirname, 'scenario.ts') - .expect({ transaction: { transaction: 'test_span' } }) + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) .start(done); }); diff --git a/packages/nestjs/src/decorators/sentry-traced.ts b/packages/nestjs/src/decorators/sentry-traced.ts index 2f90e4dab5d9..6273970af000 100644 --- a/packages/nestjs/src/decorators/sentry-traced.ts +++ b/packages/nestjs/src/decorators/sentry-traced.ts @@ -1,4 +1,4 @@ -import { startSpan } from '@sentry/node'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node'; /** * A decorator usable to wrap arbitrary functions with spans. @@ -14,6 +14,10 @@ export function SentryTraced(op: string = 'function') { { op: op, name: propertyKey, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }, () => { return originalMethod.apply(this, args); diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index bdff0de922b2..41437766b455 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -173,6 +173,8 @@ export function init(options: NodeOptions): NodeClient | undefined { ) { const route = spanAttributes['next.route'].replace(/\/route$/, ''); rootSpan.updateName(route); + // set the source of the root span to what it was before cleaning up the name + rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanAttributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]); rootSpan.setAttribute(ATTR_HTTP_ROUTE, route); } } diff --git a/packages/node/src/integrations/fs.ts b/packages/node/src/integrations/fs.ts index 2288096dad43..7fb9fe78ec82 100644 --- a/packages/node/src/integrations/fs.ts +++ b/packages/node/src/integrations/fs.ts @@ -1,5 +1,10 @@ import { FsInstrumentation } from '@opentelemetry/instrumentation-fs'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, defineIntegration } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + defineIntegration, +} from '@sentry/core'; import { generateInstrumentOnce } from '../otel/instrument'; const INTEGRATION_NAME = 'FileSystem'; @@ -45,6 +50,7 @@ export const fsIntegration = defineIntegration( span.setAttributes({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'file', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.file.fs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }); if (options.recordErrorMessagesAsSpanAttributes) { diff --git a/packages/node/src/integrations/tracing/connect.ts b/packages/node/src/integrations/tracing/connect.ts index b2938d5ee6f2..8dc84d8f60ba 100644 --- a/packages/node/src/integrations/tracing/connect.ts +++ b/packages/node/src/integrations/tracing/connect.ts @@ -2,6 +2,7 @@ import { ConnectInstrumentation } from '@opentelemetry/instrumentation-connect'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, defineIntegration, getClient, @@ -108,5 +109,7 @@ function addConnectSpanAttributes(span: Span): void { const name = attributes['connect.name']; if (typeof name === 'string') { span.updateName(name); + // set the source of the span to what it was before (likely undefined) + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]); } } diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index 0b4b6aa35c3e..996f1c7663de 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -1,6 +1,12 @@ import type * as http from 'node:http'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, defineIntegration, getDefaultIsolationScope, spanToJSON } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + defineIntegration, + getDefaultIsolationScope, + spanToJSON, +} from '@sentry/core'; import { captureException, getClient, getIsolationScope } from '@sentry/core'; import type { IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -31,6 +37,8 @@ export const instrumentExpress = generateInstrumentOnce( const name = attributes['express.name']; if (typeof name === 'string') { span.updateName(name); + // set the source of the span to what it was before (likely undefined) + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]); } }, spanNameHook(info, defaultName) { diff --git a/packages/node/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts index 5a75e204b5f5..74bdb6c16f76 100644 --- a/packages/node/src/integrations/tracing/fastify.ts +++ b/packages/node/src/integrations/tracing/fastify.ts @@ -2,6 +2,7 @@ import { FastifyInstrumentation } from '@opentelemetry/instrumentation-fastify'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, defineIntegration, getClient, @@ -156,5 +157,7 @@ function addFastifySpanAttributes(span: Span): void { if (typeof name === 'string') { // Also remove `fastify -> ` prefix span.updateName(name.replace(/^fastify -> /, '')); + // set the source of the span to what it was before (likely undefined) + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]); } } diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index a244c6cb56d7..95dce3e42c02 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -3,6 +3,7 @@ import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, defineIntegration, getDefaultIsolationScope, @@ -120,5 +121,7 @@ function addKoaSpanAttributes(span: Span): void { // Somehow, name is sometimes `''` for middleware spans // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 span.updateName(name || '< unknown >'); + // set the source of the span to what it was before (likely undefined) + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]); } } diff --git a/packages/node/src/integrations/tracing/nest/helpers.ts b/packages/node/src/integrations/tracing/nest/helpers.ts index cc83dda3855d..8f326a5aa333 100644 --- a/packages/node/src/integrations/tracing/nest/helpers.ts +++ b/packages/node/src/integrations/tracing/nest/helpers.ts @@ -1,5 +1,10 @@ -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, withActiveSpan } from '@sentry/core'; -import type { Span } from '@sentry/types'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + withActiveSpan, +} from '@sentry/core'; +import type { Span, StartSpanOptions } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; import type { CatchTarget, InjectableTarget, NextFunction, Observable, Subscription } from './types'; @@ -23,8 +28,10 @@ export function isPatched(target: InjectableTarget | CatchTarget): boolean { /** * Returns span options for nest middleware spans. */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget, name: string | undefined = undefined) { +export function getMiddlewareSpanOptions( + target: InjectableTarget | CatchTarget, + name: string | undefined = undefined, +): StartSpanOptions { const span_name = name ?? target.name; // fallback to class name if no name is provided return { @@ -32,6 +39,7 @@ export function getMiddlewareSpanOptions(target: InjectableTarget | CatchTarget, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }, }; } diff --git a/packages/opentelemetry/src/spanProcessor.ts b/packages/opentelemetry/src/spanProcessor.ts index 71dab1359b94..b17391b5afa0 100644 --- a/packages/opentelemetry/src/spanProcessor.ts +++ b/packages/opentelemetry/src/spanProcessor.ts @@ -8,6 +8,7 @@ import { getDefaultIsolationScope, logSpanEnd, logSpanStart, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, setCapturedScopesOnSpan, } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_PARENT_IS_REMOTE } from './semanticAttributes'; @@ -16,6 +17,21 @@ import { getScopesFromContext } from './utils/contextData'; import { setIsSetup } from './utils/setupCheck'; function onSpanStart(span: Span, parentContext: Context): void { + // Ugly hack to set the source to custom when updating the span name + // We want to add this functionality so that users don't have to care + // about setting the source attribute themselves + // This is in-line with other SDKs as well as the `SentrySpan` class. + try { + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalSpanUpdateName = span.updateName; + span.updateName = (name: string) => { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + return originalSpanUpdateName.call(span, name); + }; + } catch { + // Safe-guarding this in case bundlers add freezing logic that breaks the assignment + } + // This is a reliable way to get the parent span - because this is exactly how the parent is identified in the OTEL SDK const parentSpan = trace.getSpan(parentContext); diff --git a/packages/opentelemetry/src/utils/parseSpanDescription.ts b/packages/opentelemetry/src/utils/parseSpanDescription.ts index 8a07f2aadc2a..9cfadf40ccfe 100644 --- a/packages/opentelemetry/src/utils/parseSpanDescription.ts +++ b/packages/opentelemetry/src/utils/parseSpanDescription.ts @@ -16,7 +16,11 @@ import { import type { SpanAttributes, TransactionSource } from '@sentry/types'; import { getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from '@sentry/utils'; -import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '../semanticAttributes'; import type { AbstractSpan } from '../types'; import { getSpanKind } from './getSpanKind'; @@ -33,6 +37,13 @@ interface SpanDescription { * Infer the op & description for a set of name, attributes and kind of a span. */ export function inferSpanData(name: string, attributes: SpanAttributes, kind: SpanKind): SpanDescription { + // If users (or in very rare occasions our SDK instrumentation) set the source to "custom" + // or users manually started a span, we just bail because we don't want to override their data + const previouslySetSource = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + if (previouslySetSource && previouslySetSource === 'custom') { + return { op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], description: name, source: previouslySetSource }; + } + // if http.method exists, this is an http request span // eslint-disable-next-line deprecation/deprecation const httpMethod = attributes[ATTR_HTTP_REQUEST_METHOD] || attributes[SEMATTRS_HTTP_METHOD]; diff --git a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts index c44645c62888..d91b5b08f27f 100644 --- a/packages/opentelemetry/test/utils/parseSpanDescription.test.ts +++ b/packages/opentelemetry/test/utils/parseSpanDescription.test.ts @@ -16,6 +16,7 @@ import { } from '@opentelemetry/semantic-conventions'; import { descriptionForHttpMethod, getSanitizedUrl, parseSpanDescription } from '../../src/utils/parseSpanDescription'; +import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; describe('parseSpanDescription', () => { it.each([ @@ -137,6 +138,15 @@ describe('parseSpanDescription', () => { const actual = parseSpanDescription({ attributes, kind, name } as unknown as Span); expect(actual).toEqual(expected); }); + + it.each(['http.client', undefined])('returns the original values if source is custom (op: %s)', originalOp => { + const actual = parseSpanDescription({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: originalOp }, + kind: SpanKind.CLIENT, + name: 'test name', + } as unknown as Span); + expect(actual).toEqual({ description: 'test name', op: originalOp, source: 'custom' }); + }); }); describe('descriptionForHttpMethod', () => { @@ -230,6 +240,27 @@ describe('descriptionForHttpMethod', () => { source: 'custom', }, ], + [ + 'works with prefetch requests', + 'GET', + { + [SEMATTRS_HTTP_METHOD]: 'GET', + [SEMATTRS_HTTP_URL]: 'https://www.example.com/my-path/123', + [SEMATTRS_HTTP_TARGET]: '/my-path/123', + [ATTR_HTTP_ROUTE]: '/my-path/:id', + 'sentry.http.prefetch': true, + }, + 'test name', + SpanKind.CLIENT, + { + op: 'http.client.prefetch', + description: 'GET /my-path/:id', + data: { + url: 'https://www.example.com/my-path/123', + }, + source: 'route', + }, + ], ])('%s', (_, httpMethod, attributes, name, kind, expected) => { const actual = descriptionForHttpMethod({ attributes, kind, name }, httpMethod); expect(actual).toEqual(expected); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index e83c14dfbbc4..80bf1b04508d 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -135,6 +135,7 @@ function makeWrappedDocumentRequestFunction(autoInstrumentRemix?: boolean, remix url: request.url, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.remix.document_request', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }, }, () => { @@ -175,6 +176,7 @@ function makeWrappedDataFunction( name: id, attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', name, }, }, diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index a2ee74fd7cfa..d521e36ad3bc 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -234,6 +234,13 @@ export interface Span { /** * Update the name of the span. + * + * Calling this method will also override or set the `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` attribute to `custom`. + * This indicates to the Sentry backend that the span name was set manually to skip potential post processing of the name. + * This is very likely what you want to achieve when calling this method! + * + * If you still need to change the source attribute (for instance for custom router instrumentation), + * you can do so by calling `span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, sourceValue)`. */ updateName(name: string): this;