Skip to content

Commit 7d367d8

Browse files
committed
update data request tx
1 parent a55f023 commit 7d367d8

File tree

9 files changed

+100
-7
lines changed

9 files changed

+100
-7
lines changed

dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@ export default [
1515
route('ssr', 'routes/performance/ssr.tsx'),
1616
route('with/:param', 'routes/performance/dynamic-param.tsx'),
1717
route('static', 'routes/performance/static.tsx'),
18+
route('server-loader', 'routes/performance/server-loader.tsx'),
1819
]),
1920
] satisfies RouteConfig;

dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/dynamic-param.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import type { Route } from './+types/dynamic-param';
22

3+
export async function loader() {
4+
await new Promise(resolve => setTimeout(resolve, 500));
5+
return { data: 'burritos' };
6+
}
7+
38
export default function DynamicParamPage({ params }: Route.ComponentProps) {
49
const { param } = params;
510

dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default function PerformancePage() {
77
<nav>
88
<Link to="/performance/ssr">SSR Page</Link>
99
<Link to="/performance/with/sentry">With Param Page</Link>
10+
<Link to="/performance/server-loader">Server Loader</Link>
1011
</nav>
1112
</div>
1213
);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { Route } from './+types/server-loader';
2+
3+
export async function loader() {
4+
await new Promise(resolve => setTimeout(resolve, 500));
5+
return { data: 'burritos' };
6+
}
7+
8+
export default function ServerLoaderPage({ loaderData }: Route.ComponentProps) {
9+
const { data } = loaderData;
10+
return (
11+
<div>
12+
<h1>Server Loader Page</h1>
13+
<div>{data}</div>
14+
</div>
15+
);
16+
}

dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,32 @@ test.describe('servery - performance', () => {
104104
},
105105
});
106106
});
107+
108+
test('should automatically instrument server loader', async ({ page }) => {
109+
const txPromise = waitForTransaction(APP_NAME, async transactionEvent => {
110+
return transactionEvent.transaction === 'GET /performance/server-loader.data';
111+
});
112+
113+
await page.goto(`/performance`); // initial ssr pageloads do not contain .data requests
114+
await page.waitForTimeout(500); // quick breather before navigation
115+
await page.getByRole('link', { name: 'Server Loader' }).click(); // this will actually trigger a .data request
116+
117+
const transaction = await txPromise;
118+
119+
expect(transaction?.spans?.[transaction.spans?.length - 1]).toMatchObject({
120+
span_id: expect.any(String),
121+
trace_id: expect.any(String),
122+
data: {
123+
'sentry.origin': 'auto.http.react-router',
124+
'sentry.op': 'function.react-router.loader',
125+
},
126+
description: 'Executing Server Loader',
127+
parent_span_id: expect.any(String),
128+
start_timestamp: expect.any(Number),
129+
timestamp: expect.any(Number),
130+
status: 'ok',
131+
op: 'function.react-router.loader',
132+
origin: 'auto.http.react-router',
133+
});
134+
});
107135
});

packages/react-router/src/server/instrumentation/reactRouter.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@sentry/core';
1313
import type * as reactRouter from 'react-router';
1414
import { DEBUG_BUILD } from '../../common/debug-build';
15-
import { getOpName, getSpanName, isDataRequest } from './util';
15+
import { getOpName, getSpanName, isDataRequest, SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE } from './util';
1616

1717
type ReactRouterModuleExports = typeof reactRouter;
1818

@@ -81,15 +81,20 @@ export class ReactRouterInstrumentation extends InstrumentationBase<Instrumentat
8181
return originalRequestHandler(request, initialContext);
8282
}
8383

