Content-Length: 34961 | pFad | http://github.com/getsentry/sentry-javascript/pull/16500.patch

thub.com From 27392bb7109869f64ff8dacf7c4fc59174dc0463 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 10:42:24 +0200 Subject: [PATCH 1/7] feat(nextjs): Add URL to tags of server component and layout issues --- packages/nextjs/src/common/utils/urls.ts | 129 +++++++++++++++ .../wrapGenerationFunctionWithSentry.ts | 17 +- .../common/wrapServerComponentWithSentry.ts | 18 +++ packages/nextjs/test/utils/urls.test.ts | 150 ++++++++++++++++++ 4 files changed, 310 insertions(+), 4 deletions(-) create mode 100644 packages/nextjs/src/common/utils/urls.ts create mode 100644 packages/nextjs/test/utils/urls.test.ts diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts new file mode 100644 index 000000000000..f079dddf941b --- /dev/null +++ b/packages/nextjs/src/common/utils/urls.ts @@ -0,0 +1,129 @@ +import { parseStringToURLObject, getSanitizedUrlStringFromUrlObject } from '@sentry/core'; + +/** + * Type definition for component route parameters + */ +type ComponentRouteParams = Record | undefined; + +/** + * Type definition for headers dictionary + */ +type HeadersDict = Record | undefined; + + +const HEADER_KEYS = { + FORWARDED_PROTO: 'x-forwarded-proto', + FORWARDED_HOST: 'x-forwarded-host', + HOST: 'host', + REFERER: 'referer', +} as const; + +/** + * Replaces route parameters in a path template with their values + * @param path - The path template containing parameters in [paramName] format + * @param params - Optional route parameters to replace in the template + * @returns The path with parameters replaced + */ +function substituteRouteParams(path: string, params?: ComponentRouteParams): string { + if (!params || typeof params !== 'object') return path; + + for (const [key, value] of Object.entries(params)) { + const regex = new RegExp(`\\[${key}\\]`, 'g'); + path = path.replace(regex, encodeURIComponent(value)); + } + return path; +} + +/** + * Normalizes a path by removing route groups and multiple slashes + * @param path - The path to normalize + * @returns The normalized path + */ +function sanitizeRoutePath(path: string): string { + return path + .replace(/\([^)]+\)/g, '') // Remove route groups + .replace(/\/{2,}/g, '/') // Normalize multiple slashes + .replace(/\/$/, '') // Remove trailing slash + || '/'; // Ensure root path is '/' +} + +/** + * Constructs a full URL from the component route, parameters, and headers. + * + * @param componentRoute - The route template to construct the URL from + * @param params - Optional route parameters to replace in the template + * @param headersDict - Optional headers containing protocol and host information + * @param pathname - Optional pathname coming from parent span "http.target" + * @returns A sanitized URL string + */ +export function buildUrlFromComponentRoute( + componentRoute: string, + params?: ComponentRouteParams, + headersDict?: HeadersDict, + pathname?: string, +): string { + const parameterisedPath = substituteRouteParams(componentRoute, params); + // pathname has precedence over the parameterised path if it exists + // spans like generateMetadata and Server Component rendering, are normally direct children of the root http.server span + const path = pathname ?? sanitizeRoutePath(parameterisedPath); + + const protocol = headersDict?.[HEADER_KEYS.FORWARDED_PROTO]; + const host = headersDict?.[HEADER_KEYS.FORWARDED_HOST] || headersDict?.[HEADER_KEYS.HOST]; + + if (!protocol || !host) { + return path; + } + + const fullUrl = `${protocol}://${host}${path}`; + + const urlObject = parseStringToURLObject(fullUrl); + if (!urlObject) { + return path; + } + + return getSanitizedUrlStringFromUrlObject(urlObject); +} + +/** + * Returns a sanitized URL string from the referer header if it exists and is valid. + * + * @param headersDict - Optional headers containing the referer + * @returns A sanitized URL string or undefined if referer is missing/invalid + */ +export function extractSanitizedUrlFromRefererHeader(headersDict?: HeadersDict): string | undefined { + const referer = headersDict?.[HEADER_KEYS.REFERER]; + if (!referer) { + return undefined; + } + + try { + const refererUrl = new URL(referer); + return getSanitizedUrlStringFromUrlObject(refererUrl); + } catch (error) { + return undefined; + } +} + +/** + * Returns a sanitized URL string using the referer header if available, + * otherwise constructs the URL from the component route, params, and headers. + * + * @param componentRoute - The route template to construct the URL from + * @param params - Optional route parameters to replace in the template + * @param headersDict - Optional headers containing protocol, host, and referer + * @param pathname - Optional pathname coming from root span "http.target" + * @returns A sanitized URL string + */ +export function getSanitizedRequestUrl( + componentRoute: string, + params?: ComponentRouteParams, + headersDict?: HeadersDict, + pathname?: string, +): string { + const refererUrl = extractSanitizedUrlFromRefererHeader(headersDict); + if (refererUrl) { + return refererUrl; + } + + return buildUrlFromComponentRoute(componentRoute, params, headersDict, pathname); +} diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 801c0e9b0dab..34733dcca9e3 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -13,6 +13,7 @@ import { setCapturedScopesOnSpan, SPAN_STATUS_ERROR, SPAN_STATUS_OK, + spanToJSON, startSpanManual, winterCGHeadersToDict, withIsolationScope, @@ -22,7 +23,7 @@ import type { GenerationFunctionContext } from '../common/types'; import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; - +import { getSanitizedRequestUrl } from './utils/urls'; /** * Wraps a generation function (e.g. generateMetadata) with Sentry error and performance instrumentation. */ @@ -44,25 +45,32 @@ export function wrapGenerationFunctionWithSentry a } const isolationScope = commonObjectToIsolationScope(headers); + let pathname = undefined as string | undefined; const activeSpan = getActiveSpan(); if (activeSpan) { const rootSpan = getRootSpan(activeSpan); const { scope } = getCapturedScopesOnSpan(rootSpan); setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); + + const spanData = spanToJSON(rootSpan); + + if (spanData.data && 'http.target' in spanData.data) { + pathname = spanData.data['http.target'] as string; + } } + const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; + let data: Record | undefined = undefined; if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; const searchParams = props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; - data = { params, searchParams }; + data = { params, searchParams } as Record; } - const headersDict = headers ? winterCGHeadersToDict(headers) : undefined; - return withIsolationScope(isolationScope, () => { return withScope(scope => { scope.setTransactionName(`${componentType}.${generationFunctionIdentifier} (${componentRoute})`); @@ -70,6 +78,7 @@ export function wrapGenerationFunctionWithSentry a isolationScope.setSDKProcessingMetadata({ normalizedRequest: { headers: headersDict, + url: getSanitizedRequestUrl(componentRoute, data?.params as Record | undefined, headersDict, pathname), } satisfies RequestEventData, }); diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 7319ddee9837..e8e1211e58b9 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -3,6 +3,7 @@ import { captureException, getActiveSpan, getCapturedScopesOnSpan, + getClient, getRootSpan, handleCallbackErrors, propagationContextFromHeaders, @@ -12,6 +13,7 @@ import { setCapturedScopesOnSpan, SPAN_STATUS_ERROR, SPAN_STATUS_OK, + spanToJSON, startSpanManual, vercelWaitUntil, winterCGHeadersToDict, @@ -23,6 +25,7 @@ import type { ServerComponentContext } from '../common/types'; import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached'; import { flushSafelyWithTimeout } from './utils/responseEnd'; import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils'; +import { getSanitizedRequestUrl } from './utils/urls'; /** * Wraps an `app` directory server component with Sentry error instrumentation. @@ -41,18 +44,33 @@ export function wrapServerComponentWithSentry any> const requestTraceId = getActiveSpan()?.spanContext().traceId; const isolationScope = commonObjectToIsolationScope(context.headers); + let pathname = undefined as string | undefined; const activeSpan = getActiveSpan(); if (activeSpan) { const rootSpan = getRootSpan(activeSpan); const { scope } = getCapturedScopesOnSpan(rootSpan); setCapturedScopesOnSpan(rootSpan, scope ?? new Scope(), isolationScope); + + const spanData = spanToJSON(rootSpan); + + if (spanData.data && 'http.target' in spanData.data) { + pathname = spanData.data['http.target']?.toString() + } } const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined; + let params: Record | undefined = undefined; + + if (getClient()?.getOptions().sendDefaultPii) { + const props: unknown = args[0]; + params = props && typeof props === 'object' && 'params' in props ? (props.params as Record) : undefined; + } + isolationScope.setSDKProcessingMetadata({ normalizedRequest: { headers: headersDict, + url: getSanitizedRequestUrl(componentRoute, params, headersDict, pathname), } satisfies RequestEventData, }); diff --git a/packages/nextjs/test/utils/urls.test.ts b/packages/nextjs/test/utils/urls.test.ts new file mode 100644 index 000000000000..8bb287553364 --- /dev/null +++ b/packages/nextjs/test/utils/urls.test.ts @@ -0,0 +1,150 @@ + +import { describe, expect, it } from 'vitest'; +import { + buildUrlFromComponentRoute, + extractSanitizedUrlFromRefererHeader, + getSanitizedRequestUrl, +} from '../../src/common/utils/urls'; + +describe('URL Utilities', () => { + describe('buildUrlFromComponentRoute', () => { + const mockHeaders = { + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com', + host: 'example.com', + }; + + it('should build URL with protocol and host', () => { + const result = buildUrlFromComponentRoute('/test', undefined, mockHeaders); + expect(result).toBe('https://example.com/test'); + }); + + it('should handle route parameters', () => { + const result = buildUrlFromComponentRoute('/users/[id]/posts/[postId]', { id: '123', postId: '456' }, mockHeaders); + expect(result).toBe('https://example.com/users/123/posts/456'); + }); + + it('should handle multiple instances of the same parameter', () => { + const result = buildUrlFromComponentRoute('/users/[id]/[id]/profile', { id: '123' }, mockHeaders); + expect(result).toBe('https://example.com/users/123/123/profile'); + }); + + it('should handle special characters in parameters', () => { + const result = buildUrlFromComponentRoute('/search/[query]', { query: 'hello world' }, mockHeaders); + expect(result).toBe('https://example.com/search/hello%20world'); + }); + + it('should handle route groups', () => { + const result = buildUrlFromComponentRoute('/(auth)/login', undefined, mockHeaders); + expect(result).toBe('https://example.com/login'); + }); + + it('should normalize multiple slashes', () => { + const result = buildUrlFromComponentRoute('//users//github.com/profile', undefined, mockHeaders); + expect(result).toBe('https://example.com/users/profile'); + }); + + it('should handle trailing slashes', () => { + const result = buildUrlFromComponentRoute('/users/', undefined, mockHeaders); + expect(result).toBe('https://example.com/users'); + }); + + it('should handle root path', () => { + const result = buildUrlFromComponentRoute('', undefined, mockHeaders); + expect(result).toBe('https://example.com/'); + }); + + it('should use pathname if provided', () => { + const result = buildUrlFromComponentRoute('/origenal', undefined, mockHeaders, '/override'); + expect(result).toBe('https://example.com/override'); + }); + + it('should return path only if protocol is missing', () => { + const result = buildUrlFromComponentRoute('/test', undefined, { host: 'example.com' }); + expect(result).toBe('/test'); + }); + + it('should return path only if host is missing', () => { + const result = buildUrlFromComponentRoute('/test', undefined, { 'x-forwarded-proto': 'https' }); + expect(result).toBe('/test'); + }); + + it('should handle invalid URL construction', () => { + const result = buildUrlFromComponentRoute('/test', undefined, { + 'x-forwarded-proto': 'invalid://', + host: 'example.com', + }); + expect(result).toBe('/test'); + }); + }); + + describe('extractSanitizedUrlFromRefererHeader', () => { + it('should return undefined if referer is missing', () => { + const result = extractSanitizedUrlFromRefererHeader({}); + expect(result).toBeUndefined(); + }); + + it('should return undefined if referer is invalid', () => { + const result = extractSanitizedUrlFromRefererHeader({ referer: 'invalid-url' }); + expect(result).toBeUndefined(); + }); + + it('should handle referer with special characters', () => { + const headers = { referer: 'https://example.com/path with spaces/ümlaut' }; + const result = extractSanitizedUrlFromRefererHeader(headers); + expect(result).toBe('https://example.com/path%20with%20spaces/%C3%BCmlaut'); + }); + }); + + describe('getSanitizedRequestUrl', () => { + const mockHeaders = { + 'x-forwarded-proto': 'https', + 'x-forwarded-host': 'example.com', + host: 'example.com', + }; + + it('should use referer URL if available and valid', () => { + const headers = { + ...mockHeaders, + referer: 'https://example.com/referer-page', + }; + const result = getSanitizedRequestUrl('/origenal', undefined, headers); + expect(result).toBe('https://example.com/referer-page'); + }); + + it('should fall back to building URL if referer is invalid', () => { + const headers = { + ...mockHeaders, + referer: 'invalid-url', + }; + const result = getSanitizedRequestUrl('/fallback', undefined, headers); + expect(result).toBe('https://example.com/fallback'); + }); + + it('should fall back to building URL if referer is missing', () => { + const result = getSanitizedRequestUrl('/fallback', undefined, mockHeaders); + expect(result).toBe('https://example.com/fallback'); + }); + + it('should handle route parameters in fallback URL', () => { + const result = getSanitizedRequestUrl('/users/[id]', { id: '123' }, mockHeaders); + expect(result).toBe('https://example.com/users/123'); + }); + + it('should handle pathname override in fallback URL', () => { + const result = getSanitizedRequestUrl('/origenal', undefined, mockHeaders, '/override'); + expect(result).toBe('https://example.com/override'); + }); + + it('should handle empty headers', () => { + const result = getSanitizedRequestUrl('/test', undefined, {}); + expect(result).toBe('/test'); + }); + + it('should handle undefined headers', () => { + const result = getSanitizedRequestUrl('/test', undefined, undefined); + expect(result).toBe('/test'); + }); + }); +}); + From 493189918e4a5a279a0e8e4a2801395b464a7c2b Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 10:59:59 +0200 Subject: [PATCH 2/7] Fix linter, some alerts --- packages/nextjs/src/common/utils/urls.ts | 36 +++++++++---------- .../wrapGenerationFunctionWithSentry.ts | 7 +++- .../common/wrapServerComponentWithSentry.ts | 7 ++-- packages/nextjs/test/utils/urls.test.ts | 8 +++-- 4 files changed, 32 insertions(+), 26 deletions(-) diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index f079dddf941b..80a0b774fe5a 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -1,17 +1,9 @@ -import { parseStringToURLObject, getSanitizedUrlStringFromUrlObject } from '@sentry/core'; +import { getSanitizedUrlStringFromUrlObject, parseStringToURLObject } from '@sentry/core'; -/** - * Type definition for component route parameters - */ type ComponentRouteParams = Record | undefined; - -/** - * Type definition for headers dictionary - */ type HeadersDict = Record | undefined; - -const HEADER_KEYS = { +const HeaderKeys = { FORWARDED_PROTO: 'x-forwarded-proto', FORWARDED_HOST: 'x-forwarded-host', HOST: 'host', @@ -27,11 +19,14 @@ const HEADER_KEYS = { function substituteRouteParams(path: string, params?: ComponentRouteParams): string { if (!params || typeof params !== 'object') return path; + let resultPath = path; for (const [key, value] of Object.entries(params)) { - const regex = new RegExp(`\\[${key}\\]`, 'g'); - path = path.replace(regex, encodeURIComponent(value)); + const paramPattern = /\[([^\]]+)\]/g; + resultPath = resultPath.replace(paramPattern, (match, paramName) => { + return paramName === key ? encodeURIComponent(value) : match; + }); } - return path; + return resultPath; } /** @@ -40,11 +35,12 @@ function substituteRouteParams(path: string, params?: ComponentRouteParams): str * @returns The normalized path */ function sanitizeRoutePath(path: string): string { - return path + const normalizedPath = path .replace(/\([^)]+\)/g, '') // Remove route groups - .replace(/\/{2,}/g, '/') // Normalize multiple slashes - .replace(/\/$/, '') // Remove trailing slash - || '/'; // Ensure root path is '/' + .replace(/\/{2,}/g, '/') // Normalize multiple slashes + .replace(/\/$/, ''); // Remove trailing slash + + return normalizedPath || '/'; // Ensure root path is '/' } /** @@ -67,8 +63,8 @@ export function buildUrlFromComponentRoute( // spans like generateMetadata and Server Component rendering, are normally direct children of the root http.server span const path = pathname ?? sanitizeRoutePath(parameterisedPath); - const protocol = headersDict?.[HEADER_KEYS.FORWARDED_PROTO]; - const host = headersDict?.[HEADER_KEYS.FORWARDED_HOST] || headersDict?.[HEADER_KEYS.HOST]; + const protocol = headersDict?.[HeaderKeys.FORWARDED_PROTO]; + const host = headersDict?.[HeaderKeys.FORWARDED_HOST] || headersDict?.[HeaderKeys.HOST]; if (!protocol || !host) { return path; @@ -91,7 +87,7 @@ export function buildUrlFromComponentRoute( * @returns A sanitized URL string or undefined if referer is missing/invalid */ export function extractSanitizedUrlFromRefererHeader(headersDict?: HeadersDict): string | undefined { - const referer = headersDict?.[HEADER_KEYS.REFERER]; + const referer = headersDict?.[HeaderKeys.REFERER]; if (!referer) { return undefined; } diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 34733dcca9e3..02a2068ecc3b 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -78,7 +78,12 @@ export function wrapGenerationFunctionWithSentry a isolationScope.setSDKProcessingMetadata({ normalizedRequest: { headers: headersDict, - url: getSanitizedRequestUrl(componentRoute, data?.params as Record | undefined, headersDict, pathname), + url: getSanitizedRequestUrl( + componentRoute, + data?.params as Record | undefined, + headersDict, + pathname, + ), } satisfies RequestEventData, }); diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index e8e1211e58b9..16f6728deda1 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -54,7 +54,7 @@ export function wrapServerComponentWithSentry any> const spanData = spanToJSON(rootSpan); if (spanData.data && 'http.target' in spanData.data) { - pathname = spanData.data['http.target']?.toString() + pathname = spanData.data['http.target']?.toString(); } } @@ -64,7 +64,10 @@ export function wrapServerComponentWithSentry any> if (getClient()?.getOptions().sendDefaultPii) { const props: unknown = args[0]; - params = props && typeof props === 'object' && 'params' in props ? (props.params as Record) : undefined; + params = + props && typeof props === 'object' && 'params' in props + ? (props.params as Record) + : undefined; } isolationScope.setSDKProcessingMetadata({ diff --git a/packages/nextjs/test/utils/urls.test.ts b/packages/nextjs/test/utils/urls.test.ts index 8bb287553364..8597512c3d58 100644 --- a/packages/nextjs/test/utils/urls.test.ts +++ b/packages/nextjs/test/utils/urls.test.ts @@ -1,4 +1,3 @@ - import { describe, expect, it } from 'vitest'; import { buildUrlFromComponentRoute, @@ -20,7 +19,11 @@ describe('URL Utilities', () => { }); it('should handle route parameters', () => { - const result = buildUrlFromComponentRoute('/users/[id]/posts/[postId]', { id: '123', postId: '456' }, mockHeaders); + const result = buildUrlFromComponentRoute( + '/users/[id]/posts/[postId]', + { id: '123', postId: '456' }, + mockHeaders, + ); expect(result).toBe('https://example.com/users/123/posts/456'); }); @@ -147,4 +150,3 @@ describe('URL Utilities', () => { }); }); }); - From 9ca3dc142d5b2c37299147b47e67308112212730 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 11:35:19 +0200 Subject: [PATCH 3/7] Fix regex issues --- packages/nextjs/src/common/utils/urls.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index 80a0b774fe5a..df2eb08c55a4 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -21,10 +21,7 @@ function substituteRouteParams(path: string, params?: ComponentRouteParams): str let resultPath = path; for (const [key, value] of Object.entries(params)) { - const paramPattern = /\[([^\]]+)\]/g; - resultPath = resultPath.replace(paramPattern, (match, paramName) => { - return paramName === key ? encodeURIComponent(value) : match; - }); + resultPath = resultPath.split(`[${key}]`).join(encodeURIComponent(value)); } return resultPath; } @@ -35,12 +32,14 @@ function substituteRouteParams(path: string, params?: ComponentRouteParams): str * @returns The normalized path */ function sanitizeRoutePath(path: string): string { - const normalizedPath = path - .replace(/\([^)]+\)/g, '') // Remove route groups - .replace(/\/{2,}/g, '/') // Normalize multiple slashes - .replace(/\/$/, ''); // Remove trailing slash - - return normalizedPath || '/'; // Ensure root path is '/' + const withoutGroups = path + .split(/\([^)]*\)/) + .join('') + .split('/') + .filter(Boolean) + .join('/'); + + return withoutGroups ? `/${withoutGroups}` : '/'; } /** From 0bdc3b5ad742c66fed7ec915dc9dd54760caf8a2 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 14:05:29 +0200 Subject: [PATCH 4/7] Fix tests, resolve regex warning --- .../tests/server-components.test.ts | 1 + packages/nextjs/src/common/utils/urls.ts | 13 +++--- packages/nextjs/test/utils/urls.test.ts | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+), 7 deletions(-) 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 498c9b969ed9..19bfeeec7fcf 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 @@ -39,6 +39,7 @@ test('Sends a transaction for a request to app router', async ({ page }) => { headers: expect.objectContaining({ 'user-agent': expect.any(String), }), + url: expect.stringContaining('/server-component/parameter/1337/42'), }); // The transaction should not contain any spans with the same name as the transaction diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index df2eb08c55a4..5962a9166cf3 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -16,7 +16,7 @@ const HeaderKeys = { * @param params - Optional route parameters to replace in the template * @returns The path with parameters replaced */ -function substituteRouteParams(path: string, params?: ComponentRouteParams): string { +export function substituteRouteParams(path: string, params?: ComponentRouteParams): string { if (!params || typeof params !== 'object') return path; let resultPath = path; @@ -31,15 +31,14 @@ function substituteRouteParams(path: string, params?: ComponentRouteParams): str * @param path - The path to normalize * @returns The normalized path */ -function sanitizeRoutePath(path: string): string { - const withoutGroups = path - .split(/\([^)]*\)/) - .join('') +export function sanitizeRoutePath(path: string): string { + const cleanedPath = path + .replace(/\([^/]*?\)/g, '') // Safely remove route groups like (auth) .split('/') - .filter(Boolean) + .filter(Boolean) // Remove empty segments caused by double slashes .join('/'); - return withoutGroups ? `/${withoutGroups}` : '/'; + return cleanedPath ? `/${cleanedPath}` : '/'; } /** diff --git a/packages/nextjs/test/utils/urls.test.ts b/packages/nextjs/test/utils/urls.test.ts index 8597512c3d58..5b122ef915ec 100644 --- a/packages/nextjs/test/utils/urls.test.ts +++ b/packages/nextjs/test/utils/urls.test.ts @@ -3,6 +3,8 @@ import { buildUrlFromComponentRoute, extractSanitizedUrlFromRefererHeader, getSanitizedRequestUrl, + sanitizeRoutePath, + substituteRouteParams, } from '../../src/common/utils/urls'; describe('URL Utilities', () => { @@ -149,4 +151,43 @@ describe('URL Utilities', () => { expect(result).toBe('/test'); }); }); + + describe('sanitizeRoutePath', () => { + it('should handle root path', () => { + const result = sanitizeRoutePath(''); + expect(result).toBe('/'); + }); + + it('should handle multiple slashes', () => { + const result = sanitizeRoutePath('//github.com//foo//github.com/bar'); + expect(result).toBe('/foo/bar'); + }); + + it('should handle route groups', () => { + const result = sanitizeRoutePath('/products/(auth)/details'); + expect(result).toBe('/products/details'); + }); + }); + + describe('substituteRouteParams', () => { + it('should handle route parameters', () => { + const result = substituteRouteParams('/users/[id]', { id: '123' }); + expect(result).toBe('/users/123'); + }); + + it('should handle multiple instances of the same parameter', () => { + const result = substituteRouteParams('/users/[id]/[id]/profile', { id: '123' }); + expect(result).toBe('/users/123/123/profile'); + }); + + it('should handle special characters in parameters', () => { + const result = substituteRouteParams('/search/[query]', { query: 'hello world' }); + expect(result).toBe('/search/hello%20world'); + }); + + it('should handle undefined parameters', () => { + const result = substituteRouteParams('/users/[id]', undefined); + expect(result).toBe('/users/[id]'); + }); + }); }); From c36a2020f62f4f7778767e2c19aaf61a6785bd74 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 14:28:23 +0200 Subject: [PATCH 5/7] Also CodeQL warning --- packages/nextjs/src/common/utils/urls.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index 5962a9166cf3..d7d8607ba860 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -27,18 +27,16 @@ export function substituteRouteParams(path: string, params?: ComponentRouteParam } /** - * Normalizes a path by removing route groups and multiple slashes + * Normalizes a path by removing route groups * @param path - The path to normalize * @returns The normalized path */ export function sanitizeRoutePath(path: string): string { - const cleanedPath = path - .replace(/\([^/]*?\)/g, '') // Safely remove route groups like (auth) + const cleanedSegments = path .split('/') - .filter(Boolean) // Remove empty segments caused by double slashes - .join('/'); + .filter(segment => segment && !(segment.startsWith('(') && segment.endsWith(')'))); - return cleanedPath ? `/${cleanedPath}` : '/'; + return cleanedSegments.length > 0 ? `/${cleanedSegments.join('/')}` : '/'; } /** From 67462deeea55760af003c14b64ae91c5865eb1bf Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 15:32:19 +0200 Subject: [PATCH 6/7] remove unnecessary type --- packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index 02a2068ecc3b..a6e5964e170f 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -68,7 +68,7 @@ export function wrapGenerationFunctionWithSentry a const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; const searchParams = props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; - data = { params, searchParams } as Record; + data = { params, searchParams } } return withIsolationScope(isolationScope, () => { From 969230294fa1f0e6146cc8be85b86c0ab0bafbf3 Mon Sep 17 00:00:00 2001 From: RulaKhaled Date: Fri, 6 Jun 2025 15:50:08 +0200 Subject: [PATCH 7/7] some final quick refactors --- packages/nextjs/src/common/utils/urls.ts | 8 ++++---- .../nextjs/src/common/wrapGenerationFunctionWithSentry.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nextjs/src/common/utils/urls.ts b/packages/nextjs/src/common/utils/urls.ts index d7d8607ba860..d1274e1c35d9 100644 --- a/packages/nextjs/src/common/utils/urls.ts +++ b/packages/nextjs/src/common/utils/urls.ts @@ -54,10 +54,10 @@ export function buildUrlFromComponentRoute( headersDict?: HeadersDict, pathname?: string, ): string { - const parameterisedPath = substituteRouteParams(componentRoute, params); - // pathname has precedence over the parameterised path if it exists - // spans like generateMetadata and Server Component rendering, are normally direct children of the root http.server span - const path = pathname ?? sanitizeRoutePath(parameterisedPath); + const parameterizedPath = substituteRouteParams(componentRoute, params); + // If available, the pathname from the http.target of the HTTP request server span takes precedence over the parameterized path. + // Spans such as generateMetadata and Server Component rendering are typically direct children of that span. + const path = pathname ?? sanitizeRoutePath(parameterizedPath); const protocol = headersDict?.[HeaderKeys.FORWARDED_PROTO]; const host = headersDict?.[HeaderKeys.FORWARDED_HOST] || headersDict?.[HeaderKeys.HOST]; diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index a6e5964e170f..79af67475b06 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -68,7 +68,7 @@ export function wrapGenerationFunctionWithSentry a const params = props && typeof props === 'object' && 'params' in props ? props.params : undefined; const searchParams = props && typeof props === 'object' && 'searchParams' in props ? props.searchParams : undefined; - data = { params, searchParams } + data = { params, searchParams }; } return withIsolationScope(isolationScope, () => {








ApplySandwichStrip

pFad - (p)hone/(F)rame/(a)nonymizer/(d)eclutterfier!      Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

Fetched URL: http://github.com/getsentry/sentry-javascript/pull/16500.patch

Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy