Skip to content

Commit 2435b87

Browse files
authored
ref: Refactor remaining usage of request to normalizedRequest (#14401)
This includes exporting `httpRequestToRequestEventData` from `@sentry/node`, which we can reuse in a few places. This also allows us to remove a bunch of stuff from remix, because actually we can just use `winterCGRequestToRequestData` instead of the custom implementation we have there. at least type wise, this should all work, let's see if any test complains... Closes #14298
1 parent 0c0f8c6 commit 2435b87

File tree

13 files changed

+88
-339
lines changed

13 files changed

+88
-339
lines changed

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

+1
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/requestdata.ts

+38
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/google-cloud-serverless/src/gcpfunction/http.ts

+7-6
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

+4-3
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/pages-router-instrumentation/_error.ts

+3-2
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

+3-1
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(

packages/nextjs/src/common/utils/wrapperUtils.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getIsolationScope,
77
getRootSpan,
88
getTraceData,
9+
httpRequestToRequestData,
910
} from '@sentry/core';
1011
import { TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL } from '../span-attributes-with-logic-attached';
1112

@@ -61,10 +62,9 @@ export function withTracedServerSideDataFetcher<F extends (...args: any[]) => Pr
6162
this: unknown,
6263
...args: Parameters<F>
6364
): Promise<{ data: ReturnType<F>; sentryTrace?: string; baggage?: string }> {
65+
const normalizedRequest = httpRequestToRequestData(req);
6466
getCurrentScope().setTransactionName(`${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`);
65-
getIsolationScope().setSDKProcessingMetadata({
66-
request: req,
67-
});
67+
getIsolationScope().setSDKProcessingMetadata({ normalizedRequest });
6868

6969
const span = getActiveSpan();
7070

packages/nextjs/src/common/wrapMiddlewareWithSentry.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import {
88
handleCallbackErrors,
99
setCapturedScopesOnSpan,
1010
startSpan,
11+
vercelWaitUntil,
12+
winterCGRequestToRequestData,
1113
withIsolationScope,
1214
} from '@sentry/core';
13-
import { vercelWaitUntil, winterCGRequestToRequestData } from '@sentry/core';
1415
import type { TransactionSource } from '@sentry/types';
1516
import type { EdgeRouteHandler } from '../edge/types';
1617
import { flushSafelyWithTimeout } from './utils/responseEnd';

packages/node/src/integrations/http/SentryHttpInstrumentation.ts

+8-22
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import { VERSION } from '@opentelemetry/core';
55
import type { InstrumentationConfig } from '@opentelemetry/instrumentation';
66
import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation';
77
import { getRequestInfo } from '@opentelemetry/instrumentation-http';
8-
import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core';
98
import {
10-
extractQueryParamsFromUrl,
9+
addBreadcrumb,
1110
getBreadcrumbLogLevelFromHttpStatusCode,
11+
getClient,
12+
getIsolationScope,
1213
getSanitizedUrlString,
13-
headersToDict,
14+
httpRequestToRequestData,
1415
logger,
1516
parseUrl,
1617
stripUrlQueryAndFragment,
18+
withIsolationScope,
1719
} from '@sentry/core';
18-
import type { PolymorphicRequest, RequestEventData, SanitizedRequestData, Scope } from '@sentry/types';
20+
import type { RequestEventData, SanitizedRequestData, Scope } from '@sentry/types';
1921
import { DEBUG_BUILD } from '../../debug-build';
2022
import type { NodeClient } from '../../sdk/client';
2123
import { getRequestUrl } from '../../utils/getRequestUrl';
@@ -131,26 +133,10 @@ export class SentryHttpInstrumentation extends InstrumentationBase<SentryHttpIns
131133

132134
instrumentation._diag.debug('http instrumentation for incoming request');
133135

134-
const request = args[0] as http.IncomingMessage;
135-
136136
const isolationScope = getIsolationScope().clone();
137+
const request = args[0] as http.IncomingMessage;
137138

138-
const headers = request.headers;
139-
const host = headers.host || '<no host>';
140-
const protocol = request.socket && (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http';
141-
const originalUrl = request.url || '';
142-
const absoluteUrl = originalUrl.startsWith(protocol) ? originalUrl : `${protocol}://${host}${originalUrl}`;
143-
144-
// This is non-standard, but may be set on e.g. Next.js or Express requests
145-
const cookies = (request as PolymorphicRequest).cookies;
146-
147-
const normalizedRequest: RequestEventData = {
148-
url: absoluteUrl,
149-
method: request.method,
150-
query_string: extractQueryParamsFromUrl(request.url || ''),
151-
headers: headersToDict(request.headers),
152-
cookies,
153-
};
139+
const normalizedRequest = httpRequestToRequestData(request);
154140

155141
patchRequestToCaptureBody(request, isolationScope);
156142

packages/remix/src/utils/errors.ts

+14-13
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import type { AppData, DataFunctionArgs, EntryContext, HandleDocumentRequestFunction } from '@remix-run/node';
2-
import { captureException, getClient, handleCallbackErrors } from '@sentry/core';
3-
import { addExceptionMechanism, isPrimitive, logger, objectify } from '@sentry/core';
4-
import type { Span } from '@sentry/types';
2+
import {
3+
addExceptionMechanism,
4+
captureException,
5+
getClient,
6+
handleCallbackErrors,
7+
isPrimitive,
8+
logger,
9+
objectify,
10+
winterCGRequestToRequestData,
11+
} from '@sentry/core';
12+
import type { RequestEventData, Span } from '@sentry/types';
513
import { DEBUG_BUILD } from './debug-build';
614
import type { RemixOptions } from './remixOptions';
715
import { storeFormDataKeys } from './utils';
816
import { extractData, isResponse, isRouteErrorResponse } from './vendor/response';
917
import type { DataFunction, RemixRequest } from './vendor/types';
10-
import { normalizeRemixRequest } from './web-fetch';
1118

1219
/**
1320
* Captures an exception happened in the Remix server.
@@ -41,24 +48,18 @@ export async function captureRemixServerException(
4148
return;
4249
}
4350

44-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45-
let normalizedRequest: Record<string, unknown> = request as unknown as any;
51+
let normalizedRequest: RequestEventData = {};
4652

4753
try {
48-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
49-
normalizedRequest = normalizeRemixRequest(request as unknown as any);
54+
normalizedRequest = winterCGRequestToRequestData(request);
5055
} catch (e) {
5156
DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
5257
}
5358

5459
const objectifiedErr = objectify(err);
5560

5661
captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => {
57-
scope.setSDKProcessingMetadata({
58-
request: {
59-
...normalizedRequest,
60-
},
61-
});
62+
scope.setSDKProcessingMetadata({ normalizedRequest });
6263

6364
scope.addEventProcessor(event => {
6465
addExceptionMechanism(event, {

packages/remix/src/utils/instrumentServer.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import {
1111
spanToJSON,
1212
spanToTraceHeader,
1313
startSpan,
14+
winterCGRequestToRequestData,
1415
withIsolationScope,
1516
} from '@sentry/core';
1617
import { dynamicSamplingContextToSentryBaggageHeader, fill, isNodeEnv, loadModule, logger } from '@sentry/core';
1718
import { continueTrace, getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry';
18-
import type { TransactionSource, WrappedFunction } from '@sentry/types';
19+
import type { RequestEventData, TransactionSource, WrappedFunction } from '@sentry/types';
1920
import type { Span } from '@sentry/types';
2021

2122
import { DEBUG_BUILD } from './debug-build';
@@ -39,7 +40,6 @@ import type {
3940
ServerRoute,
4041
ServerRouteManifest,
4142
} from './vendor/types';
42-
import { normalizeRemixRequest } from './web-fetch';
4343

4444
let FUTURE_FLAGS: FutureConfig | undefined;
4545

@@ -296,10 +296,10 @@ function wrapRequestHandler(
296296
return withIsolationScope(async isolationScope => {
297297
const options = getClient()?.getOptions();
298298

299-
let normalizedRequest: Record<string, unknown> = request;
299+
let normalizedRequest: RequestEventData = {};
300300

301301
try {
302-
normalizedRequest = normalizeRemixRequest(request);
302+
normalizedRequest = winterCGRequestToRequestData(request);
303303
} catch (e) {
304304
DEBUG_BUILD && logger.warn('Failed to normalize Remix request');
305305
}
@@ -311,11 +311,7 @@ function wrapRequestHandler(
311311
isolationScope.setTransactionName(name);
312312
}
313313

314-
isolationScope.setSDKProcessingMetadata({
315-
request: {
316-
...normalizedRequest,
317-
},
318-
});
314+
isolationScope.setSDKProcessingMetadata({ normalizedRequest });
319315

320316
if (!options || !hasTracingEnabled(options)) {
321317
return origRequestHandler.call(this, request, loadContext);

0 commit comments

Comments
 (0)