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..19e05647a908 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 { RequestEventData, 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 RequestEventData, }); 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..268c96876799 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, RequestEventData, 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 RequestEventData, }); 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/nextjs/src/common/captureRequestError.ts b/packages/nextjs/src/common/captureRequestError.ts index 1556076619a0..cce8b3d148ba 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 { RequestEventData } 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 RequestEventData, }); scope.setContext('nextjs', { diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index b13d3ebef3dd..4bada5fe7f91 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 { RequestEventData } 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 RequestEventData, }); 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..7079542cacab 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 { RequestEventData } 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 RequestEventData, }); } diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index c4bbde29eb53..e2d06ec87333 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 { RequestEventData } 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 RequestEventData, }); 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 060d30f2d5a3..e6de8b057f51 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, RequestEventData, SanitizedRequestData, Scope } 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/utils/src/index.ts b/packages/utils/src/index.ts index e60bc3bec409..fb90f8dcabb7 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -73,6 +73,8 @@ export { extractRequestData, winterCGHeadersToDict, winterCGRequestToRequestData, + extractQueryParamsFromUrl, + headersToDict, } from './requestdata'; export type { AddRequestDataToEventOptions, diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 13ec367addda..a82fe2ff7fec 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -416,18 +416,58 @@ 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): RequestEventData { 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: RequestEventData, { include }: { include: string[] },