84+
// Set the source and overwrite attributes on the root span to ensure the transaction name
85+
// is derived from the raw URL pathname rather than any parameterized route that may be set later
86+
// TODO: try to set derived parameterized route from build here (args[0])
87+
rootSpan.setAttributes({
88+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
89+
[SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE]: `${request.method} ${url.pathname}`,
90+
});
91+
8492
return startSpan(
8593
{
8694
name: getSpanName(url.pathname, request.method),
8795
attributes: {
88-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
8996
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.react-router',
9097
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: getOpName(url.pathname, request.method),
91-
url: url.pathname,
92-
method: request.method,
9398
},
9499
},
95100
() => {

packages/react-router/src/server/instrumentation/util.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ export function isActionRequest(pathname: string, requestMethod: string): boolea
4949
export function isDataRequest(pathname: string): boolean {
5050
return pathname.endsWith('.data');
5151
}
52+
53+
export const SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE = 'sentry.overwrite-route';

packages/react-router/src/server/sdk.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import type { Integration } from '@sentry/core';
2-
import { applySdkMetadata, logger, setTag } from '@sentry/core';
1+
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
2+
import type { EventProcessor, Integration } from '@sentry/core';
3+
import { applySdkMetadata, getGlobalScope, logger, setTag } from '@sentry/core';
34
import type { NodeClient, NodeOptions } from '@sentry/node';
45
import { getDefaultIntegrations, init as initNodeSdk } from '@sentry/node';
56
import { DEBUG_BUILD } from '../common/debug-build';
7+
import { SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE } from './instrumentation/util';
68
import { reactRouterServerIntegration } from './integration/reactRouterServer';
79

810
/**
@@ -22,6 +24,30 @@ export function init(options: NodeOptions): NodeClient | undefined {
2224

2325
setTag('runtime', 'node');
2426

27+
// Overwrite the transaction name for instrumented data loaders because the trace data gets overwritten at a later point.
28+
// We only update the tx in case SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE got set in our instrumentation before.
29+
getGlobalScope().addEventProcessor(
30+
Object.assign(
31+
(event => {
32+
const overwrite = event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE];
33+
if (
34+
event.type === 'transaction' &&
35+
event.transaction === 'GET *' &&
36+
event.contexts?.trace?.data?.[ATTR_HTTP_ROUTE] === '*' &&
37+
overwrite
38+
) {
39+
event.transaction = overwrite;
40+
delete event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_OVERWRITE];
41+
event.contexts.trace.data[ATTR_HTTP_ROUTE] = 'url';
42+
return event;
43+
} else {
44+
return event;
45+
}
46+
}) satisfies EventProcessor,
47+
{ id: 'ReactRouterTransactionEnhancer' },
48+
),
49+
);
50+
2551
DEBUG_BUILD && logger.log('SDK successfully initialized');
2652
return client;
2753
}

packages/react-router/src/server/wrapSentryHandleRequest.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { context } from '@opentelemetry/api';
22
import { getRPCMetadata, RPCType } from '@opentelemetry/core';
33
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
4-
import { getActiveSpan, getRootSpan, getTraceMetaTags, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
4+
import {
5+
getActiveSpan,
6+
getRootSpan,
7+
getTraceMetaTags,
8+
SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME,
9+
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
10+
} from '@sentry/core';
511
import type { AppLoadContext, EntryContext } from 'react-router';
612
import type { PassThrough } from 'stream';
713
import { Transform } from 'stream';
@@ -30,6 +36,7 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
3036
) {
3137
const parameterizedPath =
3238
routerContext?.staticHandlerContext?.matches?.[routerContext.staticHandlerContext.matches.length - 1]?.route.path;
39+
3340
if (parameterizedPath) {
3441
const activeSpan = getActiveSpan();
3542
if (activeSpan) {
@@ -38,6 +45,7 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
3845

3946
// The express instrumentation writes on the rpcMetadata and that ends up stomping on the `http.route` attribute.
4047
const rpcMetadata = getRPCMetadata(context.active());
48+
4149
if (rpcMetadata?.type === RPCType.HTTP) {
4250
rpcMetadata.route = routeName;
4351
}
@@ -46,6 +54,7 @@ export function wrapSentryHandleRequest(originalHandle: OriginalHandleRequest):
4654
rootSpan.setAttributes({
4755
[ATTR_HTTP_ROUTE]: routeName,
4856
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
57+
[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]: `${request.method} ${routeName}`,
4958
});
5059
}
5160
}

0 commit comments

Comments
 (0)