Skip to content

Commit a548013

Browse files
authored
feat(react-router): Clean up bogus * http.route attribute on segment spans (#20471)
In span-streaming mode, the existing `processEvent` cleanup of bogus `*` `http.route` attributes doesn't run, so segment spans end up with the garbage route. Ports the same logic to the `processSegmentSpan` hook. Added unit tests for this (also for the event path), since the alternative would have been to add a separate streaming e2e test for react-router, which seems overkill to me. Closes #20361
1 parent 57c0287 commit a548013

2 files changed

Lines changed: 150 additions & 1 deletion

File tree

packages/react-router/src/server/integration/reactRouterServer.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
2-
import { defineIntegration } from '@sentry/core';
2+
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
33
import { generateInstrumentOnce, NODE_VERSION } from '@sentry/node';
44
import { ReactRouterInstrumentation } from '../instrumentation/reactRouter';
55
import { registerServerBuildGlobal } from '../serverBuild';
@@ -60,5 +60,23 @@ export const reactRouterServerIntegration = defineIntegration(() => {
6060

6161
return event;
6262
},
63+
processSegmentSpan(span) {
64+
// Express generates bogus `*` routes for data loaders, which we want to remove here
65+
// we cannot do this earlier because some OTEL instrumentation adds this at some unexpected point
66+
const attributes = span.attributes;
67+
if (attributes?.[ATTR_HTTP_ROUTE] !== '*') {
68+
return;
69+
}
70+
71+
const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];
72+
const isInstrumentationApiOrigin = typeof origin === 'string' && origin.includes('instrumentation_api');
73+
74+
// For instrumentation_api, always clean up bogus `*` route since we set better names
75+
// For legacy, only clean up if the name has been adjusted (not METHOD *)
76+
if (isInstrumentationApiOrigin || !span.name?.endsWith(' *')) {
77+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
78+
delete attributes[ATTR_HTTP_ROUTE];
79+
}
80+
},
6381
};
6482
});

packages/react-router/test/server/integration/reactRouterServer.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
2+
import type { Client, Event, EventType, StreamedSpanJSON } from '@sentry/core';
13
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
24
import { ReactRouterInstrumentation } from '../../../src/server/instrumentation/reactRouter';
35
import { reactRouterServerIntegration } from '../../../src/server/integration/reactRouterServer';
@@ -98,4 +100,133 @@ describe('reactRouterServerIntegration', () => {
98100
expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
99101
expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
100102
});
103+
104+
describe('processEvent', () => {
105+
const client = {} as Client;
106+
const hint = {};
107+
108+
it('preserves http.route when it is not "*"', () => {
109+
const integration = reactRouterServerIntegration();
110+
const event = {
111+
type: 'transaction' as EventType,
112+
transaction: 'GET /users/:id',
113+
contexts: {
114+
trace: {
115+
data: { [ATTR_HTTP_ROUTE]: '/users/:id' },
116+
origin: 'auto.http.otel.http',
117+
},
118+
},
119+
} as unknown as Event;
120+
121+
integration.processEvent!(event, hint, client);
122+
123+
expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBe('/users/:id');
124+
});
125+
126+
it('deletes bogus "*" route when origin is instrumentation_api', () => {
127+
const integration = reactRouterServerIntegration();
128+
const event = {
129+
type: 'transaction' as EventType,
130+
transaction: 'GET *',
131+
contexts: {
132+
trace: {
133+
data: { [ATTR_HTTP_ROUTE]: '*' },
134+
origin: 'auto.http.otel.instrumentation_api',
135+
},
136+
},
137+
} as unknown as Event;
138+
139+
integration.processEvent!(event, hint, client);
140+
141+
expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBeUndefined();
142+
});
143+
144+
it('deletes bogus "*" route when legacy origin and transaction name was renamed', () => {
145+
const integration = reactRouterServerIntegration();
146+
const event = {
147+
type: 'transaction' as EventType,
148+
transaction: 'GET /api/users',
149+
contexts: {
150+
trace: {
151+
data: { [ATTR_HTTP_ROUTE]: '*' },
152+
origin: 'auto.http.otel.http',
153+
},
154+
},
155+
} as unknown as Event;
156+
157+
integration.processEvent!(event, hint, client);
158+
159+
expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBeUndefined();
160+
});
161+
162+
it('keeps "*" when legacy origin and transaction name still ends with " *"', () => {
163+
const integration = reactRouterServerIntegration();
164+
const event = {
165+
type: 'transaction' as EventType,
166+
transaction: 'GET *',
167+
contexts: {
168+
trace: {
169+
data: { [ATTR_HTTP_ROUTE]: '*' },
170+
origin: 'auto.http.otel.http',
171+
},
172+
},
173+
} as unknown as Event;
174+
175+
integration.processEvent!(event, hint, client);
176+
177+
expect(event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE]).toBe('*');
178+
});
179+
});
180+
181+
describe('processSegmentSpan', () => {
182+
const client = {} as Client;
183+
184+
it('preserves http.route when it is not "*"', () => {
185+
const integration = reactRouterServerIntegration();
186+
const span = {
187+
name: 'GET /users/:id',
188+
attributes: { [ATTR_HTTP_ROUTE]: '/users/:id', 'sentry.origin': 'auto.http.otel.http' },
189+
} as unknown as StreamedSpanJSON;
190+
191+
integration.processSegmentSpan!(span, client);
192+
193+
expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBe('/users/:id');
194+
});
195+
196+
it('deletes bogus "*" route when origin is instrumentation_api', () => {
197+
const integration = reactRouterServerIntegration();
198+
const span = {
199+
name: 'GET *',
200+
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.instrumentation_api' },
201+
} as unknown as StreamedSpanJSON;
202+
203+
integration.processSegmentSpan!(span, client);
204+
205+
expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBeUndefined();
206+
});
207+
208+
it('deletes bogus "*" route when legacy origin and span name was renamed', () => {
209+
const integration = reactRouterServerIntegration();
210+
const span = {
211+
name: 'GET /api/users',
212+
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
213+
} as unknown as StreamedSpanJSON;
214+
215+
integration.processSegmentSpan!(span, client);
216+
217+
expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBeUndefined();
218+
});
219+
220+
it('keeps "*" when legacy origin and span name still ends with " *"', () => {
221+
const integration = reactRouterServerIntegration();
222+
const span = {
223+
name: 'GET *',
224+
attributes: { [ATTR_HTTP_ROUTE]: '*', 'sentry.origin': 'auto.http.otel.http' },
225+
} as unknown as StreamedSpanJSON;
226+
227+
integration.processSegmentSpan!(span, client);
228+
229+
expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBe('*');
230+
});
231+
});
101232
});

0 commit comments

Comments
 (0)