Skip to content

fix(opentelemetry): Prevent span name update overwrites and ensure sentry.source is set correctly #14280

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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);
});
6 changes: 5 additions & 1 deletion packages/nestjs/src/decorators/sentry-traced.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
8 changes: 7 additions & 1 deletion packages/node/src/integrations/fs.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/integrations/tracing/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]);
}
}
10 changes: 9 additions & 1 deletion packages/node/src/integrations/tracing/express.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions packages/node/src/integrations/tracing/fastify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]);
}
}
3 changes: 3 additions & 0 deletions packages/node/src/integrations/tracing/koa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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]);
}
}
16 changes: 12 additions & 4 deletions packages/node/src/integrations/tracing/nest/helpers.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -23,15 +28,18 @@ 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 {
name: span_name,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'middleware.nestjs',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.middleware.nestjs',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
},
};
}
Expand Down
16 changes: 16 additions & 0 deletions packages/opentelemetry/src/spanProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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);

Expand Down
13 changes: 12 additions & 1 deletion packages/opentelemetry/src/utils/parseSpanDescription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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];
Expand Down
31 changes: 31 additions & 0 deletions packages/opentelemetry/test/utils/parseSpanDescription.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/remix/src/utils/instrumentServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
},
() => {
Expand Down Expand Up @@ -175,6 +176,7 @@ function makeWrappedDataFunction(
name: id,
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.ui.remix',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component',
name,
},
},
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/span.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading