Skip to content
Draft
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
2 changes: 1 addition & 1 deletion packages/react-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
"@opentelemetry/api": "^1.9.1",
"@opentelemetry/core": "^2.6.1",
"@opentelemetry/instrumentation": "^0.214.0",
"@opentelemetry/semantic-conventions": "^1.40.0",
"@sentry/browser": "10.57.0",
"@sentry/cli": "^2.58.6",
"@sentry/core": "10.57.0",
Expand All @@ -60,6 +59,7 @@
"devDependencies": {
"@react-router/dev": "^7.13.0",
"@react-router/node": "^7.13.1",
"@sentry/conventions": "^0.11.0",
"react": "^18.3.1",
"react-router": "^7.15.0",
"vite": "^6.4.2"
Expand Down
3 changes: 3 additions & 0 deletions packages/react-router/rollup.npm.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export default [
output: {
// make it so Rollup calms down about the fact that we're combining default and named exports
exports: 'named',
// keep emitted module paths relative to `src` so the bundled `@sentry/conventions`
// (a devDependency, vendored into the build) doesn't shift the output layout
preserveModulesRoot: 'src',
},
},
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { context, createContextKey } from '@opentelemetry/api';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import {
debug,
flushIfServerless,
Expand Down Expand Up @@ -200,7 +200,7 @@ export function createSentryServerInstrumentation(
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.react_router.middleware',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.react_router.instrumentation_api',
'react_router.route.id': routeId,
[ATTR_HTTP_ROUTE]: routePattern,
[HTTP_ROUTE]: routePattern,
...(middlewareName && { 'react_router.middleware.name': middlewareName }),
'react_router.middleware.index': middlewareIndex,
},
Expand Down Expand Up @@ -259,7 +259,7 @@ function updateRootSpanWithRoute(method: string, pattern: string | undefined, ur
const transactionName = `${method} ${routeName}`;
updateSpanName(rootSpan, transactionName);
rootSpan.setAttributes({
[ATTR_HTTP_ROUTE]: routeName,
[HTTP_ROUTE]: routeName,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: hasPattern ? 'route' : 'url',
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
import { HTTP_TARGET } from '@sentry/conventions/attributes';
import {
debug,
getActiveSpan,
Expand Down Expand Up @@ -112,8 +112,7 @@ export class ReactRouterInstrumentation extends InstrumentationBase<Instrumentat
// So we force this to be a more sensible name here
// TODO: try to set derived parameterized route from build here (args[0])
const spanData = spanToJSON(rootSpan);
// eslint-disable-next-line deprecation/deprecation
const target = spanData.data[SEMATTRS_HTTP_TARGET] || url.pathname;
const target = spanData.data[HTTP_TARGET] || url.pathname;
updateSpanName(rootSpan, `${request.method} ${target}`);
rootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { generateInstrumentOnce, NODE_VERSION } from '@sentry/node';
import { ReactRouterInstrumentation } from '../instrumentation/reactRouter';
Expand Down Expand Up @@ -45,7 +45,7 @@ export const reactRouterServerIntegration = defineIntegration(() => {
if (
event.type === 'transaction' &&
event.contexts?.trace?.data &&
event.contexts.trace.data[ATTR_HTTP_ROUTE] === '*'
event.contexts.trace.data[HTTP_ROUTE] === '*'
) {
const origin = event.contexts.trace.origin;
const isInstrumentationApiOrigin = origin?.includes('instrumentation_api');
Expand All @@ -54,7 +54,7 @@ export const reactRouterServerIntegration = defineIntegration(() => {
// For legacy, only clean up if the name has been adjusted (not METHOD *)
if (isInstrumentationApiOrigin || !event.transaction?.endsWith(' *')) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete event.contexts.trace.data[ATTR_HTTP_ROUTE];
delete event.contexts.trace.data[HTTP_ROUTE];
}
}

Expand All @@ -64,7 +64,7 @@ export const reactRouterServerIntegration = defineIntegration(() => {
// Express generates bogus `*` routes for data loaders, which we want to remove here
// we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point
const attributes = span.attributes;
if (attributes?.[ATTR_HTTP_ROUTE] !== '*') {
if (attributes?.[HTTP_ROUTE] !== '*') {
return;
}

Expand All @@ -75,7 +75,7 @@ export const reactRouterServerIntegration = defineIntegration(() => {
// For legacy, only clean up if the name has been adjusted (not METHOD *)
if (isInstrumentationApiOrigin || !span.name?.endsWith(' *')) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete attributes[ATTR_HTTP_ROUTE];
delete attributes[HTTP_ROUTE];
}
},
};
Expand Down
6 changes: 3 additions & 3 deletions packages/react-router/src/server/wrapSentryHandleRequest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import {
flushIfServerless,
getActiveSpan,
Expand Down Expand Up @@ -80,12 +80,12 @@ export function wrapSentryHandleRequest(
// Don't override origin when instrumentation API is used (preserve instrumentation_api origin)
if (isInstrumentationApiUsed()) {
rootSpan.setAttributes({
[ATTR_HTTP_ROUTE]: routeName,
[HTTP_ROUTE]: routeName,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
});
} else {
rootSpan.setAttributes({
[ATTR_HTTP_ROUTE]: routeName,
[HTTP_ROUTE]: routeName,
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler',
});
Expand Down
5 changes: 2 additions & 3 deletions packages/react-router/src/server/wrapServerAction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
import { HTTP_TARGET } from '@sentry/conventions/attributes';
import type { SpanAttributes } from '@sentry/core';
import {
debug,
Expand Down Expand Up @@ -66,8 +66,7 @@ export function wrapServerAction<T>(
const root = getRootSpan(active);
const spanData = spanToJSON(root);
if (spanData.origin === 'auto.http.otel.http') {
// eslint-disable-next-line deprecation/deprecation
const target = spanData.data[SEMATTRS_HTTP_TARGET];
const target = spanData.data[HTTP_TARGET];

if (target) {
// We cannot rely on the regular span name inferral here, as the express instrumentation sets `*` as the route
Expand Down
5 changes: 2 additions & 3 deletions packages/react-router/src/server/wrapServerLoader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SEMATTRS_HTTP_TARGET } from '@opentelemetry/semantic-conventions';
import { HTTP_TARGET } from '@sentry/conventions/attributes';
import type { SpanAttributes } from '@sentry/core';
import {
debug,
Expand Down Expand Up @@ -67,8 +67,7 @@ export function wrapServerLoader<T>(
const root = getRootSpan(active);
const spanData = spanToJSON(root);
if (spanData.origin === 'auto.http.otel.http') {
// eslint-disable-next-line deprecation/deprecation
const target = spanData.data[SEMATTRS_HTTP_TARGET];
const target = spanData.data[HTTP_TARGET];

if (target) {
// We cannot rely on the regular span name inferral here, as the express instrumentation sets `*` as the route
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import type { Client, Event, EventType, StreamedSpanJSON } from '@sentry/core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter';
Expand Down Expand Up @@ -112,15 +112,15 @@ describe('reactRouterServerIntegration', () => {
transaction: 'GET /users/:id',
contexts: {
trace: {
data: { [ATTR_HTTP_ROUTE]: '/users/:id' },
data: { [HTTP_ROUTE]: '/users/:id' },
origin: 'auto.http.otel.http',
},
},
} as unknown as Event;

integration.processEvent!(event, hint, client);

expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBe('/users/:id');
expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('/users/:id');
});

it('deletes bogus "*" route when origin is instrumentation_api', () => {
Expand All @@ -130,15 +130,15 @@ describe('reactRouterServerIntegration', () => {
transaction: 'GET *',
contexts: {
trace: {
data: { [ATTR_HTTP_ROUTE]: '*' },
data: { [HTTP_ROUTE]: '*' },
origin: 'auto.http.otel.instrumentation_api',
},
},
} as unknown as Event;

integration.processEvent!(event, hint, client);

expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBeUndefined();
expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined();
});

it('deletes bogus "*" route when legacy origin and transaction name was renamed', () => {
Expand All @@ -148,15 +148,15 @@ describe('reactRouterServerIntegration', () => {
transaction: 'GET /api/users',
contexts: {
trace: {
data: { [ATTR_HTTP_ROUTE]: '*' },
data: { [HTTP_ROUTE]: '*' },
origin: 'auto.http.otel.http',
},
},
} as unknown as Event;

integration.processEvent!(event, hint, client);

expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBeUndefined();
expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBeUndefined();
});

it('keeps "*" when legacy origin and transaction name still ends with " *"', () => {
Expand All @@ -166,15 +166,15 @@ describe('reactRouterServerIntegration', () => {
transaction: 'GET *',
contexts: {
trace: {
data: { [ATTR_HTTP_ROUTE]: '*' },
data: { [HTTP_ROUTE]: '*' },
origin: 'auto.http.otel.http',
},
},
} as unknown as Event;

integration.processEvent!(event, hint, client);

expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBe('*');
expect(event.contexts?.trace?.data?.[HTTP_ROUTE]).toBe('*');
});
});

Expand All @@ -185,48 +185,48 @@ describe('reactRouterServerIntegration', () => {
const integration = reactRouterServerIntegration();
const span = {
name: 'GET /users/:id',
attributes: { [ATTR_HTTP_ROUTE]: '/users/:id', 'sentry.origin': 'auto.http.otel.http' },
attributes: { [HTTP_ROUTE]: '/users/:id', 'sentry.origin': 'auto.http.otel.http' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBe('/users/:id');
expect(span.attributes?.[HTTP_ROUTE]).toBe('/users/:id');
});

it('deletes bogus "*" route when origin is instrumentation_api', () => {
const integration = reactRouterServerIntegration();
const span = {
name: 'GET *',
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.instrumentation_api' },
attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.instrumentation_api' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBeUndefined();
expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined();
});

it('deletes bogus "*" route when legacy origin and span name was renamed', () => {
const integration = reactRouterServerIntegration();
const span = {
name: 'GET /api/users',
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBeUndefined();
expect(span.attributes?.[HTTP_ROUTE]).toBeUndefined();
});

it('keeps "*" when legacy origin and span name still ends with " *"', () => {
const integration = reactRouterServerIntegration();
const span = {
name: 'GET *',
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
attributes: { [HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBe('*');
expect(span.attributes?.[HTTP_ROUTE]).toBe('*');
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PassThrough } from 'node:stream';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { HTTP_ROUTE } from '@sentry/conventions/attributes';
import {
flushIfServerless,
getActiveSpan,
Expand Down Expand Up @@ -72,7 +72,7 @@ describe('wrapSentryHandleRequest', () => {
await wrappedHandler(new Request('https://nacho.queso'), 200, new Headers(), routerContext, {} as any);

expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({
[ATTR_HTTP_ROUTE]: '/some-path',
[HTTP_ROUTE]: '/some-path',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react_router.request_handler',
});
Expand Down Expand Up @@ -191,7 +191,7 @@ describe('wrapSentryHandleRequest', () => {

// Should set route attributes without origin (to preserve instrumentation_api origin)
expect(mockRootSpan.setAttributes).toHaveBeenCalledWith({
[ATTR_HTTP_ROUTE]: '/some-path',
[HTTP_ROUTE]: '/some-path',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
});
});
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7578,6 +7578,11 @@
"@sentry/cli-win32-i686" "2.58.6"
"@sentry/cli-win32-x64" "2.58.6"

"@sentry/conventions@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@sentry/conventions/-/conventions-0.11.0.tgz#5a324b8368dc5c141260bd8ccc684756ea3dd843"
integrity sha512-AQTAKeq9mDpOElDFSPymZTPZF/c50rk355mWTf5Y1ZxZJKKOBli5qTttskJyCxrE5ynNgN1KwcXoU5MRrMSRmQ==

"@sentry/node-cpu-profiler@^2.4.2":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@sentry/node-cpu-profiler/-/node-cpu-profiler-2.4.2.tgz#d0ba01370545297d015df1497daf7f81e27f2ab5"
Expand Down
Loading