Skip to content

Commit ea74266

Browse files
authored
ref: Remove startTransaction call from withEdgeWrapping (#9984)
1 parent 152e901 commit ea74266

File tree

4 files changed

+81
-133
lines changed

4 files changed

+81
-133
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
1-
import { addTracingExtensions, captureException, getCurrentScope, startTransaction } from '@sentry/core';
2-
import type { Span } from '@sentry/types';
3-
import {
4-
addExceptionMechanism,
5-
logger,
6-
objectify,
7-
tracingContextFromHeaders,
8-
winterCGRequestToRequestData,
9-
} from '@sentry/utils';
1+
import { addTracingExtensions, captureException, continueTrace, trace } from '@sentry/core';
2+
import { winterCGRequestToRequestData } from '@sentry/utils';
103

114
import type { EdgeRouteHandler } from '../../edge/types';
12-
import { DEBUG_BUILD } from '../debug-build';
135
import { flushQueue } from './responseEnd';
146

157
/**
@@ -21,84 +13,56 @@ export function withEdgeWrapping<H extends EdgeRouteHandler>(
2113
): (...params: Parameters<H>) => Promise<ReturnType<H>> {
2214
return async function (this: unknown, ...args) {
2315
addTracingExtensions();
16+
const req: unknown = args[0];
2417

25-
const req = args[0];
26-
const currentScope = getCurrentScope();
27-
const prevSpan = currentScope.getSpan();
18+
let sentryTrace;
19+
let baggage;
2820

29-
let span: Span | undefined;
21+
if (req instanceof Request) {
22+
sentryTrace = req.headers.get('sentry-trace') || '';
23+
baggage = req.headers.get('baggage');
24+
}
3025

31-
if (prevSpan) {
32-
span = prevSpan.startChild({
33-
description: options.spanDescription,
34-
op: options.spanOp,
35-
origin: 'auto.function.nextjs',
36-
});
37-
} else if (req instanceof Request) {
38-
const sentryTrace = req.headers.get('sentry-trace') || '';
39-
const baggage = req.headers.get('baggage');
40-
const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
41-
sentryTrace,
42-
baggage,
43-
);
44-
currentScope.setPropagationContext(propagationContext);
45-
if (traceparentData) {
46-
DEBUG_BUILD && logger.log(`[Tracing] Continuing trace ${traceparentData.traceId}.`);
47-
}
26+
const transactionContext = continueTrace({
27+
sentryTrace,
28+
baggage,
29+
});
4830

49-
span = startTransaction({
31+
return trace(
32+
{
33+
...transactionContext,
5034
name: options.spanDescription,
5135
op: options.spanOp,
52-
origin: 'auto.ui.nextjs.withEdgeWrapping',
53-
...traceparentData,
36+
origin: 'auto.function.nextjs.withEdgeWrapping',
5437
metadata: {
55-
request: winterCGRequestToRequestData(req),
56-
dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext,
38+
...transactionContext.metadata,
39+
request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined,
5740
source: 'route',
5841
},
59-
});
60-
}
61-
62-
currentScope?.setSpan(span);
63-
64-
try {
65-
const handlerResult: ReturnType<H> = await handler.apply(this, args);
42+
},
43+
async span => {
44+
const handlerResult = await handler.apply(this, args);
6645

67-
if ((handlerResult as unknown) instanceof Response) {
68-
span?.setHttpStatus(handlerResult.status);
69-
} else {
70-
span?.setStatus('ok');
71-
}
46+
if (handlerResult instanceof Response) {
47+
span?.setHttpStatus(handlerResult.status);
48+
} else {
49+
span?.setStatus('ok');
50+
}
7251

73-
return handlerResult;
74-
} catch (e) {
75-
// In case we have a primitive, wrap it in the equivalent wrapper class (string -> String, etc.) so that we can
76-
// store a seen flag on it.
77-
const objectifiedErr = objectify(e);
78-
79-
span?.setStatus('internal_error');
80-
81-
captureException(objectifiedErr, scope => {
82-
scope.setSpan(span);
83-
scope.addEventProcessor(event => {
84-
addExceptionMechanism(event, {
52+
return handlerResult;
53+
},
54+
(err, span) => {
55+
span?.setStatus('internal_error');
56+
captureException(err, {
57+
mechanism: {
8558
type: 'instrument',
8659
handled: false,
8760
data: {
8861
function: options.mechanismFunctionName,
8962
},
90-
});
91-
return event;
63+
},
9264
});
93-
94-
return scope;
95-
});
96-
97-
throw objectifiedErr;
98-
} finally {
99-
span?.end();
100-
currentScope?.setSpan(prevSpan);
101-
await flushQueue();
102-
}
65+
},
66+
).finally(() => flushQueue());
10367
};
10468
}

packages/nextjs/test/edge/edgeWrapperUtils.test.ts

+11-15
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
import * as coreSdk from '@sentry/core';
2-
import { addTracingExtensions } from '@sentry/core';
32

43
import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils';
54

6-
// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
7-
// constructor but the client isn't used in these tests.
8-
addTracingExtensions();
9-
105
// @ts-expect-error Request does not exist on type Global
116
const origRequest = global.Request;
127
// @ts-expect-error Response does not exist on type Global
@@ -33,8 +28,6 @@ afterAll(() => {
3328

3429
beforeEach(() => {
3530
jest.clearAllMocks();
36-
jest.resetAllMocks();
37-
jest.spyOn(coreSdk, 'hasTracingEnabled').mockImplementation(() => true);
3831
});
3932

4033
describe('withEdgeWrapping', () => {
@@ -71,27 +64,30 @@ describe('withEdgeWrapping', () => {
7164
expect(captureExceptionSpy).toHaveBeenCalledTimes(1);
7265
});
7366

74-
it('should return a function that starts a transaction when a request object is passed', async () => {
75-
const startTransactionSpy = jest.spyOn(coreSdk, 'startTransaction');
67+
it('should return a function that calls trace', async () => {
68+
const traceSpy = jest.spyOn(coreSdk, 'trace');
7669

77-
const origFunctionReturnValue = new Response();
78-
const origFunction = jest.fn(_req => origFunctionReturnValue);
70+
const request = new Request('https://sentry.io/');
71+
const origFunction = jest.fn(_req => new Response());
7972

8073
const wrappedFunction = withEdgeWrapping(origFunction, {
8174
spanDescription: 'some label',
8275
mechanismFunctionName: 'some name',
8376
spanOp: 'some op',
8477
});
8578

86-
const request = new Request('https://sentry.io/');
8779
await wrappedFunction(request);
88-
expect(startTransactionSpy).toHaveBeenCalledTimes(1);
89-
expect(startTransactionSpy).toHaveBeenCalledWith(
80+
81+
expect(traceSpy).toHaveBeenCalledTimes(1);
82+
expect(traceSpy).toHaveBeenCalledWith(
9083
expect.objectContaining({
91-
metadata: expect.objectContaining({ source: 'route' }),
84+
metadata: { request: { headers: {} }, source: 'route' },
9285
name: 'some label',
9386
op: 'some op',
87+
origin: 'auto.function.nextjs.withEdgeWrapping',
9488
}),
89+
expect.any(Function),
90+
expect.any(Function),
9591
);
9692
});
9793

packages/nextjs/test/edge/withSentryAPI.test.ts

+33-46
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,30 @@ import * as coreSdk from '@sentry/core';
22

33
import { wrapApiHandlerWithSentry } from '../../src/edge';
44

5-
// The wrap* functions require the hub to have tracing extensions. This is normally called by the EdgeClient
6-
// constructor but the client isn't used in these tests.
7-
coreSdk.addTracingExtensions();
8-
95
// @ts-expect-error Request does not exist on type Global
106
const origRequest = global.Request;
117
// @ts-expect-error Response does not exist on type Global
128
const origResponse = global.Response;
139

1410
// @ts-expect-error Request does not exist on type Global
1511
global.Request = class Request {
16-
headers = {
12+
public url: string;
13+
14+
public headers = {
1715
get() {
1816
return null;
1917
},
2018
};
2119

22-
method = 'POST';
20+
public method = 'POST';
21+
22+
public constructor(input: string) {
23+
this.url = input;
24+
}
2325
};
2426

2527
// @ts-expect-error Response does not exist on type Global
26-
global.Response = class Request {};
28+
global.Response = class Response {};
2729

2830
afterAll(() => {
2931
// @ts-expect-error Request does not exist on type Global
@@ -32,66 +34,51 @@ afterAll(() => {
3234
global.Response = origResponse;
3335
});
3436

35-
beforeEach(() => {
36-
jest.resetAllMocks();
37-
jest.restoreAllMocks();
38-
jest.spyOn(coreSdk, 'hasTracingEnabled').mockImplementation(() => true);
37+
const traceSpy = jest.spyOn(coreSdk, 'trace');
38+
39+
afterEach(() => {
40+
jest.clearAllMocks();
3941
});
4042

4143
describe('wrapApiHandlerWithSentry', () => {
42-
it('should return a function that starts a transaction with the correct name when there is no active transaction and a request is being passed', async () => {
43-
const startTransactionSpy = jest.spyOn(coreSdk, 'startTransaction');
44-
45-
const origFunctionReturnValue = new Response();
46-
const origFunction = jest.fn(_req => origFunctionReturnValue);
44+
it('should return a function that calls trace', async () => {
45+
const request = new Request('https://sentry.io/');
46+
const origFunction = jest.fn(_req => new Response());
4747

4848
const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]');
4949

50-
const request = new Request('https://sentry.io/');
5150
await wrappedFunction(request);
52-
expect(startTransactionSpy).toHaveBeenCalledTimes(1);
53-
expect(startTransactionSpy).toHaveBeenCalledWith(
51+
52+
expect(traceSpy).toHaveBeenCalledTimes(1);
53+
expect(traceSpy).toHaveBeenCalledWith(
5454
expect.objectContaining({
55-
metadata: expect.objectContaining({ source: 'route' }),
55+
metadata: { request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, source: 'route' },
5656
name: 'POST /user/[userId]/post/[postId]',
5757
op: 'http.server',
58+
origin: 'auto.function.nextjs.withEdgeWrapping',
5859
}),
60+
expect.any(Function),
61+
expect.any(Function),
5962
);
6063
});
6164

62-
it('should return a function that should not start a transaction when there is no active span and no request is being passed', async () => {
63-
const startTransactionSpy = jest.spyOn(coreSdk, 'startTransaction');
64-
65-
const origFunctionReturnValue = new Response();
66-
const origFunction = jest.fn(() => origFunctionReturnValue);
65+
it('should return a function that calls trace without throwing when no request is passed', async () => {
66+
const origFunction = jest.fn(() => new Response());
6767

6868
const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]');
6969

7070
await wrappedFunction();
71-
expect(startTransactionSpy).not.toHaveBeenCalled();
72-
});
73-
74-
it('should return a function that starts a span on the current transaction with the correct description when there is an active transaction and no request is being passed', async () => {
75-
const testTransaction = coreSdk.startTransaction({ name: 'testTransaction' });
76-
coreSdk.getCurrentHub().getScope().setSpan(testTransaction);
77-
78-
const startChildSpy = jest.spyOn(testTransaction, 'startChild');
7971

80-
const origFunctionReturnValue = new Response();
81-
const origFunction = jest.fn(() => origFunctionReturnValue);
82-
83-
const wrappedFunction = wrapApiHandlerWithSentry(origFunction, '/user/[userId]/post/[postId]');
84-
85-
await wrappedFunction();
86-
expect(startChildSpy).toHaveBeenCalledTimes(1);
87-
expect(startChildSpy).toHaveBeenCalledWith(
72+
expect(traceSpy).toHaveBeenCalledTimes(1);
73+
expect(traceSpy).toHaveBeenCalledWith(
8874
expect.objectContaining({
89-
description: 'handler (/user/[userId]/post/[postId])',
90-
op: 'function',
75+
metadata: { source: 'route' },
76+
name: 'handler (/user/[userId]/post/[postId])',
77+
op: 'http.server',
78+
origin: 'auto.function.nextjs.withEdgeWrapping',
9179
}),
80+
expect.any(Function),
81+
expect.any(Function),
9282
);
93-
94-
testTransaction.end();
95-
coreSdk.getCurrentHub().getScope().setSpan(undefined);
9683
});
9784
});

packages/utils/src/requestdata.ts

+1
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,7 @@ function extractQueryParams(
374374
* Transforms a `Headers` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into a simple key-value dict.
375375
* The header keys will be lower case: e.g. A "Content-Type" header will be stored as "content-type".
376376
*/
377+
// TODO(v8): Make this function return undefined when the extraction fails.
377378
export function winterCGHeadersToDict(winterCGHeaders: WebFetchHeaders): Record<string, string> {
378379
const headers: Record<string, string> = {};
379380
try {

0 commit comments

Comments
 (0)