Skip to content

Commit cd5d63a

Browse files
authored
feat(remix): Refactor to use new performance APIs (#10980)
This removes all usage of `startTransaction` and `span.startChild()` in remix. Instead, we now use `startSpan`, which works reasonably well as we always have callbacks anyhow. I also updated it to use `continueTrace` for trace propagation.
1 parent 24c4eba commit cd5d63a

File tree

4 files changed

+141
-144
lines changed

4 files changed

+141
-144
lines changed

packages/remix/src/utils/instrumentServer.ts

Lines changed: 113 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
/* eslint-disable max-lines */
22
import {
3+
SEMANTIC_ATTRIBUTE_SENTRY_OP,
34
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
45
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
6+
continueTrace,
57
getActiveSpan,
68
getActiveTransaction,
79
getClient,
8-
getCurrentScope,
910
getDynamicSamplingContextFromSpan,
11+
getRootSpan,
12+
handleCallbackErrors,
1013
hasTracingEnabled,
1114
setHttpStatus,
1215
spanToJSON,
1316
spanToTraceHeader,
17+
startSpan,
1418
withIsolationScope,
1519
} from '@sentry/core';
16-
import { captureException, getCurrentHub } from '@sentry/node-experimental';
17-
import type { Hub, Transaction, TransactionSource, WrappedFunction } from '@sentry/types';
20+
import { captureException } from '@sentry/node-experimental';
21+
import type { Span, TransactionSource, WrappedFunction } from '@sentry/types';
1822
import {
1923
addExceptionMechanism,
2024
dynamicSamplingContextToSentryBaggageHeader,
@@ -24,7 +28,6 @@ import {
2428
loadModule,
2529
logger,
2630
objectify,
27-
tracingContextFromHeaders,
2831
} from '@sentry/utils';
2932

3033
import { DEBUG_BUILD } from './debug-build';
@@ -186,46 +189,50 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) {
186189
context: EntryContext,
187190
loadContext?: Record<string, unknown>,
188191
): Promise<Response> {
189-
let res: Response;
190-
// eslint-disable-next-line deprecation/deprecation
191-
const activeTransaction = getActiveTransaction();
192+
const activeSpan = getActiveSpan();
193+
const rootSpan = activeSpan && getRootSpan(activeSpan);
192194

193-
try {
194-
// eslint-disable-next-line deprecation/deprecation
195-
const span = activeTransaction?.startChild({
196-
op: 'function.remix.document_request',
197-
origin: 'auto.function.remix',
198-
name: spanToJSON(activeTransaction).description,
195+
const name = rootSpan ? spanToJSON(rootSpan).description : undefined;
196+
197+
return startSpan(
198+
{
199+
// If we don't have a root span, `onlyIfParent` will lead to the span not being created anyhow
200+
// So we don't need to care too much about the fallback name, it's just for typing purposes....
201+
name: name || '<unknown>',
202+
onlyIfParent: true,
199203
attributes: {
200204
method: request.method,
201205
url: request.url,
206+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.remix',
207+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.remix.document_request',
202208
},
203-
});
204-
205-
res = await origDocumentRequestFunction.call(
206-
this,
207-
request,
208-
responseStatusCode,
209-
responseHeaders,
210-
context,
211-
loadContext,
212-
);
213-
214-
span?.end();
215-
} catch (err) {
216-
const isRemixV1 = !FUTURE_FLAGS?.v2_errorBoundary && remixVersion !== 2;
217-
218-
// This exists to capture the server-side rendering errors on Remix v1
219-
// On Remix v2, we capture SSR errors at `handleError`
220-
// We also skip primitives here, as we can't dedupe them, and also we don't expect any primitive SSR errors.
221-
if (isRemixV1 && !isPrimitive(err)) {
222-
await captureRemixServerException(err, 'documentRequest', request);
223-
}
224-
225-
throw err;
226-
}
227-
228-
return res;
209+
},
210+
() => {
211+
return handleCallbackErrors(
212+
() => {
213+
return origDocumentRequestFunction.call(
214+
this,
215+
request,
216+
responseStatusCode,
217+
responseHeaders,
218+
context,
219+
loadContext,
220+
);
221+
},
222+
err => {
223+
const isRemixV1 = !FUTURE_FLAGS?.v2_errorBoundary && remixVersion !== 2;
224+
225+
// This exists to capture the server-side rendering errors on Remix v1
226+
// On Remix v2, we capture SSR errors at `handleError`
227+
// We also skip primitives here, as we can't dedupe them, and also we don't expect any primitive SSR errors.
228+
if (isRemixV1 && !isPrimitive(err)) {
229+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
230+
captureRemixServerException(err, 'documentRequest', request);
231+
}
232+
},
233+
);
234+
},
235+
);
229236
};
230237
};
231238
}
@@ -242,47 +249,32 @@ function makeWrappedDataFunction(
242249
return origFn.call(this, args);
243250
}
244251

