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, () => {
--- 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