From 99eea585ce26ece84358d3b84f7b421c12402523 Mon Sep 17 00:00:00 2001 From: Alexey Kulakov Date: Sat, 5 Sep 2026 19:14:22 -0700 Subject: [PATCH] fix(ember): Guard `routerService.recognize()` against unrecognizable URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `instrumentEmberAppInstanceForPerformance` calls `routerService.recognize(url)` at three sites without guarding it. `recognize()` throws for URLs the router cannot resolve — most notably it asserts "You must pass a url that begins with the application's rootURL" whenever the URL is not prefixed with the app's `rootURL`, which is the case under Ember's `none` location used by `@ember/test-helpers`. The first call happens inside `browserTracingIntegration`'s `afterAllSetup`, so the assertion propagates out of integration setup and breaks every acceptance test that boots the app. All three call sites already treat a missing `routeInfo` as a fall back to the URL, so route them through a small `_recognizeURL()` helper that catches and returns `undefined`. No intended behavior changes. --- ...nstrumentEmberAppInstanceForPerformance.ts | 23 ++++++++++-- .../tests/instrument-router-location.test.ts | 35 ++++++++++++++++++- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts index 0de3dc47ffa0..3abd359bad21 100644 --- a/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts +++ b/packages/ember/src/utils/instrumentEmberAppInstanceForPerformance.ts @@ -63,7 +63,7 @@ export function instrumentEmberAppInstanceForPerformance( // Somehow the router service etc. may not be fully ready/initialized yet at this point // Probably because we are running this before the Ember setup is necessarily completed // So in order to accomodate this, we fall back to starting the pageload span with the current URL and update it later - const routeInfo = url ? routerService.recognize(url) : undefined; + const routeInfo = url ? _recognizeURL(routerService, url) : undefined; activeRootSpan = startBrowserTracingPageLoadSpan(client, { // With span streaming, span names have to be low cardinality, so we can't fall back to the URL. @@ -129,7 +129,7 @@ export function instrumentEmberAppInstanceForPerformance( const location = getRouterMain(appInstance).location; const url = _getLocationURL(location); if (url) { - const routeInfo = routerService.recognize(url); + const routeInfo = _recognizeURL(routerService, url); activeRootSpan.updateName(`route:${toRoute}`); activeRootSpan.setAttributes({ [SENTRY_SEGMENT_NAME_SOURCE]: 'route', @@ -164,7 +164,7 @@ export function instrumentEmberAppInstanceForPerformance( const url = routerService.currentURL ?? _getLocationURL(location); if (url) { - const routeInfo = routerService.recognize(url); + const routeInfo = _recognizeURL(routerService, url); // `currentURL` is the normalized route path and never includes the hash fragment, so we source // `url.full` from the location URL (which preserves `#/...` for hash-location apps) when available. const fullUrl = _getLocationURL(location) || url; @@ -258,6 +258,23 @@ function _getRouteUrlAttributes( }; } +// Only exported for testing +export function _recognizeURL( + routerService: RouterService, + url: string, +): ReturnType | undefined { + // `recognize()` throws for URLs the router cannot resolve. Most notably it asserts + // "You must pass a url that begins with the application's rootURL" whenever the URL is not + // prefixed with the app's `rootURL`, which is the case under Ember's `none` location (used by + // `@ember/test-helpers`). Every call site already treats a missing `routeInfo` as "fall back + // to the URL", so degrade to that instead of throwing out of the integration's setup. + try { + return routerService.recognize(url); + } catch { + return undefined; + } +} + // Only exported for testing export function _getLocationURL(location: EmberRouterMain['location']): string { if (!location?.getURL || !location?.formatURL) { diff --git a/packages/ember/tests/instrument-router-location.test.ts b/packages/ember/tests/instrument-router-location.test.ts index 8587ca8803b5..554083a4fa12 100644 --- a/packages/ember/tests/instrument-router-location.test.ts +++ b/packages/ember/tests/instrument-router-location.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { _getLocationURL } from '../src/utils/instrumentEmberAppInstanceForPerformance.ts'; +import { _getLocationURL, _recognizeURL } from '../src/utils/instrumentEmberAppInstanceForPerformance.ts'; interface Location { formatURL?: (url: string) => string; @@ -80,3 +80,36 @@ describe('_getLocationURL', () => { expect(_getLocationURL(mockLocation)).toBe(''); }); }); + +type RouterServiceArg = Parameters[0]; + +function mockRouterService(recognize: (url: string) => unknown): RouterServiceArg { + return { recognize } as unknown as RouterServiceArg; +} + +describe('_recognizeURL', () => { + it('returns the route info when the router recognizes the URL', () => { + const routeInfo = { name: 'my.route', params: { id: '1' } }; + const routerService = mockRouterService(() => routeInfo); + + expect(_recognizeURL(routerService, '/my/route/1')).toBe(routeInfo); + }); + + it('returns undefined when the URL is not prefixed with the rootURL', () => { + // Ember's `none` location (used by `@ember/test-helpers`) yields URLs that `recognize()` + // rejects with this assertion, which previously threw out of the integration's setup. + const routerService = mockRouterService(() => { + throw new Error('Assertion Failed: You must pass a url that begins with the application\'s rootURL "/"'); + }); + + expect(_recognizeURL(routerService, 'not-root-url-prefixed')).toBeUndefined(); + }); + + it('returns undefined for any other recognize() failure', () => { + const routerService = mockRouterService(() => { + throw new Error('nope'); + }); + + expect(_recognizeURL(routerService, '/unknown')).toBeUndefined(); + }); +});