|
| 1 | +import { getSanitizedUrlStringFromUrlObject, parseStringToURLObject } from '@sentry/core'; |
| 2 | + |
| 3 | +type ComponentRouteParams = Record<string, string> | undefined; |
| 4 | +type HeadersDict = Record<string, string> | undefined; |
| 5 | + |
| 6 | +const HeaderKeys = { |
| 7 | + FORWARDED_PROTO: 'x-forwarded-proto', |
| 8 | + FORWARDED_HOST: 'x-forwarded-host', |
| 9 | + HOST: 'host', |
| 10 | + REFERER: 'referer', |
| 11 | +} as const; |
| 12 | + |
| 13 | +/** |
| 14 | + * Replaces route parameters in a path template with their values |
| 15 | + * @param path - The path template containing parameters in [paramName] format |
| 16 | + * @param params - Optional route parameters to replace in the template |
| 17 | + * @returns The path with parameters replaced |
| 18 | + */ |
| 19 | +export function substituteRouteParams(path: string, params?: ComponentRouteParams): string { |
| 20 | + if (!params || typeof params !== 'object') return path; |
| 21 | + |
| 22 | + let resultPath = path; |
| 23 | + for (const [key, value] of Object.entries(params)) { |
| 24 | + resultPath = resultPath.split(`[${key}]`).join(encodeURIComponent(value)); |
| 25 | + } |
| 26 | + return resultPath; |
| 27 | +} |
| 28 | + |
| 29 | +/** |
| 30 | + * Normalizes a path by removing route groups |
| 31 | + * @param path - The path to normalize |
| 32 | + * @returns The normalized path |
| 33 | + */ |
| 34 | +export function sanitizeRoutePath(path: string): string { |
| 35 | + const cleanedSegments = path |
| 36 | + .split('/') |
| 37 | + .filter(segment => segment && !(segment.startsWith('(') && segment.endsWith(')'))); |
| 38 | + |
| 39 | + return cleanedSegments.length > 0 ? `/${cleanedSegments.join('/')}` : '/'; |
| 40 | +} |
| 41 | + |
| 42 | +/** |
| 43 | + * Constructs a full URL from the component route, parameters, and headers. |
| 44 | + * |
| 45 | + * @param componentRoute - The route template to construct the URL from |
| 46 | + * @param params - Optional route parameters to replace in the template |
| 47 | + * @param headersDict - Optional headers containing protocol and host information |
| 48 | + * @param pathname - Optional pathname coming from parent span "http.target" |
| 49 | + * @returns A sanitized URL string |
| 50 | + */ |
| 51 | +export function buildUrlFromComponentRoute( |
| 52 | + componentRoute: string, |
| 53 | + params?: ComponentRouteParams, |
| 54 | + headersDict?: HeadersDict, |
| 55 | + pathname?: string, |
| 56 | +): string { |
| 57 | + const parameterizedPath = substituteRouteParams(componentRoute, params); |
| 58 | + // If available, the pathname from the http.target of the HTTP request server span takes precedence over the parameterized path. |
| 59 | + // Spans such as generateMetadata and Server Component rendering are typically direct children of that span. |
| 60 | + const path = pathname ?? sanitizeRoutePath(parameterizedPath); |
| 61 | + |
| 62 | + const protocol = headersDict?.[HeaderKeys.FORWARDED_PROTO]; |
| 63 | + const host = headersDict?.[HeaderKeys.FORWARDED_HOST] || headersDict?.[HeaderKeys.HOST]; |
| 64 | + |
| 65 | + if (!protocol || !host) { |
| 66 | + return path; |
| 67 | + } |
| 68 | + |
| 69 | + const fullUrl = `${protocol}://${host}${path}`; |
| 70 | + |
| 71 | + const urlObject = parseStringToURLObject(fullUrl); |
| 72 | + if (!urlObject) { |
| 73 | + return path; |
| 74 | + } |
| 75 | + |
| 76 | + return getSanitizedUrlStringFromUrlObject(urlObject); |
| 77 | +} |
| 78 | + |
| 79 | +/** |
| 80 | + * Returns a sanitized URL string from the referer header if it exists and is valid. |
| 81 | + * |
| 82 | + * @param headersDict - Optional headers containing the referer |
| 83 | + * @returns A sanitized URL string or undefined if referer is missing/invalid |
| 84 | + */ |
| 85 | +export function extractSanitizedUrlFromRefererHeader(headersDict?: HeadersDict): string | undefined { |
| 86 | + const referer = headersDict?.[HeaderKeys.REFERER]; |
| 87 | + if (!referer) { |
| 88 | + return undefined; |
| 89 | + } |
| 90 | + |
| 91 | + try { |
| 92 | + const refererUrl = new URL(referer); |
| 93 | + return getSanitizedUrlStringFromUrlObject(refererUrl); |
| 94 | + } catch (error) { |
| 95 | + return undefined; |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +/** |
| 100 | + * Returns a sanitized URL string using the referer header if available, |
| 101 | + * otherwise constructs the URL from the component route, params, and headers. |
| 102 | + * |
| 103 | + * @param componentRoute - The route template to construct the URL from |
| 104 | + * @param params - Optional route parameters to replace in the template |
| 105 | + * @param headersDict - Optional headers containing protocol, host, and referer |
| 106 | + * @param pathname - Optional pathname coming from root span "http.target" |
| 107 | + * @returns A sanitized URL string |
| 108 | + */ |
| 109 | +export function getSanitizedRequestUrl( |
| 110 | + componentRoute: string, |
| 111 | + params?: ComponentRouteParams, |
| 112 | + headersDict?: HeadersDict, |
| 113 | + pathname?: string, |
| 114 | +): string { |
| 115 | + const refererUrl = extractSanitizedUrlFromRefererHeader(headersDict); |
| 116 | + if (refererUrl) { |
| 117 | + return refererUrl; |
| 118 | + } |
| 119 | + |
| 120 | + return buildUrlFromComponentRoute(componentRoute, params, headersDict, pathname); |
| 121 | +} |
0 commit comments