From 69b380f0ea42c513014f1d9f3f671526ebe3f7c5 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:05:33 +0100 Subject: [PATCH] fix(ember): Make `implementation` field optional (`hash` routes) --- .../sentry-performance.ts | 8 +- packages/ember/addon/types.ts | 2 +- .../unit/instrument-router-location-test.ts | 100 ++++++++++++++++++ 3 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 packages/ember/tests/unit/instrument-router-location-test.ts diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 666e9f245839..4c5491c6a5a4 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -84,13 +84,15 @@ function getTransitionInformation( }; } -function getLocationURL(location: EmberRouterMain['location']): string { +// Only exported for testing +export function _getLocationURL(location: EmberRouterMain['location']): string { if (!location?.getURL || !location?.formatURL) { return ''; } const url = location.formatURL(location.getURL()); - if (location.implementation === 'hash') { + // `implementation` is optional in Ember's predefined location types, so we also check if the URL starts with '#'. + if (location.implementation === 'hash' || url.startsWith('#')) { return `${location.rootURL}${url}`; } return url; @@ -110,7 +112,7 @@ export function _instrumentEmberRouter( // Maintaining backwards compatibility with config.browserTracingOptions, but passing it with Sentry options is preferred. const browserTracingOptions = config.browserTracingOptions || config.sentry.browserTracingOptions || {}; - const url = getLocationURL(location); + const url = _getLocationURL(location); const client = getClient(); diff --git a/packages/ember/addon/types.ts b/packages/ember/addon/types.ts index a66a290004f0..887fa59b9901 100644 --- a/packages/ember/addon/types.ts +++ b/packages/ember/addon/types.ts @@ -29,7 +29,7 @@ export interface EmberRouterMain { location: { getURL?: () => string; formatURL?: (url: string) => string; - implementation: string; + implementation?: string; rootURL: string; }; } diff --git a/packages/ember/tests/unit/instrument-router-location-test.ts b/packages/ember/tests/unit/instrument-router-location-test.ts new file mode 100644 index 000000000000..16cc95da906a --- /dev/null +++ b/packages/ember/tests/unit/instrument-router-location-test.ts @@ -0,0 +1,100 @@ +import type { EmberRouterMain } from '@sentry/ember/addon/types'; +import { _getLocationURL } from '@sentry/ember/instance-initializers/sentry-performance'; +import { setupTest } from 'ember-qunit'; +import { module, test } from 'qunit'; +import type { SentryTestContext } from '../helpers/setup-sentry'; +import { setupSentryTest } from '../helpers/setup-sentry'; + +module('Unit | Utility | instrument-router-location', function (hooks) { + setupTest(hooks); + setupSentryTest(hooks); + + test('getLocationURL handles hash location without implementation field', function (this: SentryTestContext, assert) { + // This simulates the default Ember HashLocation which doesn't include the implementation field + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '#/test-route', + formatURL: (url: string) => url, + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '/#/test-route', 'Should prepend rootURL to hash URL when implementation is not set'); + }); + + test('_getLocationURL handles hash location with implementation field', function (this: SentryTestContext, assert) { + // This simulates a custom HashLocation with explicit implementation field + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '#/test-route', + formatURL: (url: string) => url, + implementation: 'hash', + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '/#/test-route', 'Should prepend rootURL to hash URL when implementation is hash'); + }); + + test('_getLocationURL handles history location', function (this: SentryTestContext, assert) { + // This simulates a history location + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '/test-route', + formatURL: (url: string) => url, + implementation: 'history', + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '/test-route', 'Should return URL as-is for non-hash locations'); + }); + + test('_getLocationURL handles none location type', function (this: SentryTestContext, assert) { + // This simulates a 'none' location (often used in tests) + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '', + formatURL: (url: string) => url, + implementation: 'none', + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '', 'Should return empty string when URL is empty'); + }); + + test('_getLocationURL handles custom rootURL for hash location', function (this: SentryTestContext, assert) { + // Test with non-root rootURL + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '#/test-route', + formatURL: (url: string) => url, + rootURL: '/my-app/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual( + result, + '/my-app/#/test-route', + 'Should prepend custom rootURL to hash URL when implementation is not set', + ); + }); + + test('_getLocationURL handles location without getURL method', function (this: SentryTestContext, assert) { + // This simulates an incomplete location object + const mockLocation: EmberRouterMain['location'] = { + formatURL: (url: string) => url, + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '', 'Should return empty string when getURL is not available'); + }); + + test('_getLocationURL handles location without formatURL method', function (this: SentryTestContext, assert) { + // This simulates an incomplete location object + const mockLocation: EmberRouterMain['location'] = { + getURL: () => '#/test-route', + rootURL: '/', + }; + + const result = _getLocationURL(mockLocation); + assert.strictEqual(result, '', 'Should return empty string when formatURL is not available'); + }); +});