Skip to content
Open
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
Expand Up @@ -21,11 +21,11 @@
/* eslint-disable */

import { context, Span, SpanOptions } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import type { ServerResponse } from 'http';
import { AttributeNames, ConnectNames, ConnectTypes } from './enums/AttributeNames';
import { HandleFunction, NextFunction, Server, PatchedRequest, Use, UseArgs, UseArgs2 } from './internal-types';
import { SDK_VERSION } from '@sentry/core';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';
import {
InstrumentationBase,
InstrumentationConfig,
Expand Down Expand Up @@ -119,9 +119,8 @@ export class ConnectInstrumentation extends InstrumentationBase {

replaceCurrentStackRoute(req, routeName);

const rpcMetadata = getRPCMetadata(context.active());
if (routeName && rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = generateRoute(req);
if (routeName) {
setHttpServerSpanRouteAttribute(generateRoute(req));
}

let spanName = '';
Expand Down
8 changes: 3 additions & 5 deletions packages/node/src/integrations/tracing/express.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
// Automatic istrumentation for Express using OTel
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import { context } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';

import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core';
import {
Expand All @@ -17,6 +15,7 @@ import {
} from '@sentry/core';
export { expressErrorHandler } from '@sentry/core';
import { DEBUG_BUILD } from '../../debug-build';
import { setHttpServerSpanRouteAttribute } from '../../utils/setHttpServerSpanRouteAttribute';

const INTEGRATION_NAME = 'Express';
const SUPPORTED_VERSIONS = ['>=4.0.0 <6'];
Expand Down Expand Up @@ -51,9 +50,8 @@ export class ExpressInstrumentation extends InstrumentationBase<ExpressInstrumen
patchExpressModule(express, () => ({
...this.getConfig(),
onRouteResolved(route) {
const rpcMetadata = getRPCMetadata(context.active());
if (route && rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = route;
if (route) {
setHttpServerSpanRouteAttribute(route);
}
},
}));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
*/

import { type Attributes, context, SpanStatusCode, trace } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import {
InstrumentationBase,
InstrumentationNodeModuleDefinition,
Expand All @@ -37,6 +36,7 @@ import {
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
spanToJSON,
} from '@sentry/core';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';
import type {
FastifyErrorCodes,
FastifyInstance,
Expand Down Expand Up @@ -99,12 +99,11 @@ export class FastifyInstrumentationV3 extends InstrumentationBase<FastifyInstrum

const anyRequest = request as any;

const rpcMetadata = getRPCMetadata(context.active());
const routeName = anyRequest.routeOptions
? anyRequest.routeOptions.url // since fastify@4.10.0
: request.routerPath;
if (routeName && rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = routeName;
if (routeName) {
setHttpServerSpanRouteAttribute(routeName);
}

const method = request.method || 'GET';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@

import * as dc from 'node:diagnostics_channel';
import { context, trace, SpanStatusCode, propagation, diag, type Span } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import {
ATTR_HTTP_ROUTE,
ATTR_HTTP_RESPONSE_STATUS_CODE,
Expand All @@ -40,6 +39,7 @@ import {
} from '@opentelemetry/semantic-conventions';
import { InstrumentationBase, type InstrumentationConfig } from '@opentelemetry/instrumentation';
import { SDK_VERSION } from '@sentry/core';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';

const PACKAGE_VERSION = SDK_VERSION;
const PACKAGE_NAME = '@sentry/instrumentation-fastify';
Expand Down Expand Up @@ -254,10 +254,8 @@ export class FastifyOtelInstrumentation extends InstrumentationBase<FastifyOtelI
ctx = propagation.extract(ctx, request.headers);
}

const rpcMetadata = getRPCMetadata(ctx);

if (request.routeOptions.url != null && rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = request.routeOptions.url;
if (request.routeOptions.url != null) {
setHttpServerSpanRouteAttribute(request.routeOptions.url);
}

const attributes: Record<string, string> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
/* eslint-disable */

import * as api from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';
import {
InstrumentationBase,
InstrumentationConfig,
Expand Down Expand Up @@ -309,10 +309,7 @@ export class HapiInstrumentation extends InstrumentationBase {
if (api.trace.getSpan(api.context.active()) === undefined) {
return await oldHandler.call(this, ...params);
}
const rpcMetadata = getRPCMetadata(api.context.active());
if (rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = route.path;
}
setHttpServerSpanRouteAttribute(route.path);
const metadata = getRouteMetadata(route, instrumentation._semconvStability, pluginName);
const span = instrumentation.tracer.startSpan(metadata.name, {
attributes: metadata.attributes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
import { KoaLayerType, KoaInstrumentationConfig } from './types';
import { SDK_VERSION } from '@sentry/core';
import { getMiddlewareMetadata, isLayerIgnored } from './utils';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import { setHttpServerSpanRouteAttribute } from '../../../../utils/setHttpServerSpanRouteAttribute';
import { Next, kLayerPatched, KoaContext, KoaMiddleware, KoaPatchedMiddleware } from './internal-types';

const PACKAGE_NAME = '@sentry/instrumentation-koa';
Expand Down Expand Up @@ -156,10 +156,8 @@ export class KoaInstrumentation extends InstrumentationBase<KoaInstrumentationCo
attributes: metadata.attributes,
});

const rpcMetadata = getRPCMetadata(api.context.active());

if (rpcMetadata?.type === RPCType.HTTP && context._matchedRoute) {
rpcMetadata.route = context._matchedRoute.toString();
if (context._matchedRoute) {
setHttpServerSpanRouteAttribute(context._matchedRoute.toString());
}

const { requestHook } = this.getConfig();
Expand Down
23 changes: 23 additions & 0 deletions packages/node/src/utils/setHttpServerSpanRouteAttribute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getActiveSpan, getRootSpan, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core';

/**
* Set the `http.route` attribute on the root HTTP server span for the current trace.
*
* No-op when there is no active span, no root span, or the root span is not an
* `http.server` span — so framework instrumentations can call this unconditionally
* without risking attribute pollution on non-HTTP root spans.
*/
export function setHttpServerSpanRouteAttribute(route: string): void {
const activeSpan = getActiveSpan();
if (!activeSpan) {
return;
}
const rootSpan = getRootSpan(activeSpan);
if (!rootSpan) {
return;
}
if (spanToJSON(rootSpan).data[SEMANTIC_ATTRIBUTE_SENTRY_OP] !== 'http.server') {
return;
}
rootSpan.setAttribute('http.route', route);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { context, createContextKey } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import {
debug,
Expand Down Expand Up @@ -254,11 +253,6 @@ function updateRootSpanWithRoute(method: string, pattern: string | undefined, ur
const hasPattern = !!pattern;
const routeName = hasPattern ? normalizeRoutePath(pattern) || urlPath : urlPath;

const rpcMetadata = getRPCMetadata(context.active());
if (rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = routeName;
}

const transactionName = `${method} ${routeName}`;
updateSpanName(rootSpan, transactionName);
rootSpan.setAttributes({
Expand Down
9 changes: 0 additions & 9 deletions packages/react-router/src/server/wrapSentryHandleRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { context } from '@opentelemetry/api';
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import {
flushIfServerless,
Expand Down Expand Up @@ -73,13 +71,6 @@ export function wrapSentryHandleRequest(
// Normalize route name - avoid "//" for root routes
const routeName = parameterizedPath.startsWith('/') ? parameterizedPath : `/${parameterizedPath}`;

// The express instrumentation writes on the rpcMetadata and that ends up stomping on the `http.route` attribute.
const rpcMetadata = getRPCMetadata(context.active());

if (rpcMetadata?.type === RPCType.HTTP) {
rpcMetadata.route = routeName;
}

const transactionName = `${request.method} ${routeName}`;

updateSpanName(rootSpan, transactionName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ import { PassThrough } from 'stream';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { getMetaTagTransformer } from '../../src/server/getMetaTagTransformer';

vi.mock('@opentelemetry/core', () => ({
RPCType: { HTTP: 'http' },
getRPCMetadata: vi.fn(),
}));

vi.mock('@sentry/core', () => ({
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source',
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { PassThrough } from 'node:stream';
import { RPCType } from '@opentelemetry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import {
flushIfServerless,
Expand All @@ -13,10 +12,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
import { getMetaTagTransformer } from '../../src/server/getMetaTagTransformer';
import { wrapSentryHandleRequest } from '../../src/server/wrapSentryHandleRequest';

vi.mock('@opentelemetry/core', () => ({
RPCType: { HTTP: 'http' },
getRPCMetadata: vi.fn(),
}));
vi.mock('@sentry/core', () => ({
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE: 'sentry.source',
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN: 'sentry.origin',
Expand Down Expand Up @@ -64,13 +59,9 @@ describe('wrapSentryHandleRequest', () => {

const mockActiveSpan = {};
const mockRootSpan = { setAttributes: vi.fn() };
const mockRpcMetadata = { type: RPCType.HTTP, route: '/some-path' };

(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);
const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
getRPCMetadata;

const routerContext = {
staticHandlerContext: {
Expand All @@ -85,7 +76,6 @@ describe('wrapSentryHandleRequest', () => {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler',
});
expect(mockRpcMetadata.route).toBe('/some-path');
});

test('should not set span attributes when parameterized path does not exist', async () => {
Expand Down Expand Up @@ -113,13 +103,10 @@ describe('wrapSentryHandleRequest', () => {
const originalHandler = vi.fn().mockResolvedValue('test');
const wrappedHandler = wrapSentryHandleRequest(originalHandler);

const mockRpcMetadata = { type: RPCType.HTTP, route: '/some-path' };
const mockRootSpan = { setAttributes: vi.fn() };

(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(null);

const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
getRPCMetadata;
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);

const routerContext = {
staticHandlerContext: {
Expand All @@ -129,7 +116,7 @@ describe('wrapSentryHandleRequest', () => {

await wrappedHandler(new Request('https://tio.pepe'), 200, new Headers(), routerContext, {} as any);

expect(getRPCMetadata).not.toHaveBeenCalled();
expect(mockRootSpan.setAttributes).not.toHaveBeenCalled();
});

test('should call flushIfServerless on successful execution', async () => {
Expand Down Expand Up @@ -190,13 +177,9 @@ describe('wrapSentryHandleRequest', () => {

const mockActiveSpan = {};
const mockRootSpan = { setAttributes: vi.fn() };
const mockRpcMetadata = { type: RPCType.HTTP, route: '/some-path' };

(getActiveSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockActiveSpan);
(getRootSpan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(mockRootSpan);
const getRPCMetadata = vi.fn().mockReturnValue(mockRpcMetadata);
(vi.importActual('@opentelemetry/core') as unknown as { getRPCMetadata: typeof getRPCMetadata }).getRPCMetadata =
getRPCMetadata;

const routerContext = {
staticHandlerContext: {
Expand All @@ -211,7 +194,6 @@ describe('wrapSentryHandleRequest', () => {
[ATTR_HTTP_ROUTE]: '/some-path',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
});
expect(mockRpcMetadata.route).toBe('/some-path');
});
});

Expand Down
Loading