Skip to content

Commit ff416ae

Browse files
author
Luca Forstner
authored
feat(vercel-edge): Add fetch instrumentation (#9504)
1 parent 1e2bf6e commit ff416ae

File tree

13 files changed

+425
-8
lines changed

13 files changed

+425
-8
lines changed

packages/e2e-tests/test-applications/nextjs-app-dir/middleware.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
import { NextResponse } from 'next/server';
22
import type { NextRequest } from 'next/server';
33

4-
export function middleware(request: NextRequest) {
4+
export async function middleware(request: NextRequest) {
55
if (request.headers.has('x-should-throw')) {
66
throw new Error('Middleware Error');
77
}
88

9+
if (request.headers.has('x-should-make-request')) {
10+
await fetch('http://localhost:3030/');
11+
}
12+
913
return NextResponse.next();
1014
}
1115

packages/e2e-tests/test-applications/nextjs-app-dir/tests/middleware.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,46 @@ test('Records exceptions happening in middleware', async ({ request }) => {
4545

4646
expect(await errorEventPromise).toBeDefined();
4747
});
48+
49+
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
50+
const middlewareTransactionPromise = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
51+
return (
52+
transactionEvent?.transaction === 'middleware' &&
53+
!!transactionEvent.spans?.find(span => span.op === 'http.client')
54+
);
55+
});
56+
57+
request.get('/api/endpoint-behind-middleware', { headers: { 'x-should-make-request': '1' } }).catch(() => {
58+
// Noop
59+
});
60+
61+
const middlewareTransaction = await middlewareTransactionPromise;
62+
63+
expect(middlewareTransaction.spans).toEqual(
64+
expect.arrayContaining([
65+
{
66+
data: { 'http.method': 'GET', 'http.response.status_code': 200, type: 'fetch', url: 'http://localhost:3030/' },
67+
description: 'GET http://localhost:3030/',
68+
op: 'http.client',
69+
origin: 'auto.http.wintercg_fetch',
70+
parent_span_id: expect.any(String),
71+
span_id: expect.any(String),
72+
start_timestamp: expect.any(Number),
73+
status: 'ok',
74+
tags: { 'http.status_code': '200' },
75+
timestamp: expect.any(Number),
76+
trace_id: expect.any(String),
77+
},
78+
]),
79+
);
80+
expect(middlewareTransaction.breadcrumbs).toEqual(
81+
expect.arrayContaining([
82+
{
83+
category: 'fetch',
84+
data: { __span: expect.any(String), method: 'GET', status_code: 200, url: 'http://localhost:3030/' },
85+
timestamp: expect.any(Number),
86+
type: 'http',
87+
},
88+
]),
89+
);
90+
});

packages/nextjs/src/edge/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type EdgeOptions = VercelEdgeOptions;
1111

1212
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
1313
__rewriteFramesDistDir__?: string;
14+
fetch: (...args: unknown[]) => unknown;
1415
};
1516

1617
/** Inits the Sentry NextJS SDK on the Edge Runtime. */

packages/node/src/integrations/undici/index.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -272,16 +272,17 @@ function setHeadersOnRequest(
272272
sentryTrace: string,
273273
sentryBaggageHeader: string | undefined,
274274
): void {
275-
if (request.__sentry_has_headers__) {
275+
const headerLines = request.headers.split('\r\n');
276+
const hasSentryHeaders = headerLines.some(headerLine => headerLine.startsWith('sentry-trace:'));
277+
278+
if (hasSentryHeaders) {
276279
return;
277280
}
278281

279282
request.addHeader('sentry-trace', sentryTrace);
280283
if (sentryBaggageHeader) {
281284
request.addHeader('baggage', sentryBaggageHeader);
282285
}
283-
284-
request.__sentry_has_headers__ = true;
285286
}
286287

287288
function createRequestSpan(

packages/node/src/integrations/undici/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,6 @@ export interface UndiciResponse {
236236

237237
export interface RequestWithSentry extends UndiciRequest {
238238
__sentry_span__?: Span;
239-
__sentry_has_headers__?: boolean;
240239
}
241240

242241
export interface RequestCreateMessage {

packages/tracing-internal/src/node/integrations/express.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/
33
import {
44
extractPathForTransaction,
55
getNumberOfUrlSegments,
6+
GLOBAL_OBJ,
67
isRegExp,
78
logger,
89
stripUrlQueryAndFragment,
@@ -485,7 +486,8 @@ function getLayerRoutePathInfo(layer: Layer): LayerRoutePathInfo {
485486

486487
if (!lrp) {
487488
// parse node.js major version
488-
const [major] = process.versions.node.split('.').map(Number);
489+
// Next.js will complain if we directly use `proces.versions` here because of edge runtime.
490+
const [major] = (GLOBAL_OBJ as unknown as NodeJS.Global).process.versions.node.split('.').map(Number);
489491

490492
// allow call extractOriginalRoute only if node version support Regex d flag, node 16+
491493
if (major >= 16) {

packages/types/src/instrument.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface SentryFetchData {
3737

3838
export interface HandlerDataFetch {
3939
args: any[];
40-
fetchData: SentryFetchData;
40+
fetchData: SentryFetchData; // This data is among other things dumped directly onto the fetch breadcrumb data
4141
startTimestamp: number;
4242
endTimestamp?: number;
4343
// This is actually `Response` - Note: this type is not complete. Add to it if necessary.

packages/utils/src/supports.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { getGlobalObject } from './worldwide';
44
// eslint-disable-next-line deprecation/deprecation
55
const WINDOW = getGlobalObject<Window>();
66

7+
declare const EdgeRuntime: string | undefined;
8+
79
export { supportsHistory } from './vendor/supportsHistory';
810

911
/**
@@ -89,6 +91,10 @@ export function isNativeFetch(func: Function): boolean {
8991
* @returns true if `window.fetch` is natively implemented, false otherwise
9092
*/
9193
export function supportsNativeFetch(): boolean {
94+
if (typeof EdgeRuntime === 'string') {
95+
return true;
96+
}
97+
9298
if (!supportsFetch()) {
9399
return false;
94100
}

packages/vercel-edge/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@
2525
"dependencies": {
2626
"@sentry/core": "7.80.1",
2727
"@sentry/types": "7.80.1",
28-
"@sentry/utils": "7.80.1"
28+
"@sentry/utils": "7.80.1",
29+
"@sentry-internal/tracing": "7.80.1"
2930
},
3031
"devDependencies": {
3132
"@edge-runtime/jest-environment": "2.2.3",

packages/vercel-edge/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,11 @@ export { defaultIntegrations, init } from './sdk';
7070

7171
import { Integrations as CoreIntegrations } from '@sentry/core';
7272

73+
import { WinterCGFetch } from './integrations/wintercg-fetch';
74+
7375
const INTEGRATIONS = {
7476
...CoreIntegrations,
77+
...WinterCGFetch,
7578
};
7679

7780
export { INTEGRATIONS as Integrations };

0 commit comments

Comments
 (0)