245-
let res: Response | AppData;
246-
// eslint-disable-next-line deprecation/deprecation
247-
const activeTransaction = getActiveTransaction();
248-
const currentScope = getCurrentScope();
249-
250-
try {
251-
// eslint-disable-next-line deprecation/deprecation
252-
const span = activeTransaction?.startChild({
252+
return startSpan(
253+
{
253254
op: `function.remix.${name}`,
254255
origin: 'auto.ui.remix',
255256
name: id,
256257
attributes: {
257258
name,
258259
},
259-
});
260-
261-
if (span) {
262-
// Assign data function to hub to be able to see `db` transactions (if any) as children.
263-
// eslint-disable-next-line deprecation/deprecation
264-
currentScope.setSpan(span);
265-
}
266-
267-
res = await origFn.call(this, args);
268-
269-
// eslint-disable-next-line deprecation/deprecation
270-
currentScope.setSpan(activeTransaction);
271-
span?.end();
272-
} catch (err) {
273-
const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2;
274-
275-
// On Remix v2, we capture all unexpected errors (except the `Route Error Response`s / Thrown Responses) in `handleError` function.
276-
// This is both for consistency and also avoid duplicates such as primitives like `string` or `number` being captured twice.
277-
// Remix v1 does not have a `handleError` function, so we capture all errors here.
278-
if (isRemixV2 ? isResponse(err) : true) {
279-
await captureRemixServerException(err, name, args.request);
280-
}
281-
282-
throw err;
283-
}
284-
285-
return res;
260+
},
261+
() => {
262+
return handleCallbackErrors(
263+
() => origFn.call(this, args),
264+
err => {
265+
const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2;
266+
267+
// On Remix v2, we capture all unexpected errors (except the `Route Error Response`s / Thrown Responses) in `handleError` function.
268+
// This is both for consistency and also avoid duplicates such as primitives like `string` or `number` being captured twice.
269+
// Remix v1 does not have a `handleError` function, so we capture all errors here.
270+
if (isRemixV2 ? isResponse(err) : true) {
271+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
272+
captureRemixServerException(err, name, args.request);
273+
}
274+
},
275+
);
276+
},
277+
);
286278
};
287279
}
288280

@@ -382,47 +374,44 @@ export function createRoutes(manifest: ServerRouteManifest, parentId?: string):
382374
}
383375

