Skip to content

Commit 0d5c32d

Browse files
committed
Merge branch 'develop' into fn/headers-unify
2 parents 7d5865e + 2435b87 commit 0d5c32d

File tree

18 files changed

+126
-344
lines changed

18 files changed

+126
-344
lines changed

dev-packages/e2e-tests/test-applications/nextjs-turbo/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"extends": "../../package.json"
4747
},
4848
"sentryTest": {
49+
"optional": true,
4950
"optionalVariants": [
5051
{
5152
"build-command": "test:build-canary",

packages/core/src/utils-hoist/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export {
7373
extractRequestData,
7474
winterCGHeadersToDict,
7575
winterCGRequestToRequestData,
76+
httpRequestToRequestData,
7677
extractQueryParamsFromUrl,
7778
headersToDict,
7879
} from './requestdata';

packages/core/src/utils-hoist/object.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ export function fill(source: { [key: string]: any }, name: string, replacementFa
3232
markFunctionWrapped(wrapped, original);
3333
}
3434

35-
source[name] = wrapped;
35+
try {
36+
source[name] = wrapped;
37+
} catch {
38+
DEBUG_BUILD && logger.log(`Failed to replace method "${name}" in object`, source);
39+
}
3640
}
3741

3842
/**

packages/core/src/utils-hoist/requestdata.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { DEBUG_BUILD } from './debug-build';
1414
import { isPlainObject, isString } from './is';
1515
import { logger } from './logger';
1616
import { normalize } from './normalize';
17+
import { dropUndefinedKeys } from './object';
1718
import { truncate } from './string';
1819
import { stripUrlQueryAndFragment } from './url';
1920
import { getClientIPAddress, ipHeaderNames } from './vendor/getIpAddress';
@@ -451,6 +452,43 @@ export function winterCGRequestToRequestData(req: WebFetchRequest): RequestEvent
451452
};
452453
}
453454

455+
/**
456+
* Convert a HTTP request object to RequestEventData to be passed as normalizedRequest.
457+
* Instead of allowing `PolymorphicRequest` to be passed,
458+
* we want to be more specific and generally require a http.IncomingMessage-like object.
459+
*/
460+
export function httpRequestToRequestData(request: {
461+
method?: string;
462+
url?: string;
463+
headers?: {
464+
[key: string]: string | string[] | undefined;
465+
};
466+
protocol?: string;
467+
socket?: unknown;
468+
}): RequestEventData {
469+
const headers = request.headers || {};
470+
const host = headers.host || '<no host>';
471+
const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http';
472+
const originalUrl = request.url || '';
473+
const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;
474+
475+
// This is non-standard, but may be sometimes set
476+
// It may be overwritten later by our own body handling
477+
const data = (request as PolymorphicRequest).body || undefined;
478+
479+
// This is non-standard, but may be set on e.g. Next.js or Express requests
480+
const cookies = (request as PolymorphicRequest).cookies;
481+
482+
return dropUndefinedKeys({
483+
url: absoluteUrl,
484+
method: request.method,
485+
query_string: extractQueryParamsFromUrl(originalUrl),
486+
headers: headersToDict(headers),
487+
cookies,
488+
data,
489+
});
490+
}
491+
454492
/** Extract the query params from an URL. */
455493
export function extractQueryParamsFromUrl(url: string): string | undefined {
456494
// url is path and query string

packages/core/test/utils-hoist/object.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,29 @@ describe('fill()', () => {
3131
expect(replacement).toBeCalled();
3232
});
3333

34+
test('does not throw on readonly properties', () => {
35+
const originalFn = () => 41;
36+
const source = {
37+
get prop() {
38+
return originalFn;
39+
},
40+
set prop(_fn: () => number) {
41+
throw new Error('OH NO, this is not writeable...');
42+
},
43+
};
44+
45+
expect(source.prop()).toEqual(41);
46+
47+
const replacement = jest.fn().mockImplementation(() => {
48+
return () => 42;
49+
});
50+
fill(source, 'prop', replacement);
51+
expect(replacement).toBeCalled();
52+
53+
expect(source.prop).toBe(originalFn);
54+
expect(source.prop()).toEqual(41);
55+
});
56+
3457
test('can do anything inside replacement function', () => {
3558
const source = {
3659
foo: (): number => 42,

packages/google-cloud-serverless/src/gcpfunction/http.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ import {
22
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
33
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
44
handleCallbackErrors,
5+
httpRequestToRequestData,
6+
isString,
7+
logger,
58
setHttpStatus,
9+
stripUrlQueryAndFragment,
610
} from '@sentry/core';
7-
import { isString, logger, stripUrlQueryAndFragment } from '@sentry/core';
811
import { captureException, continueTrace, flush, getCurrentScope, startSpanManual } from '@sentry/node';
9-
1012
import { DEBUG_BUILD } from '../debug-build';
1113
import { domainify, markEventUnhandled, proxyFunction } from '../utils';
1214
import type { HttpFunction, WrapperOptions } from './general';
@@ -44,6 +46,9 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial<WrapperOptions>):
4446
const baggage = req.headers?.baggage;
4547

4648
return continueTrace({ sentryTrace, baggage }, () => {
49+
const normalizedRequest = httpRequestToRequestData(req);
50+
getCurrentScope().setSDKProcessingMetadata({ normalizedRequest });
51+
4752
return startSpanManual(
4853
{
4954
name: `${reqMethod} ${reqUrl}`,
@@ -54,10 +59,6 @@ function _wrapHttpFunction(fn: HttpFunction, options: Partial<WrapperOptions>):
5459
},
5560
},
5661
span => {
57-
getCurrentScope().setSDKProcessingMetadata({
58-
request: req,
59-
});
60-
6162
// eslint-disable-next-line @typescript-eslint/unbound-method
6263
const _end = res.end;
6364
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/google-cloud-serverless/test/gcpfunction/http.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,12 @@ describe('GCPFunction', () => {
176176
expect(defaultIntegrations).toContain('RequestData');
177177

178178
expect(mockScope.setSDKProcessingMetadata).toHaveBeenCalledWith({
179-
request: {
179+
normalizedRequest: {
180180
method: 'POST',
181-
url: '/path?q=query',
181+
url: 'http://hostname/path?q=query',
182182
headers: { host: 'hostname', 'content-type': 'application/json' },
183-
body: { foo: 'bar' },
183+
query_string: 'q=query',
184+
data: { foo: 'bar' },
184185
},
185186
});
186187
});

packages/nextjs/src/common/nextNavigationErrorUtils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ import { isError } from '@sentry/core';
55
* https://beta.nextjs.org/docs/api-reference/notfound#notfound
66
*/
77
export function isNotFoundNavigationError(subject: unknown): boolean {
8-
return isError(subject) && (subject as Error & { digest?: unknown }).digest === 'NEXT_NOT_FOUND';
8+
return (
9+
isError(subject) &&
10+
['NEXT_NOT_FOUND', 'NEXT_HTTP_ERROR_FALLBACK;404'].includes(
11+
(subject as Error & { digest?: unknown }).digest as string,
12+
)
13+
);
914
}
1015

1116
/**

packages/nextjs/src/common/pages-router-instrumentation/_error.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { captureException, withScope } from '@sentry/core';
1+
import { captureException, httpRequestToRequestData, withScope } from '@sentry/core';
22
import { vercelWaitUntil } from '@sentry/core';
33
import type { NextPageContext } from 'next';
44
import { flushSafelyWithTimeout } from '../utils/responseEnd';
@@ -38,7 +38,8 @@ export async function captureUnderscoreErrorException(contextOrProps: ContextOrP
3838

3939
withScope(scope => {
4040
if (req) {
41-
scope.setSDKProcessingMetadata({ request: req });
41+
const normalizedRequest = httpRequestToRequestData(req);
42+
scope.setSDKProcessingMetadata({ normalizedRequest });
4243
}
4344

4445
// If third-party libraries (or users themselves) throw something falsy, we want to capture it as a message (which

packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
44
captureException,
55
continueTrace,
6+
httpRequestToRequestData,
67
setHttpStatus,
78
startSpanManual,
89
withIsolationScope,
@@ -65,8 +66,9 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
6566
},
6667
() => {
6768
const reqMethod = `${(req.method || 'GET').toUpperCase()} `;
69+
const normalizedRequest = httpRequestToRequestData(req);
6870

69-
isolationScope.setSDKProcessingMetadata({ request: req });
71+
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
7072
isolationScope.setTransactionName(`${reqMethod}${parameterizedRoute}`);
7173

7274
return startSpanManual(

0 commit comments

Comments
 (0)