-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core): Add route provider API for parameterized route resolution #23551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| import type { Client } from './client'; | ||
| import { getClient } from './currentScopes'; | ||
| import { DEBUG_BUILD } from './debug-build'; | ||
| import { getLocationHref } from './utils/browser'; | ||
| import { LRUMap } from './utils/lru'; | ||
| import { debug } from './utils/debug-logger'; | ||
|
|
||
| /** | ||
| * Resolves URLs to low-cardinality route names. | ||
| * | ||
| * Framework SDKs register one so that everything the SDK names after a route (span names, the scope's | ||
| * transaction name, metric and span segment attributes) gets the parameterized route instead of the raw | ||
| * URL, without each integration having to reach into the framework's router itself. | ||
| * | ||
| * A provider only answers "which route is this", never what the caller does with the answer. | ||
| */ | ||
| export interface RouteProvider { | ||
| /** | ||
| * Resolves a URL path template for a specific URL, e.g. `/users/42` -> `/users/:id`. | ||
| * | ||
| * Must return a path template, never a route identifier. Routers that name routes independently of | ||
| * their path (Vue Router's `route.name`, Ember's `posts.show`) have to return the matched path | ||
| * instead: callers set `url.template` from this, and an identifier is not a template. An SDK that | ||
| * wants to name its span after the identifier still can, on the span itself. | ||
| * | ||
| * Returns `undefined` when the URL matches no known route. Must answer for the URL it is given rather | ||
| * than for wherever the router currently is, so that callers can resolve a URL they captured earlier | ||
| * (a web vital reported after a soft navigation, for example). | ||
| */ | ||
| resolveRoute(url: URL): string | undefined; | ||
|
|
||
| /** | ||
| * Resolves the route the app is currently on. | ||
| * | ||
| * Routers whose location lives in the address bar can delegate to `resolveRoute`, which is what | ||
| * {@link createUrlRouteProvider} does. Routers that keep their own location (memory and hash routers) | ||
| * have to answer from that location instead: for those, `location.href` is the unchanging shell URL | ||
| * and would bucket every route together. | ||
| */ | ||
| resolveCurrentRoute(): string | undefined; | ||
| } | ||
|
|
||
| const CLIENT_ROUTE_PROVIDERS = new WeakMap<Client, RouteProvider>(); | ||
|
|
||
| /** | ||
| * Registers the route provider for a client, replacing any previously registered one. | ||
| * | ||
| * Register during an integration's `setup` rather than `afterAllSetup`: the pageload span is named | ||
| * while `browserTracingIntegration` sets up, so a provider registered later can only rename it after | ||
| * the fact. | ||
| * | ||
| * A client holds one provider. An app running two routers (a framework migration, or a shell plus an | ||
| * island) registers twice and the last one wins, so the first router's routes stop resolving. | ||
| */ | ||
| export function setRouteProvider(provider: RouteProvider, client: Client | undefined = getClient()): void { | ||
| if (!client) { | ||
| DEBUG_BUILD && debug.warn('Cannot set a route provider without a client.'); | ||
| return; | ||
| } | ||
|
|
||
| if (DEBUG_BUILD && CLIENT_ROUTE_PROVIDERS.has(client)) { | ||
| debug.warn( | ||
| 'A route provider is already registered for this client and will be replaced. Routes only the previous provider knows about will no longer resolve.', | ||
| ); | ||
| } | ||
|
|
||
| CLIENT_ROUTE_PROVIDERS.set(client, provider); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the route provider registered for a client, if any. | ||
| */ | ||
| export function getRouteProvider(client: Client | undefined = getClient()): RouteProvider | undefined { | ||
| return client && CLIENT_ROUTE_PROVIDERS.get(client); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves a URL to a low-cardinality route name, e.g. `/users/42` -> `/users/:id`. | ||
| * | ||
| * Returns `undefined` when no route provider is registered or the URL matches no route. Callers pick | ||
| * their own fallback, because the right one differs: a span name falls back to a low-cardinality | ||
| * constant, the scope's transaction name to the raw path. | ||
| */ | ||
| export function resolveRoute(url: string | URL, client: Client | undefined = getClient()): string | undefined { | ||
| const provider = getRouteProvider(client); | ||
| if (!provider) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const urlObject = typeof url === 'string' ? toURLObject(url) : url; | ||
| if (!urlObject) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return callProvider(() => provider.resolveRoute(urlObject)); | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the route the app is currently on. | ||
| * | ||
| * Returns `undefined` when no route provider is registered or the current location matches no route. | ||
| */ | ||
| export function resolveCurrentRoute(client: Client | undefined = getClient()): string | undefined { | ||
| const provider = getRouteProvider(client); | ||
|
|
||
| return provider && callProvider(() => provider.resolveCurrentRoute()); | ||
| } | ||
|
|
||
| /** | ||
| * Builds a {@link RouteProvider} for a router whose location is the browser's, which covers every | ||
| * router except memory and hash routers. | ||
| */ | ||
| export function createUrlRouteProvider(resolveRouteFromUrl: (url: URL) => string | undefined): RouteProvider { | ||
| return { | ||
| resolveRoute: resolveRouteFromUrl, | ||
| resolveCurrentRoute: () => { | ||
| const urlObject = toURLObject(getLocationHref()); | ||
|
|
||
| return urlObject && resolveRouteFromUrl(urlObject); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * A {@link RouteProvider} that answers from routes it has been told about, rather than by matching. | ||
| */ | ||
| export interface CachedRouteProvider extends RouteProvider { | ||
| /** Records the route name a router reported for a path. Ignores empty values. */ | ||
| record(pathname: string | undefined, routeName: string | null | undefined): void; | ||
| } | ||
|
|
||
| /** | ||
| * Builds a route provider for a router with no usable matcher, which can only report the route it is | ||
| * on as it gets there (SvelteKit's `page.route.id`, Solid Router's current matches). | ||
| * | ||
| * A URL the app has not visited resolves to `undefined`, which includes the first pageload until the | ||
| * router reports. Backed by an LRU so a long-lived app visiting many URLs can't grow it without end, | ||
| * and so routes that keep being resolved outlive ones passed through once. | ||
| */ | ||
| export function createCachedRouteProvider(maxEntries: number = 50): CachedRouteProvider { | ||
| const routeNames = new LRUMap<string, string>(maxEntries); | ||
|
|
||
| return { | ||
| ...createUrlRouteProvider(url => routeNames.get(url.pathname)), | ||
| record(pathname, routeName) { | ||
| if (pathname && routeName) { | ||
| routeNames.set(pathname, routeName); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Normalizes to a real `URL` so providers never have to parse, and relative locations (which memory | ||
| * routers hand around) resolve against the document. | ||
| */ | ||
| function toURLObject(url: string): URL | undefined { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: I think we already have a helper like this in core 🤔 |
||
| try { | ||
| return new URL(url, getLocationHref() || undefined); | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Route providers are framework code we don't control, so a throw must not take down whatever the SDK | ||
| * was naming. | ||
| */ | ||
| function callProvider(resolve: () => string | undefined): string | undefined { | ||
| try { | ||
| return resolve() || undefined; | ||
| } catch (error) { | ||
| DEBUG_BUILD && debug.warn('Route provider threw while resolving a route:', error); | ||
| return undefined; | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l: As long as we're planning on adding the provider only to browser-side SDKs, can we move this to
browser-utils? Francesco is working on moving all browser-specific exports over, so I think we can take the shortcut and add it to utils right away.