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 number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import { defineIntegration } from '@sentry/core';
import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import { generateInstrumentOnce, NODE_VERSION } from '@sentry/node';
import { ReactRouterInstrumentation } from '../instrumentation/reactRouter';
import { registerServerBuildGlobal } from '../serverBuild';
Expand Down Expand Up @@ -60,5 +60,23 @@ export const reactRouterServerIntegration = defineIntegration(() => {

return event;
},
processSegmentSpan(span) {
// 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] !== '*') {
return;
}

const origin = attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN];
const isInstrumentationApiOrigin = typeof origin === 'string' && origin.includes('instrumentation_api');

// For instrumentation_api, always clean up bogus `*` route since we set better names
// 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];
}
},
};
});
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
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';
import { reactRouterServerIntegration } from '../../../src/server/integration/reactRouterServer';
Expand Down Expand Up @@ -98,4 +100,133 @@ describe('reactRouterServerIntegration', () => {
expect(ReactRouterInstrumentation).toHaveBeenCalledTimes(1);
expect(registerServerBuildGlobalSpy).toHaveBeenCalledTimes(1);
});

describe('processEvent', () => {
const client = {} as Client;
const hint = {};

it('preserves http.route when it is not "*"', () => {
const integration = reactRouterServerIntegration();
const event = {
type: 'transaction' as EventType,
transaction: 'GET /users/:id',
contexts: {
trace: {
data: { [ATTR_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');
});

it('deletes bogus "*" route when origin is instrumentation_api', () => {
const integration = reactRouterServerIntegration();
const event = {
type: 'transaction' as EventType,
transaction: 'GET *',
contexts: {
trace: {
data: { [ATTR_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();
});

it('deletes bogus "*" route when legacy origin and transaction name was renamed', () => {
const integration = reactRouterServerIntegration();
const event = {
type: 'transaction' as EventType,
transaction: 'GET /api/users',
contexts: {
trace: {
data: { [ATTR_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();
});

it('keeps "*" when legacy origin and transaction name still ends with " *"', () => {
const integration = reactRouterServerIntegration();
const event = {
type: 'transaction' as EventType,
transaction: 'GET *',
contexts: {
trace: {
data: { [ATTR_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('*');
});
});

describe('processSegmentSpan', () => {
const client = {} as Client;

it('preserves http.route when it is not "*"', () => {
const integration = reactRouterServerIntegration();
const span = {
name: 'GET /users/:id',
attributes: { [ATTR_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');
});

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' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_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' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_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' },
} as unknown as StreamedSpanJSON;

integration.processSegmentSpan!(span, client);

expect(span.attributes?.[ATTR_HTTP_ROUTE]).toBe('*');
});
});
});
Loading