Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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