Add plugin metadata to route object to simplify navigation tracking.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-09-16 21:58:32 +02:00
parent ae4680b88d
commit 0bf2112f9d
5 changed files with 58 additions and 56 deletions
+3 -3
View File
@@ -23,7 +23,7 @@ import React, {
useMemo,
useState,
} from 'react';
import { createRoutesFromChildren, Route, Routes } from 'react-router-dom';
import { Route, Routes } from 'react-router-dom';
import { useAsync } from 'react-use';
import {
ApiProvider,
@@ -368,7 +368,7 @@ export class PrivateAppImpl implements BackstageApp {
return (
<RouterComponent>
<RouteTracker objects={createRoutesFromChildren(children)} />
<RouteTracker tree={children} />
<Routes>
<Route path={mountPath} element={<>{children}</>} />
</Routes>
@@ -378,7 +378,7 @@ export class PrivateAppImpl implements BackstageApp {
return (
<RouterComponent>
<RouteTracker objects={createRoutesFromChildren(children)} />
<RouteTracker tree={children} />
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={mountPath} element={<>{children}</>} />
@@ -14,70 +14,51 @@
* limitations under the License.
*/
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import { matchRoutes, useLocation } from 'react-router-dom';
import {
createRoutesFromChildren,
matchRoutes,
useLocation,
} from 'react-router-dom';
import {
BackstagePlugin,
useAnalytics,
getComponentData,
AnalyticsContext,
CommonAnalyticsContext,
RouteRef,
} from '@backstage/core-plugin-api';
type RouteObjects = ReturnType<typeof createRoutesFromChildren>;
import { routeObjectCollector } from './collectors';
import {
childDiscoverer,
routeElementDiscoverer,
traverseElementTree,
} from '../extensions/traversal';
import { BackstageRouteObject } from './types';
/**
* Returns an extension context given the current pathname and a RouteObject
* that defines all registered routes in react.
*
* If no exact match is found, path parts are stripped away, one-by-one, until
* a parent-level path matches a route.
* Returns an extension context given the current pathname and a list of
* Backstage route objects.
*/
const getExtensionContext = (
pathname: string,
routes: RouteObjects,
routes: BackstageRouteObject[],
): CommonAnalyticsContext | {} => {
try {
const cleanPath = pathname.replace(/\/+$/, '');
const matches = matchRoutes(routes, { pathname });
const RouteElement = matches
?.filter(match => {
const pathsMatch = match.pathname.replace(/\/+$/, '') === cleanPath;
const hasRoutableElement = !!(match.route.element as React.ReactElement)
?.props?.element;
return pathsMatch && hasRoutableElement;
})
.pop()?.route?.element;
const RoutableElement = (RouteElement as React.ReactElement)?.props
?.element;
const routeObject = matches
?.filter(
match => (match?.route as BackstageRouteObject).routeRefs?.size > 0,
)
.pop()?.route as BackstageRouteObject;
if (RoutableElement) {
const plugin: BackstagePlugin | undefined = getComponentData(
RoutableElement,
'core.plugin',
);
const mountPoint: { id?: string } | undefined = getComponentData(
RoutableElement,
'core.mountPoint',
);
if (plugin && mountPoint) {
return {
pluginId: plugin.getId(),
extension: 'App',
routeRef: mountPoint?.id || '',
};
}
if (!routeObject) {
return {};
}
// Try again, one path-level shallower.
const nextLevelPath = cleanPath.split('/').slice(0, -1).join('/');
return nextLevelPath !== ''
? getExtensionContext(nextLevelPath, routes)
: {};
const routeRef = routeObject.routeRefs.values().next().value as
| RouteRef
| undefined;
return {
extension: 'App',
pluginId: routeObject.plugin?.getId() || 'root',
routeRef: (routeRef as { id?: string }).id,
};
} catch {
return {};
}
@@ -108,12 +89,20 @@ const TrackNavigation = ({
* Logs a "navigate" event with appropriate plugin-level analytics context
* attributes each time the user navigates to a page.
*/
export const RouteTracker = ({ objects }: { objects: RouteObjects }) => {
export const RouteTracker = ({ tree }: { tree: React.ReactNode }) => {
const { pathname, search, hash } = useLocation();
const attributes = getExtensionContext(pathname, objects);
const { routeObjects } = useMemo(() => {
return traverseElementTree({
root: tree,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routeObjects: routeObjectCollector,
},
});
}, [tree]);
return (
<AnalyticsContext attributes={attributes}>
<AnalyticsContext attributes={getExtensionContext(pathname, routeObjects)}>
<TrackNavigation pathname={pathname} search={search} hash={hash} />
</AnalyticsContext>
);
@@ -32,6 +32,7 @@ import {
createPlugin,
RouteRef,
attachComponentData,
BackstagePlugin,
} from '@backstage/core-plugin-api';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
@@ -98,6 +99,7 @@ function routeObj(
refs: RouteRef[],
children: any[] = [],
type: 'mounted' | 'gathered' = 'mounted',
backstagePlugin?: BackstagePlugin,
) {
return {
path: path,
@@ -113,6 +115,7 @@ function routeObj(
},
...children,
],
plugin: backstagePlugin,
};
}
@@ -186,7 +189,7 @@ describe('discovery', () => {
routeObj('/blop', [ref5]),
],
),
routeObj('/divsoup', [ref4]),
routeObj('/divsoup', [ref4], undefined, undefined, plugin),
]);
});
@@ -15,7 +15,11 @@
*/
import { isValidElement, ReactElement, ReactNode } from 'react';
import { RouteRef, getComponentData } from '@backstage/core-plugin-api';
import {
RouteRef,
getComponentData,
BackstagePlugin,
} from '@backstage/core-plugin-api';
import { BackstageRouteObject } from './types';
import { createCollector } from '../extensions/traversal';
import { FeatureFlagged, FeatureFlaggedProps } from './FeatureFlagged';
@@ -141,6 +145,10 @@ export const routeObjectCollector = createCollector(
element: 'mounted',
routeRefs: new Set([routeRef]),
children: [MATCH_ALL_ROUTE],
plugin: getComponentData<BackstagePlugin>(
node.props.element,
'core.plugin',
),
};
parentChildren.push(newObject);
return newObject;
@@ -18,6 +18,7 @@ import {
RouteRef,
SubRouteRef,
ExternalRouteRef,
BackstagePlugin,
} from '@backstage/core-plugin-api';
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
@@ -53,6 +54,7 @@ export interface BackstageRouteObject {
element: React.ReactNode;
path: string;
routeRefs: Set<RouteRef>;
plugin?: BackstagePlugin;
}
export function isRouteRef<Params extends AnyParams>(