Skip to content

Commit 74672a0

Browse files
authored
feat(remix): Continue transaction from request headers (#5600)
Enables propogation of traces through `sentry-trace` and dynamic sampling propogation through `baggage`
1 parent ef11a5e commit 74672a0

File tree

4 files changed

+103
-21
lines changed

4 files changed

+103
-21
lines changed

packages/node-integration-tests/utils/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import * as Sentry from '@sentry/node';
33
import { EnvelopeItemType } from '@sentry/types';
44
import { logger, parseSemver } from '@sentry/utils';
5-
import axios from 'axios';
5+
import axios, { AxiosRequestConfig } from 'axios';
66
import { Express } from 'express';
77
import * as http from 'http';
88
import nock from 'nock';
@@ -102,12 +102,16 @@ export async function runScenario(url: string): Promise<void> {
102102
await Sentry.flush();
103103
}
104104

105-
async function makeRequest(method: 'get' | 'post' = 'get', url: string): Promise<void> {
105+
async function makeRequest(
106+
method: 'get' | 'post' = 'get',
107+
url: string,
108+
axiosConfig?: AxiosRequestConfig,
109+
): Promise<void> {
106110
try {
107111
if (method === 'get') {
108-
await axios.get(url);
112+
await axios.get(url, axiosConfig);
109113
} else {
110-
await axios.post(url);
114+
await axios.post(url, axiosConfig);
111115
}
112116
} catch (e) {
113117
// We sometimes expect the request to fail, but not the test.
@@ -117,6 +121,8 @@ async function makeRequest(method: 'get' | 'post' = 'get', url: string): Promise
117121
}
118122

119123
export class TestEnv {
124+
private _axiosConfig: AxiosRequestConfig | undefined = undefined;
125+
120126
public constructor(public readonly server: http.Server, public readonly url: string) {
121127
this.server = server;
122128
this.url = url;
@@ -173,7 +179,7 @@ export class TestEnv {
173179
envelopeTypeArray,
174180
);
175181

176-
void makeRequest(options.method, options.url || this.url);
182+
void makeRequest(options.method, options.url || this.url, this._axiosConfig);
177183
return resProm;
178184
}
179185

@@ -246,4 +252,8 @@ export class TestEnv {
246252
.reply(200);
247253
});
248254
}
255+
256+
public setAxiosConfig(axiosConfig: AxiosRequestConfig): void {
257+
this._axiosConfig = axiosConfig;
258+
}
249259
}

packages/remix/src/utils/instrumentServer.ts

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
/* eslint-disable max-lines */
22
import { captureException, getCurrentHub, Hub } from '@sentry/node';
33
import { getActiveTransaction, hasTracingEnabled } from '@sentry/tracing';
4-
import { Transaction, WrappedFunction } from '@sentry/types';
5-
import { addExceptionMechanism, fill, isNodeEnv, loadModule, logger, serializeBaggage } from '@sentry/utils';
4+
import { Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
5+
import {
6+
addExceptionMechanism,
7+
extractTraceparentData,
8+
fill,
9+
isNodeEnv,
10+
isSentryBaggageEmpty,
11+
loadModule,
12+
logger,
13+
parseBaggageSetMutability,
14+
serializeBaggage,
15+
} from '@sentry/utils';
616
import * as domain from 'domain';
717

818
import {
@@ -289,33 +299,52 @@ function matchServerRoutes(
289299
* @param pkg
290300
*/
291301
export function startRequestHandlerTransaction(
292-
url: URL,
293-
method: string,
294-
routes: ServerRoute[],
295302
hub: Hub,
296-
pkg?: ReactRouterDomPkg,
303+
name: string,
304+
source: TransactionSource,
305+
request: {
306+
headers: {
307+
'sentry-trace': string;
308+
baggage: string;
309+
};
310+
method: string;
311+
},
297312
): Transaction {
298-
const currentScope = hub.getScope();
299-
const matches = matchServerRoutes(routes, url.pathname, pkg);
313+
// If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
314+
const traceparentData = extractTraceparentData(request.headers['sentry-trace']);
315+
const baggage = parseBaggageSetMutability(request.headers.baggage, traceparentData);
300316

301-
const match = matches && getRequestMatch(url, matches);
302-
const name = match === null ? url.pathname : match.route.id;
303-
const source = match === null ? 'url' : 'route';
304317
const transaction = hub.startTransaction({
305318
name,
306319
op: 'http.server',
307320
tags: {
308-
method: method,
321+
method: request.method,
309322
},
323+
...traceparentData,
310324
metadata: {
311325
source,
326+
// Only attach baggage if it's defined
327+
...(!isSentryBaggageEmpty(baggage) && { baggage }),
312328
},
313329
});
314330

315-
currentScope?.setSpan(transaction);
331+
hub.getScope()?.setSpan(transaction);
316332
return transaction;
317333
}
318334

335+
/**
336+
* Get transaction name from routes and url
337+
*/
338+
export function getTransactionName(
339+
routes: ServerRoute[],
340+
url: URL,
341+
pkg?: ReactRouterDomPkg,
342+
): [string, TransactionSource] {
343+
const matches = matchServerRoutes(routes, url.pathname, pkg);
344+
const match = matches && getRequestMatch(url, matches);
345+
return match === null ? [url.pathname, 'url'] : [match.route.id, 'route'];
346+
}
347+
319348
function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBuild): RequestHandler {
320349
const routes = createRoutes(build.routes);
321350
const pkg = loadModule<ReactRouterDomPkg>('react-router-dom');
@@ -330,7 +359,15 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
330359
}
331360

332361
const url = new URL(request.url);
333-
const transaction = startRequestHandlerTransaction(url, request.method, routes, hub, pkg);
362+
const [name, source] = getTransactionName(routes, url, pkg);
363+
364+
const transaction = startRequestHandlerTransaction(hub, name, source, {
365+
headers: {
366+
'sentry-trace': request.headers.get('sentry-trace') || '',
367+
baggage: request.headers.get('baggage') || '',
368+
},
369+
method: request.method,
370+
});
334371

335372
const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
336373

packages/remix/src/utils/serverAdapters/express.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { getCurrentHub } from '@sentry/hub';
22
import { flush } from '@sentry/node';
33
import { hasTracingEnabled } from '@sentry/tracing';
44
import { Transaction } from '@sentry/types';
5-
import { extractRequestData, loadModule, logger } from '@sentry/utils';
5+
import { extractRequestData, isString, loadModule, logger } from '@sentry/utils';
66

77
import {
88
createRoutes,
9+
getTransactionName,
910
instrumentBuild,
1011
isRequestHandlerWrapped,
1112
startRequestHandlerTransaction,
@@ -51,7 +52,14 @@ function wrapExpressRequestHandler(
5152
}
5253

5354
const url = new URL(request.url);
54-
const transaction = startRequestHandlerTransaction(url, request.method, routes, hub, pkg);
55+
const [name, source] = getTransactionName(routes, url, pkg);
56+
const transaction = startRequestHandlerTransaction(hub, name, source, {
57+
headers: {
58+
'sentry-trace': (req.headers && isString(req.headers['sentry-trace']) && req.headers['sentry-trace']) || '',
59+
baggage: (req.headers && isString(req.headers.baggage) && req.headers.baggage) || '',
60+
},
61+
method: request.method,
62+
});
5563
// save a link to the transaction on the response, so that even if there's an error (landing us outside of
5664
// the domain), we can still finish it (albeit possibly missing some scope data)
5765
(res as AugmentedExpressResponse).__sentryTransaction = transaction;

packages/remix/test/integration/test/server/loader.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,31 @@ describe.each(['builtin', 'express'])('Remix API Loaders with adapter = %s', ada
157157
expect(tags[key]).toEqual(val);
158158
});
159159
});
160+
161+
it('continues transaction from sentry-trace header and baggage', async () => {
162+
const env = await RemixTestEnv.init(adapter);
163+
const url = `${env.url}/loader-json-response/3`;
164+
165+
// send sentry-trace and baggage headers to loader
166+
env.setAxiosConfig({
167+
headers: {
168+
'sentry-trace': '12312012123120121231201212312012-1121201211212012-1',
169+
baggage: 'sentry-version=1.0,sentry-environment=production,sentry-trace_id=12312012123120121231201212312012',
170+
},
171+
});
172+
const envelope = await env.getEnvelopeRequest({ url, envelopeType: 'transaction' });
173+
174+
expect(envelope[0].trace).toMatchObject({
175+
trace_id: '12312012123120121231201212312012',
176+
});
177+
178+
assertSentryTransaction(envelope[2], {
179+
contexts: {
180+
trace: {
181+
trace_id: '12312012123120121231201212312012',
182+
parent_span_id: '1121201211212012',
183+
},
184+
},
185+
});
186+
});
160187
});

0 commit comments

Comments
 (0)