384376
/**
385-
* Starts a new transaction for the given request to be used by different `RequestHandler` wrappers.
377+
* Starts a new active span for the given request to be used by different `RequestHandler` wrappers.
386378
*/
387-
export function startRequestHandlerTransaction(
388-
hub: Hub,
389-
name: string,
390-
source: TransactionSource,
391-
request: {
392-
headers: {
393-
'sentry-trace': string;
394-
baggage: string;
395-
};
379+
export function startRequestHandlerSpan<T>(
380+
{
381+
name,
382+
source,
383+
sentryTrace,
384+
baggage,
385+
method,
386+
}: {
387+
name: string;
388+
source: TransactionSource;
389+
sentryTrace: string;
390+
baggage: string;
396391
method: string;
397392
},
398-
): Transaction {
399-
// eslint-disable-next-line deprecation/deprecation
400-
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
401-
request.headers['sentry-trace'],
402-
request.headers.baggage,
403-
);
404-
// eslint-disable-next-line deprecation/deprecation
405-
hub.getScope().setPropagationContext(propagationContext);
406-
407-
// TODO: Refactor this to `startSpan()`
408-
// eslint-disable-next-line deprecation/deprecation
409-
const transaction = hub.startTransaction({
410-
name,
411-
op: 'http.server',
412-
attributes: {
413-
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
414-
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
415-
method: request.method,
393+
callback: (span: Span) => T,
394+
): T {
395+
return continueTrace(
396+
{
397+
sentryTrace,
398+
baggage,
416399
},
417-
...traceparentData,
418-
metadata: {
419-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
400+
() => {
401+
return startSpan(
402+
{
403+
name,
404+
attributes: {
405+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.remix',
406+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
407+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
408+
method,
409+
},
410+
},
411+
callback,
412+
);
420413
},
421-
});
422-
423-
// eslint-disable-next-line deprecation/deprecation
424-
hub.getScope().setSpan(transaction);
425-
return transaction;
414+
);
426415
}
427416

428417
/**
@@ -445,8 +434,6 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
445434
}
446435

447436
return withIsolationScope(async isolationScope => {
448-
// eslint-disable-next-line deprecation/deprecation
449-
const hub = getCurrentHub();
450437
const options = getClient()?.getOptions();
451438

452439
let normalizedRequest: Record<string, unknown> = request;
@@ -473,23 +460,24 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui
473460
return origRequestHandler.call(this, request, loadContext);
474461
}
475462

476-
const transaction = startRequestHandlerTransaction(hub, name, source, {
477-
headers: {
478-
'sentry-trace': request.headers.get('sentry-trace') || '',
463+
return startRequestHandlerSpan(
464+
{
465+
name,
466+
source,
467+
sentryTrace: request.headers.get('sentry-trace') || '',
479468
baggage: request.headers.get('baggage') || '',
469+
method: request.method,
480470
},
481-
method: request.method,
482-
});
483-
484-
const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
471+
async span => {
472+
const res = (await origRequestHandler.call(this, request, loadContext)) as Response;
485473

486-
if (isResponse(res)) {
487-
setHttpStatus(transaction, res.status);
488-
}
489-
490-
transaction.end();
474+
if (isResponse(res)) {
475+
setHttpStatus(span, res.status);
476+
}
491477

492-
return res;
478+
return res;
479+
},
480+
);
493481
});
494482
};
495483
}

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

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { getClient, getCurrentHub, hasTracingEnabled, setHttpStatus, withIsolationScope } from '@sentry/core';
22
import { flush } from '@sentry/node-experimental';
3-
import type { Hub, Transaction } from '@sentry/types';
3+
import type { Hub, Span } from '@sentry/types';
44
import { extractRequestData, fill, isString, logger } from '@sentry/utils';
55

66
import { DEBUG_BUILD } from '../debug-build';
7-
import { createRoutes, getTransactionName, instrumentBuild, startRequestHandlerTransaction } from '../instrumentServer';
7+
import { createRoutes, getTransactionName, instrumentBuild, startRequestHandlerSpan } from '../instrumentServer';
88
import type {
99
AppLoadContext,
1010
ExpressCreateRequestHandler,
@@ -89,19 +89,24 @@ function startRequestHandlerTransactionWithRoutes(
8989
next: ExpressNextFunction,
9090
hub: Hub,
9191
url: URL,
92-
): Transaction | undefined {
92+
): unknown {
9393
const [name, source] = getTransactionName(routes, url);
94-
const transaction = startRequestHandlerTransaction(hub, name, source, {
95-
headers: {
96-
'sentry-trace': (req.headers && isString(req.headers['sentry-trace']) && req.headers['sentry-trace']) || '',
94+
95+
return startRequestHandlerSpan(
96+
{
97+
name,
98+
source,
99+
sentryTrace: (req.headers && isString(req.headers['sentry-trace']) && req.headers['sentry-trace']) || '',
97100
baggage: (req.headers && isString(req.headers.baggage) && req.headers.baggage) || '',
101+
method: req.method,
102+
},
103+
span => {
104+
// save a link to the transaction on the response, so that even if there's an error (landing us outside of
105+
// the domain), we can still finish it (albeit possibly missing some scope data)
106+
(res as AugmentedExpressResponse).__sentrySpan = span;
107+
return origRequestHandler.call(this, req, res, next);
98108
},
99-
method: req.method,
100-
});
101-
// save a link to the transaction on the response, so that even if there's an error (landing us outside of
102-
// the domain), we can still finish it (albeit possibly missing some scope data)
103-
(res as AugmentedExpressResponse).__sentryTransaction = transaction;
104-
return origRequestHandler.call(this, req, res, next);
109+
);
105110
}
106111

107112
function wrapGetLoadContext(origGetLoadContext: () => AppLoadContext): GetLoadContextFunction {
@@ -168,7 +173,7 @@ export function wrapExpressCreateRequestHandler(
168173
}
169174

170175
export type AugmentedExpressResponse = ExpressResponse & {
171-
__sentryTransaction?: Transaction;
176+
__sentrySpan?: Span;
172177
};
173178

174179
type ResponseEndMethod = AugmentedExpressResponse['end'];
@@ -201,16 +206,16 @@ function wrapEndMethod(origEnd: ResponseEndMethod): WrappedResponseEndMethod {
201206
* @param res The outgoing response for this request, on which the transaction is stored
202207
*/
203208
async function finishSentryProcessing(res: AugmentedExpressResponse): Promise<void> {
204-
const { __sentryTransaction: transaction } = res;
209+
const { __sentrySpan: span } = res;
205210

206-
if (transaction) {
207-
setHttpStatus(transaction, res.statusCode);
211+
if (span) {
212+
setHttpStatus(span, res.statusCode);
208213

209214
// Push `transaction.finish` to the next event loop so open spans have a better chance of finishing before the
210215
// transaction closes, and make sure to wait until that's done before flushing events
211216
await new Promise<void>(resolve => {
212217
setImmediate(() => {
213-
transaction.end();
218+
span.end();
214219
resolve();
215220
});
216221
});

0 commit comments

Comments
 (0)