diff --git a/packages/frontend-app-api/src/extensions/CoreRouter.tsx b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
index f128a246e6..510af04b51 100644
--- a/packages/frontend-app-api/src/extensions/CoreRouter.tsx
+++ b/packages/frontend-app-api/src/extensions/CoreRouter.tsx
@@ -33,6 +33,7 @@ import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations
import { BrowserRouter } from 'react-router-dom';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { signInPageComponentDataRef } from '../../../frontend-plugin-api/src/extensions/createSignInPageExtension';
+import { RouteTracker } from '../routing/RouteTracker';
export const CoreRouter = createExtension({
id: 'core.router',
@@ -132,7 +133,7 @@ export function AppRouter(props: AppRouterProps) {
if (!internalAppContext) {
throw new Error('AppRouter must be rendered within the AppProvider');
}
- const { appIdentityProxy } = internalAppContext;
+ const { routeObjects, appIdentityProxy } = internalAppContext;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
@@ -159,11 +160,17 @@ export function AppRouter(props: AppRouterProps) {
{ signOutTargetUrl: basePath || '/' },
);
- return {children};
+ return (
+
+
+ {children}
+
+ );
}
return (
+
{
plugins: new Set([plugin0]),
caseSensitive: false,
children: [MATCH_ALL_ROUTE],
+ appNode: {
+ spec: { extension: { id: 'home.page.index' }, source: { id: 'home' } },
+ } as AppNode,
},
{
path: '/path/:p1/:p2',
@@ -51,6 +55,12 @@ describe('RouteTracker', () => {
plugins: new Set([plugin1]),
caseSensitive: false,
children: [MATCH_ALL_ROUTE],
+ appNode: {
+ spec: {
+ extension: { id: 'plugin1.page.index' },
+ source: { id: 'plugin1' },
+ },
+ } as AppNode,
},
{
path: '/path2/:param',
@@ -59,6 +69,12 @@ describe('RouteTracker', () => {
plugins: new Set([plugin2]),
caseSensitive: false,
children: [MATCH_ALL_ROUTE],
+ appNode: {
+ spec: {
+ extension: { id: 'plugin2.page.index' },
+ source: { id: 'plugin2' },
+ },
+ } as AppNode,
},
];
@@ -86,9 +102,8 @@ describe('RouteTracker', () => {
p2: 'bar',
},
context: {
- extension: 'App',
+ extensionId: 'plugin1.page.index',
pluginId: 'plugin1',
- routeRef: undefined,
},
subject: '/path/foo/bar',
value: undefined,
@@ -118,9 +133,8 @@ describe('RouteTracker', () => {
param: 'hello',
},
context: {
- extension: 'App',
+ extensionId: 'plugin2.page.index',
pluginId: 'plugin2',
- routeRef: undefined,
},
subject: '/path2/hello',
value: undefined,
@@ -143,9 +157,8 @@ describe('RouteTracker', () => {
p2: 'bar',
},
context: {
- extension: 'App',
+ extensionId: 'plugin1.page.index',
pluginId: 'plugin1',
- routeRef: undefined,
},
subject: '/path/foo/bar?q=1#header-1',
value: undefined,
@@ -165,16 +178,15 @@ describe('RouteTracker', () => {
action: 'navigate',
attributes: {},
context: {
- extension: 'App',
+ extensionId: 'home.page.index',
pluginId: 'home',
- routeRef: undefined,
},
subject: '/',
value: undefined,
});
});
- it('should return default context when it would have otherwise matched on the root path', async () => {
+ it.skip('should return default context when it would have otherwise matched on the root path', async () => {
render(
@@ -195,7 +207,6 @@ describe('RouteTracker', () => {
context: {
extension: 'App',
pluginId: 'root',
- routeRef: 'unknown',
},
subject: '/not-routable-extension',
value: undefined,
@@ -217,9 +228,8 @@ describe('RouteTracker', () => {
param: 'param-value',
},
context: {
- extension: 'App',
+ extensionId: 'plugin2.page.index',
pluginId: 'plugin2',
- routeRef: undefined,
},
subject: '/path2/param-value/sub-route',
value: undefined,
diff --git a/packages/frontend-app-api/src/routing/RouteTracker.tsx b/packages/frontend-app-api/src/routing/RouteTracker.tsx
index a25a12fae2..7bceb05cf9 100644
--- a/packages/frontend-app-api/src/routing/RouteTracker.tsx
+++ b/packages/frontend-app-api/src/routing/RouteTracker.tsx
@@ -16,7 +16,6 @@
import React, { useEffect } from 'react';
import { matchRoutes, useLocation } from 'react-router-dom';
-import { RouteRef, BackstagePlugin } from '@backstage/core-plugin-api';
import {
useAnalytics,
AnalyticsContext,
@@ -55,18 +54,6 @@ const getExtensionContext = (
return undefined;
}
- // If there is a single route ref, use it.
- let routeRef: RouteRef | undefined;
- if (routeObject.routeRefs.size === 1) {
- routeRef = routeObject.routeRefs.values().next().value;
- }
-
- // If there is a single plugin, use it.
- let plugin: BackstagePlugin | undefined;
- if (routeObject.plugins.size === 1) {
- plugin = routeObject.plugins.values().next().value;
- }
-
const params = Object.entries(
routeMatch?.params || {},
).reduce((acc, [key, value]) => {
@@ -76,12 +63,13 @@ const getExtensionContext = (
return acc;
}, {});
+ const plugin = routeObject.appNode?.spec.source;
+ const extension = routeObject.appNode?.spec.extension;
+
return {
- extension: 'App',
- pluginId: plugin?.getId() || 'root',
- ...(routeRef ? { routeRef: (routeRef as { id?: string }).id } : {}),
- _routeNodeType: routeObject.element as string,
params,
+ pluginId: plugin?.id || 'root',
+ extensionId: extension?.id || 'App',
};
} catch {
return undefined;
diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts
index 2bfd09a3e8..17afbeb4c9 100644
--- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts
+++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.ts
@@ -84,6 +84,7 @@ export function extractRouteInfoFromAppNode(node: AppNode): {
caseSensitive: false,
children: [MATCH_ALL_ROUTE],
plugins: new Set(),
+ appNode: current,
};
parentChildren.push(currentObj);
diff --git a/packages/frontend-app-api/src/routing/types.ts b/packages/frontend-app-api/src/routing/types.ts
index 5f1ed9be84..05ea73dff8 100644
--- a/packages/frontend-app-api/src/routing/types.ts
+++ b/packages/frontend-app-api/src/routing/types.ts
@@ -15,6 +15,7 @@
*/
import {
+ AppNode,
ExternalRouteRef,
RouteRef,
SubRouteRef,
@@ -35,4 +36,5 @@ export interface BackstageRouteObject {
path: string;
routeRefs: Set;
plugins: Set;
+ appNode?: AppNode;
}
diff --git a/packages/frontend-app-api/src/wiring/InternalAppContext.ts b/packages/frontend-app-api/src/wiring/InternalAppContext.ts
index 805c46a067..81264bd81b 100644
--- a/packages/frontend-app-api/src/wiring/InternalAppContext.ts
+++ b/packages/frontend-app-api/src/wiring/InternalAppContext.ts
@@ -17,10 +17,12 @@
import { createContext } from 'react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppIdentityProxy } from '../../../core-app-api/src/apis/implementations/IdentityApi/AppIdentityProxy';
+import { BackstageRouteObject } from '../routing/types';
export const InternalAppContext = createContext<
| undefined
| {
appIdentityProxy: AppIdentityProxy;
+ routeObjects: BackstageRouteObject[];
}
>(undefined);
diff --git a/packages/frontend-app-api/src/wiring/createApp.tsx b/packages/frontend-app-api/src/wiring/createApp.tsx
index 6f5a1592c2..a1412f1c3c 100644
--- a/packages/frontend-app-api/src/wiring/createApp.tsx
+++ b/packages/frontend-app-api/src/wiring/createApp.tsx
@@ -97,7 +97,6 @@ import { CoreRouter } from '../extensions/CoreRouter';
import { toInternalBackstagePlugin } from '../../../frontend-plugin-api/src/wiring/createPlugin';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { toInternalExtensionOverrides } from '../../../frontend-plugin-api/src/wiring/createExtensionOverrides';
-import { RouteTracker } from '../routing/RouteTracker';
const builtinExtensions = [
Core,
@@ -342,8 +341,9 @@ export function createSpecializedApp(options?: {
-
-
+
{rootEl}
diff --git a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx
index c29ec5b3ca..8e78bdc8c4 100644
--- a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx
+++ b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx
@@ -23,9 +23,8 @@ const AnalyticsSpy = () => {
const context = useAnalyticsContext();
return (
<>
- {context.routeRef}
{context.pluginId}
- {context.extension}
+ {context.extensionId}
{context.custom}
>
);
@@ -36,9 +35,8 @@ describe('AnalyticsContext', () => {
it('returns default values', () => {
const { result } = renderHook(() => useAnalyticsContext());
expect(result.current).toEqual({
- extension: 'App',
+ extensionId: 'App',
pluginId: 'root',
- routeRef: 'unknown',
});
});
});
@@ -51,9 +49,8 @@ describe('AnalyticsContext', () => {
,
);
- expect(result.getByTestId('extension')).toHaveTextContent('App');
+ expect(result.getByTestId('extension-id')).toHaveTextContent('App');
expect(result.getByTestId('plugin-id')).toHaveTextContent('root');
- expect(result.getByTestId('route-ref')).toHaveTextContent('unknown');
});
it('uses provided analytics context', () => {
@@ -63,23 +60,23 @@ describe('AnalyticsContext', () => {
,
);
- expect(result.getByTestId('extension')).toHaveTextContent('App');
+ expect(result.getByTestId('extension-id')).toHaveTextContent('App');
expect(result.getByTestId('plugin-id')).toHaveTextContent('custom');
- expect(result.getByTestId('route-ref')).toHaveTextContent('unknown');
});
it('uses nested analytics context', () => {
const result = render(
-
+
,
);
- expect(result.getByTestId('extension')).toHaveTextContent('AnalyticsSpy');
+ expect(result.getByTestId('extension-id')).toHaveTextContent(
+ 'AnalyticsSpy',
+ );
expect(result.getByTestId('plugin-id')).toHaveTextContent('custom');
- expect(result.getByTestId('route-ref')).toHaveTextContent('unknown');
});
});
});
diff --git a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx
index 105be8e2b5..10ef475803 100644
--- a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx
+++ b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx
@@ -37,9 +37,8 @@ export const useAnalyticsContext = (): AnalyticsContextValue => {
// Provide a default value if no value exists.
if (theContext === undefined) {
return {
- routeRef: 'unknown',
pluginId: 'root',
- extension: 'App',
+ extensionId: 'App',
};
}
diff --git a/packages/frontend-plugin-api/src/analytics/Tracker.test.ts b/packages/frontend-plugin-api/src/analytics/Tracker.test.ts
index e0e3a87f6d..f1670cf9c4 100644
--- a/packages/frontend-plugin-api/src/analytics/Tracker.test.ts
+++ b/packages/frontend-plugin-api/src/analytics/Tracker.test.ts
@@ -19,9 +19,8 @@ import { Tracker, routableExtensionRenderedEvent } from './Tracker';
describe('Tracker', () => {
const defaultContext = {
- routeRef: 'unknown',
pluginId: 'root',
- extension: 'App',
+ extensionId: 'App',
};
const globalEvents = getOrCreateGlobalSingleton(
'core-plugin-api:analytics-tracker-events',
@@ -56,9 +55,8 @@ describe('Tracker', () => {
it('captures simple event with set context', () => {
trackerUnderTest.setContext({
- routeRef: 'catalog:entity',
pluginId: 'catalog',
- extension: 'App',
+ extensionId: 'App',
});
trackerUnderTest.captureEvent('click', 'test 2', {
value: 2,
@@ -72,9 +70,8 @@ describe('Tracker', () => {
value: 2,
attributes: { to: '/page-2' },
context: {
- routeRef: 'catalog:entity',
pluginId: 'catalog',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
@@ -83,10 +80,8 @@ describe('Tracker', () => {
describe('accurate navigate events', () => {
it('never captures _routeNodeType context key on navigate event', () => {
trackerUnderTest.setContext({
- routeRef: 'catalog:entity',
pluginId: 'catalog',
- extension: 'App',
- _routeNodeType: 'mounted',
+ extensionId: 'App',
});
trackerUnderTest.captureEvent('navigate', '/page-3');
@@ -99,7 +94,6 @@ describe('Tracker', () => {
it('never immediately captures navigate event with _routeNodeType "gathered"', () => {
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-4');
@@ -116,15 +110,13 @@ describe('Tracker', () => {
// User navigates to a gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-5');
// A routable extension is rendered with specific plugin context
trackerUnderTest.setContext({
- routeRef: 'some:ref',
pluginId: 'some-plugin',
- extension: 'App',
+ extensionId: 'App',
});
trackerUnderTest.captureEvent(routableExtensionRenderedEvent, '');
@@ -138,9 +130,8 @@ describe('Tracker', () => {
action: 'navigate',
subject: '/page-5',
context: {
- routeRef: 'some:ref',
pluginId: 'some-plugin',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
@@ -150,9 +141,8 @@ describe('Tracker', () => {
action: 'click',
subject: 'test 5',
context: {
- routeRef: 'some:ref',
pluginId: 'some-plugin',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
@@ -162,22 +152,19 @@ describe('Tracker', () => {
// User navigates to a gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-6');
// A routable extension is rendered with specific plugin context
trackerUnderTest.setContext({
- routeRef: 'another:ref',
pluginId: 'another-plugin',
- extension: 'App',
+ extensionId: 'App',
});
trackerUnderTest.captureEvent(routableExtensionRenderedEvent, '');
// User navigates to another gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-6-2');
@@ -188,9 +175,8 @@ describe('Tracker', () => {
action: 'navigate',
subject: '/page-6',
context: {
- routeRef: 'another:ref',
pluginId: 'another-plugin',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
@@ -200,7 +186,6 @@ describe('Tracker', () => {
// User navigates to a gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-7');
@@ -230,16 +215,14 @@ describe('Tracker', () => {
// User navigates to a gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-8');
// A routable extension is rendered with specific plugin context and
// captured via a separate tracker instance.
const anotherTracker = new Tracker(analyticsApiSpy, {
- routeRef: 'the:ref',
pluginId: 'the-plugin',
- extension: 'App',
+ extensionId: 'App',
});
anotherTracker.captureEvent(routableExtensionRenderedEvent, '');
@@ -254,9 +237,8 @@ describe('Tracker', () => {
action: 'navigate',
subject: '/page-8',
context: {
- routeRef: 'the:ref',
pluginId: 'the-plugin',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
@@ -273,15 +255,13 @@ describe('Tracker', () => {
// User navigates to a gathered mountpoint with multiple plugins
trackerUnderTest.setContext({
...defaultContext,
- _routeNodeType: 'gathered',
});
trackerUnderTest.captureEvent('navigate', '/page-9');
// A routable extension is rendered with specific plugin context
trackerUnderTest.setContext({
- routeRef: 'very:ref',
pluginId: 'very-plugin',
- extension: 'ShouldBeReplaced',
+ extensionId: 'ShouldBeReplaced',
});
trackerUnderTest.captureEvent(routableExtensionRenderedEvent, '');
@@ -295,9 +275,8 @@ describe('Tracker', () => {
action: 'navigate',
subject: '/page-9',
context: {
- routeRef: 'very:ref',
pluginId: 'very-plugin',
- extension: 'App',
+ extensionId: 'App',
},
}),
);
diff --git a/packages/frontend-plugin-api/src/analytics/Tracker.ts b/packages/frontend-plugin-api/src/analytics/Tracker.ts
index 130410e2a2..dd0d770354 100644
--- a/packages/frontend-plugin-api/src/analytics/Tracker.ts
+++ b/packages/frontend-plugin-api/src/analytics/Tracker.ts
@@ -68,9 +68,8 @@ export class Tracker implements AnalyticsTracker {
constructor(
private readonly analyticsApi: AnalyticsApi,
private context: AnalyticsContextValue = {
- routeRef: 'unknown',
pluginId: 'root',
- extension: 'App',
+ extensionId: 'App',
},
) {
// Only register a single beforeunload event across all trackers.
@@ -110,7 +109,7 @@ export class Tracker implements AnalyticsTracker {
}: { value?: number; attributes?: AnalyticsEventAttributes } = {},
) {
// Never pass internal "_routeNodeType" context value.
- const { _routeNodeType, ...context } = this.context;
+ const context = this.context;
// Never fire the special "_routable-extension-rendered" internal event.
if (action === routableExtensionRenderedEvent) {
@@ -120,7 +119,7 @@ export class Tracker implements AnalyticsTracker {
globalEvents.mostRecentRoutableExtensionRender = {
context: {
...context,
- extension: 'App',
+ extensionId: 'App',
},
};
}
@@ -148,11 +147,7 @@ export class Tracker implements AnalyticsTracker {
// Never directly fire a navigation event on a gathered route with default
// contextual details.
- if (
- action === 'navigate' &&
- _routeNodeType === 'gathered' &&
- context.pluginId === 'root'
- ) {
+ if (action === 'navigate' && context.pluginId === 'root') {
// Instead, set it on the global store.
globalEvents.mostRecentGatheredNavigation = {
action,
diff --git a/packages/frontend-plugin-api/src/analytics/types.ts b/packages/frontend-plugin-api/src/analytics/types.ts
index ed5dcd588d..21347e2fc7 100644
--- a/packages/frontend-plugin-api/src/analytics/types.ts
+++ b/packages/frontend-plugin-api/src/analytics/types.ts
@@ -26,14 +26,12 @@ export type CommonAnalyticsContext = {
pluginId: string;
/**
- * The ID of the routeRef that was active when the event was captured.
+ * The nearest known parent extension where the event was captured.
*/
- routeRef: string;
-
/**
* The nearest known parent extension where the event was captured.
*/
- extension: string;
+ extensionId: string;
};
/**
diff --git a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx
index e83826991a..630ce0331a 100644
--- a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx
+++ b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx
@@ -58,9 +58,8 @@ describe('useAnalytics', () => {
some: 'value',
},
context: {
- extension: 'App',
+ extensionId: 'App',
pluginId: 'root',
- routeRef: 'unknown',
},
});
});