diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 75f30075a47f..ff61cb3bd682 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -33,17 +33,12 @@ test('Sends a transaction for a request to app router', async ({ page }) => { trace_id: expect.any(String), }); - expect(transactionEvent).toEqual( - expect.objectContaining({ - request: { - cookies: {}, - headers: expect.any(Object), - url: expect.any(String), - }, + expect(transactionEvent.request).toEqual({ + cookies: {}, + headers: expect.objectContaining({ + 'user-agent': expect.any(String), }), - ); - - expect(Object.keys(transactionEvent.request?.headers!).length).toBeGreaterThan(0); + }); // The transaction should not contain any spans with the same name as the transaction // e.g. "GET /server-component/parameter/[...parameters]" diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 50c8e973adfe..6439d6cb99d4 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -12,9 +12,10 @@ import { startSpan, withIsolationScope, } from '@sentry/node'; -import type { Scope, SpanAttributes } from '@sentry/types'; +import type { Request, Scope, SpanAttributes } from '@sentry/types'; import { addNonEnumerableProperty, + extractQueryParamsFromUrl, logger, objectify, stripUrlQueryAndFragment, @@ -111,7 +112,13 @@ async function instrumentRequest( getCurrentScope().setSDKProcessingMetadata({ // We store the request on the current scope, not isolation scope, // because we may have multiple requests nested inside each other - request: isDynamicPageRequest ? winterCGRequestToRequestData(request) : { method, url: request.url }, + normalizedRequest: (isDynamicPageRequest + ? winterCGRequestToRequestData(request) + : { + method, + url: request.url, + query_string: extractQueryParamsFromUrl(request.url), + }) satisfies Request, }); if (options.trackClientIp && isDynamicPageRequest) { diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 093b2fad2d6b..87fcb611eab2 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -221,7 +221,7 @@ describe('sentryMiddleware', () => { await middleware(ctx, next); expect(setSDKProcessingMetadataMock).toHaveBeenCalledWith({ - request: { + normalizedRequest: { method: 'GET', url: '/users', headers: { @@ -254,7 +254,7 @@ describe('sentryMiddleware', () => { await middleware(ctx, next); expect(setSDKProcessingMetadataMock).toHaveBeenCalledWith({ - request: { + normalizedRequest: { method: 'GET', url: '/users', }, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 193ae6f286ca..c48083306acd 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -9,8 +9,8 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; -import type { IntegrationFn, SpanAttributes } from '@sentry/types'; -import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; +import type { IntegrationFn, Request, SpanAttributes } from '@sentry/types'; +import { extractQueryParamsFromUrl, getSanitizedUrlString, parseUrl } from '@sentry/utils'; const INTEGRATION_NAME = 'BunServer'; @@ -76,11 +76,12 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const url = getSanitizedUrlString(parsedUrl); isolationScope.setSDKProcessingMetadata({ - request: { + normalizedRequest: { url, method: request.method, headers: request.headers.toJSON(), - }, + query_string: extractQueryParamsFromUrl(url), + } satisfies Request, }); return continueTrace( diff --git a/packages/cloudflare/src/scope-utils.ts b/packages/cloudflare/src/scope-utils.ts index 1f5bbce8f0fc..12c500b711a8 100644 --- a/packages/cloudflare/src/scope-utils.ts +++ b/packages/cloudflare/src/scope-utils.ts @@ -25,5 +25,5 @@ export function addCultureContext(scope: Scope, cf: IncomingRequestCfProperties) * Set request data on scope */ export function addRequest(scope: Scope, request: Request): void { - scope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(request) }); + scope.setSDKProcessingMetadata({ normalizedRequest: winterCGRequestToRequestData(request) }); } diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts index 5218e8afe20b..d35ccf3d50a7 100644 --- a/packages/cloudflare/test/request.test.ts +++ b/packages/cloudflare/test/request.test.ts @@ -109,7 +109,7 @@ describe('withSentry', () => { }, ); - expect(sentryEvent.sdkProcessingMetadata?.request).toEqual({ + expect(sentryEvent.sdkProcessingMetadata?.normalizedRequest).toEqual({ headers: {}, url: 'https://example.com/', method: 'GET', diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index ff89c0d593a9..f0c940d01971 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -24,6 +24,7 @@ import type { import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { updateSession } from './session'; +import { mergeSdkProcessingMetadata } from './utils/applyScopeDataToEvent'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; /** @@ -479,8 +480,7 @@ class ScopeClass implements ScopeInterface { * @inheritDoc */ public setSDKProcessingMetadata(newData: { [key: string]: unknown }): this { - this._sdkProcessingMetadata = { ...this._sdkProcessingMetadata, ...newData }; - + this._sdkProcessingMetadata = mergeSdkProcessingMetadata(this._sdkProcessingMetadata, newData); return this; } diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index f3b1ac0d0be7..772065f191e0 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -46,7 +46,8 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { mergeAndOverwriteScopeData(data, 'tags', tags); mergeAndOverwriteScopeData(data, 'user', user); mergeAndOverwriteScopeData(data, 'contexts', contexts); - mergeAndOverwriteScopeData(data, 'sdkProcessingMetadata', sdkProcessingMetadata); + + data.sdkProcessingMetadata = mergeSdkProcessingMetadata(data.sdkProcessingMetadata, sdkProcessingMetadata); if (level) { data.level = level; @@ -115,6 +116,35 @@ export function mergeArray( event[prop] = merged.length ? merged : undefined; } +/** + * Merge new SDK processing metadata into existing data. + * New data will overwrite existing data. + * `normalizedRequest` is special handled and will also be merged. + */ +export function mergeSdkProcessingMetadata( + sdkProcessingMetadata: ScopeData['sdkProcessingMetadata'], + newSdkProcessingMetadata: ScopeData['sdkProcessingMetadata'], +): ScopeData['sdkProcessingMetadata'] { + // We want to merge `normalizedRequest` to avoid some partial entry on the scope + // overwriting potentially more complete data on the isolation scope + const normalizedRequestBefore = sdkProcessingMetadata['normalizedRequest']; + const normalizedRequest = newSdkProcessingMetadata['normalizedRequest']; + + const newData = { + ...sdkProcessingMetadata, + ...newSdkProcessingMetadata, + }; + + if (normalizedRequestBefore || normalizedRequest) { + newData['normalizedRequest'] = { + ...(normalizedRequestBefore || {}), + ...(normalizedRequest || {}), + }; + } + + return newData; +} + function applyDataToEvent(event: Event, data: ScopeData): void { const { extra, tags, user, contexts, level, transactionName } = data; diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 33c89a2e9eb5..5711e90c3da8 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -204,10 +204,47 @@ describe('Scope', () => { expect(scope['_user']).toEqual({}); }); - test('setProcessingMetadata', () => { - const scope = new Scope(); - scope.setSDKProcessingMetadata({ dogs: 'are great!' }); - expect(scope['_sdkProcessingMetadata'].dogs).toEqual('are great!'); + describe('setProcessingMetadata', () => { + test('it works with no initial data', () => { + const scope = new Scope(); + scope.setSDKProcessingMetadata({ dogs: 'are great!' }); + expect(scope['_sdkProcessingMetadata'].dogs).toEqual('are great!'); + }); + + test('it overwrites arbitrary data', () => { + const scope = new Scope(); + scope.setSDKProcessingMetadata({ dogs: 'are great!' }); + scope.setSDKProcessingMetadata({ dogs: 'are really great!' }); + scope.setSDKProcessingMetadata({ cats: 'are also great!' }); + scope.setSDKProcessingMetadata({ obj: { nested: 'value1' } }); + scope.setSDKProcessingMetadata({ obj: { nested2: 'value2' } }); + + expect(scope['_sdkProcessingMetadata']).toEqual({ + dogs: 'are really great!', + cats: 'are also great!', + obj: { nested2: 'value2' }, + }); + }); + + test('it merges normalizedRequest data', () => { + const scope = new Scope(); + scope.setSDKProcessingMetadata({ + normalizedRequest: { + url: 'value1', + method: 'value1', + }, + }); + scope.setSDKProcessingMetadata({ + normalizedRequest: { + url: 'value2', + headers: {}, + }, + }); + + expect(scope['_sdkProcessingMetadata']).toEqual({ + normalizedRequest: { url: 'value2', method: 'value1', headers: {} }, + }); + }); }); test('set and get propagation context', () => { diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts index e6370931f4cf..a0a52ccffa76 100644 --- a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -5,6 +5,7 @@ import { mergeAndOverwriteScopeData, mergeArray, mergeScopeData, + mergeSdkProcessingMetadata, } from '../../../src/utils/applyScopeDataToEvent'; describe('mergeArray', () => { @@ -134,7 +135,15 @@ describe('mergeScopeData', () => { contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, attachments: [attachment1], propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, + sdkProcessingMetadata: { + aa: 'aa', + bb: 'aa', + obj: { key: 'value' }, + normalizedRequest: { + url: 'oldUrl', + method: 'oldMethod', + }, + }, fingerprint: ['aa', 'bb'], }; const data2: ScopeData = { @@ -146,7 +155,17 @@ describe('mergeScopeData', () => { contexts: { os: { name: 'os2' } }, attachments: [attachment2, attachment3], propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, + sdkProcessingMetadata: { + bb: 'bb', + cc: 'bb', + // Regular objects are not deep merged + obj: { key2: 'value2' }, + // Only normalizedRequest is deep merged + normalizedRequest: { + url: 'newUrl', + headers: {}, + }, + }, fingerprint: ['cc'], }; mergeScopeData(data1, data2); @@ -159,12 +178,136 @@ describe('mergeScopeData', () => { contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, attachments: [attachment1, attachment2, attachment3], propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, + sdkProcessingMetadata: { + aa: 'aa', + bb: 'bb', + cc: 'bb', + obj: { key2: 'value2' }, + normalizedRequest: { + url: 'newUrl', + method: 'oldMethod', + headers: {}, + }, + }, fingerprint: ['aa', 'bb', 'cc'], }); }); }); +describe('mergeSdkProcessingMetadata', () => { + it('works with empty objects', () => { + const oldData = {}; + const newData = {}; + + const actual = mergeSdkProcessingMetadata(oldData, newData); + + expect(actual).toEqual({}); + expect(actual).not.toBe(oldData); + expect(actual).not.toBe(newData); + }); + + it('works with arbitrary data', () => { + const oldData = { + old1: 'old1', + old2: 'old2', + obj: { key: 'value' }, + }; + const newData = { + new1: 'new1', + old2: 'new2', + obj: { key2: 'value2' }, + }; + + const actual = mergeSdkProcessingMetadata(oldData, newData); + + expect(actual).toEqual({ + old1: 'old1', + old2: 'new2', + new1: 'new1', + obj: { key2: 'value2' }, + }); + expect(actual).not.toBe(oldData); + expect(actual).not.toBe(newData); + }); + + it('merges normalizedRequest', () => { + const oldData = { + old1: 'old1', + normalizedRequest: { + url: 'oldUrl', + method: 'oldMethod', + }, + }; + const newData = { + new1: 'new1', + normalizedRequest: { + url: 'newUrl', + headers: {}, + }, + }; + + const actual = mergeSdkProcessingMetadata(oldData, newData); + + expect(actual).toEqual({ + old1: 'old1', + new1: 'new1', + normalizedRequest: { + url: 'newUrl', + method: 'oldMethod', + headers: {}, + }, + }); + }); + + it('keeps existing normalizedRequest', () => { + const oldData = { + old1: 'old1', + normalizedRequest: { + url: 'oldUrl', + method: 'oldMethod', + }, + }; + const newData = { + new1: 'new1', + }; + + const actual = mergeSdkProcessingMetadata(oldData, newData); + + expect(actual).toEqual({ + old1: 'old1', + new1: 'new1', + normalizedRequest: { + url: 'oldUrl', + method: 'oldMethod', + }, + }); + }); + + it('adds new normalizedRequest', () => { + const oldData = { + old1: 'old1', + }; + const newData = { + new1: 'new1', + normalizedRequest: { + url: 'newUrl', + method: 'newMethod', + }, + }; + + const actual = mergeSdkProcessingMetadata(oldData, newData); + + expect(actual).toEqual({ + old1: 'old1', + new1: 'new1', + normalizedRequest: { + url: 'newUrl', + method: 'newMethod', + }, + }); + }); +}); + describe('applyScopeDataToEvent', () => { it("doesn't apply scope.transactionName to transaction events", () => { const data: ScopeData = { diff --git a/packages/nextjs/src/common/captureRequestError.ts b/packages/nextjs/src/common/captureRequestError.ts index 1556076619a0..304fc205ad0b 100644 --- a/packages/nextjs/src/common/captureRequestError.ts +++ b/packages/nextjs/src/common/captureRequestError.ts @@ -1,4 +1,6 @@ import { captureException, withScope } from '@sentry/core'; +import type { Request } from '@sentry/types'; +import { headersToDict } from '@sentry/utils'; type RequestInfo = { path: string; @@ -18,10 +20,10 @@ type ErrorContext = { export function captureRequestError(error: unknown, request: RequestInfo, errorContext: ErrorContext): void { withScope(scope => { scope.setSDKProcessingMetadata({ - request: { - headers: request.headers, + normalizedRequest: { + headers: headersToDict(request.headers), method: request.method, - }, + } satisfies Request, }); scope.setContext('nextjs', { diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index b13d3ebef3dd..1f68befcb47e 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -9,6 +9,7 @@ import { startSpan, withIsolationScope, } from '@sentry/core'; +import type { Request } from '@sentry/types'; import { logger, vercelWaitUntil } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; @@ -89,9 +90,9 @@ async function withServerActionInstrumentationImplementation a scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); isolationScope.setSDKProcessingMetadata({ - request: { + normalizedRequest: { headers: headersDict, - }, + } satisfies Request, }); const activeSpan = getActiveSpan(); diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts index e8b57c7d2b8b..8abcd3723eda 100644 --- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts +++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts @@ -36,7 +36,7 @@ export function wrapMiddlewareWithSentry( if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ - request: winterCGRequestToRequestData(req), + normalizedRequest: winterCGRequestToRequestData(req), }); spanName = `middleware ${req.method} ${new URL(req.url).pathname}`; spanSource = 'url'; diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts index 215bb35ce9a5..a17512b5a2d1 100644 --- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts @@ -13,7 +13,7 @@ import { withIsolationScope, withScope, } from '@sentry/core'; - +import type { Request as SentryRequest } from '@sentry/types'; import type { RouteHandlerContext } from './types'; import { propagationContextFromHeaders, winterCGHeadersToDict } from '@sentry/utils'; @@ -64,10 +64,10 @@ export function wrapRouteHandlerWithSentry any>( ); scope.setPropagationContext(incomingPropagationContext); scope.setSDKProcessingMetadata({ - request: { + normalizedRequest: { method, headers: completeHeadersDict, - }, + } satisfies SentryRequest, }); } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index c4bbde29eb53..51c5c3788db2 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -13,6 +13,7 @@ import { withIsolationScope, withScope, } from '@sentry/core'; +import type { Request as SentryRequest } from '@sentry/types'; import { propagationContextFromHeaders, uuid4, vercelWaitUntil, winterCGHeadersToDict } from '@sentry/utils'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; @@ -49,9 +50,9 @@ export function wrapServerComponentWithSentry any> const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; isolationScope.setSDKProcessingMetadata({ - request: { + normalizedRequest: { headers: headersDict, - }, + } satisfies SentryRequest, }); return withIsolationScope(isolationScope, () => { diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 5c8ce043ecb8..64bb8eceaad7 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -32,7 +32,7 @@ export function wrapApiHandlerWithSentry( if (req instanceof Request) { isolationScope.setSDKProcessingMetadata({ - request: winterCGRequestToRequestData(req), + normalizedRequest: winterCGRequestToRequestData(req), }); currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`); } else { diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts index 6b6fe8aaad40..9bc109d75cef 100644 --- a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node/src/integrations/http/SentryHttpInstrumentation.ts @@ -8,8 +8,10 @@ import { getRequestInfo } from '@opentelemetry/instrumentation-http'; import { addBreadcrumb, getClient, getIsolationScope, withIsolationScope } from '@sentry/core'; import type { PolymorphicRequest, Request, SanitizedRequestData } from '@sentry/types'; import { + extractQueryParamsFromUrl, getBreadcrumbLogLevelFromHttpStatusCode, getSanitizedUrlString, + headersToDict, logger, parseUrl, stripUrlQueryAndFragment, @@ -145,7 +147,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase): Record { - const headers: Record = Object.create(null); - - try { - Object.entries(reqHeaders).forEach(([key, value]) => { - if (typeof value === 'string') { - headers[key] = value; - } - }); - } catch (e) { - DEBUG_BUILD && - logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); - } - - return headers; -} diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index cee8520400c1..7d8a570aa6a2 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -131,7 +131,9 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { return withIsolationScope(isolationScope => { // We only call continueTrace in the initial top level request to avoid // creating a new root span for the sub request. - isolationScope.setSDKProcessingMetadata({ request: winterCGRequestToRequestData(input.event.request.clone()) }); + isolationScope.setSDKProcessingMetadata({ + normalizedRequest: winterCGRequestToRequestData(input.event.request.clone()), + }); return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); }); }; @@ -167,7 +169,9 @@ async function instrumentHandle( name: routeName, }, async (span?: Span) => { - getCurrentScope().setSDKProcessingMetadata({ request: winterCGRequestToRequestData(event.request.clone()) }); + getCurrentScope().setSDKProcessingMetadata({ + normalizedRequest: winterCGRequestToRequestData(event.request.clone()), + }); const res = await resolve(event, { transformPageChunk: addSentryCodeToPage(options), }); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 2d5c72230aef..d2c33bf0b720 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -217,7 +217,9 @@ export interface Scope { clearAttachments(): this; /** - * Add data which will be accessible during event processing but won't get sent to Sentry + * Add data which will be accessible during event processing but won't get sent to Sentry. + * + * TODO(v9): We should type this stricter, so that e.g. `normalizedRequest` is strictly typed. */ setSDKProcessingMetadata(newData: { [key: string]: unknown }): this; diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index edffc6f67da7..415a1c021757 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -434,18 +434,57 @@ export function winterCGHeadersToDict(winterCGHeaders: WebFetchHeaders): Record< return headers; } +/** + * Convert common request headers to a simple dictionary. + */ +export function headersToDict(reqHeaders: Record): Record { + const headers: Record = Object.create(null); + + try { + Object.entries(reqHeaders).forEach(([key, value]) => { + if (typeof value === 'string') { + headers[key] = value; + } + }); + } catch (e) { + DEBUG_BUILD && + logger.warn('Sentry failed extracting headers from a request object. If you see this, please file an issue.'); + } + + return headers; +} + /** * Converts a `Request` object that implements the `Web Fetch API` (https://developer.mozilla.org/en-US/docs/Web/API/Headers) into the format that the `RequestData` integration understands. */ -export function winterCGRequestToRequestData(req: WebFetchRequest): PolymorphicRequest { +export function winterCGRequestToRequestData(req: WebFetchRequest): Request { const headers = winterCGHeadersToDict(req.headers); return { method: req.method, url: req.url, + query_string: extractQueryParamsFromUrl(req.url), headers, + // TODO: Can we extract body data from the request? }; } +/** Extract the query params from an URL. */ +export function extractQueryParamsFromUrl(url: string): string | undefined { + // url is path and query string + if (!url) { + return; + } + + try { + // The `URL` constructor can't handle internal URLs of the form `/some/path/here`, so stick a dummy protocol and + // hostname as the base. Since the point here is just to grab the query string, it doesn't matter what we use. + const queryParams = new URL(url, 'http://dogs.are.great').search.slice(1); + return queryParams.length ? queryParams : undefined; + } catch { + return undefined; + } +} + function extractNormalizedRequestData(normalizedRequest: Request, { include }: { include: string[] }): Request { const includeKeys = include ? (Array.isArray(include) ? include : DEFAULT_REQUEST_INCLUDES) : [];