Handle navigate events with multiple gathered extensions
Signed-off-by: Eric Peterson <i.am@eric.pe>
This commit is contained in:
@@ -51,12 +51,15 @@ const getExtensionContext = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If there is a single route ref, return it.
|
||||
// todo: get routeRef of rendered gathered mount point(?)
|
||||
// If there is a single route ref, use it.
|
||||
let routeRef: RouteRef | undefined;
|
||||
let plugin: BackstagePlugin | 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;
|
||||
}
|
||||
|
||||
@@ -73,6 +76,7 @@ const getExtensionContext = (
|
||||
extension: 'App',
|
||||
pluginId: plugin?.getId() || 'root',
|
||||
...(routeRef ? { routeRef: (routeRef as { id?: string }).id } : {}),
|
||||
_routeNodeType: routeObject.element as string,
|
||||
params,
|
||||
};
|
||||
} catch {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
import {
|
||||
AnalyticsApi,
|
||||
AnalyticsEventAttributes,
|
||||
@@ -21,6 +22,42 @@ import {
|
||||
} from '../apis';
|
||||
import { AnalyticsContextValue } from './';
|
||||
|
||||
type TempGlobalEvents = {
|
||||
/**
|
||||
* Stores the most recent "gathered" mountpoint navigation.
|
||||
*/
|
||||
mostRecentGatheredNavigation?: {
|
||||
action: string;
|
||||
subject: string;
|
||||
value?: number;
|
||||
attributes?: AnalyticsEventAttributes;
|
||||
context: AnalyticsContextValue;
|
||||
};
|
||||
/**
|
||||
* Stores the most recent routable extension render.
|
||||
*/
|
||||
mostRecentRoutableExtensionRender?: {
|
||||
context: AnalyticsContextValue;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Temporary global store for select event data. Used to make `navigate` events
|
||||
* more accurate when gathered mountpoints are used.
|
||||
*/
|
||||
const globalEvents = getOrCreateGlobalSingleton<TempGlobalEvents>(
|
||||
'core-plugin-api:analytics-tracker-events',
|
||||
() => ({
|
||||
mostRecentGatheredNavigation: undefined,
|
||||
mostRecentRoutableExtensionRender: undefined,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Internal-only event representing when a routable extension is rendered.
|
||||
*/
|
||||
export const routableExtensionRenderedEvent = '_ROUTABLE-EXTENSION-RENDERED';
|
||||
|
||||
export class Tracker implements AnalyticsTracker {
|
||||
constructor(
|
||||
private readonly analyticsApi: AnalyticsApi,
|
||||
@@ -43,13 +80,65 @@ export class Tracker implements AnalyticsTracker {
|
||||
attributes,
|
||||
}: { value?: number; attributes?: AnalyticsEventAttributes } = {},
|
||||
) {
|
||||
// Never pass internal "_routeNodeType" context value.
|
||||
const { _routeNodeType, ...context } = this.context;
|
||||
|
||||
// Never fire the special "_routable-extension-rendered" internal event.
|
||||
if (action === routableExtensionRenderedEvent) {
|
||||
// Instead, set it on the global store.
|
||||
globalEvents.mostRecentRoutableExtensionRender = {
|
||||
context: {
|
||||
...context,
|
||||
extension: 'App',
|
||||
},
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are about to fire a real event, and we have an un-fired gathered
|
||||
// mountpoint navigation on the global store, we need to fire the navigate
|
||||
// event first, so this real event happens accurately after the navigation.
|
||||
if (globalEvents.mostRecentGatheredNavigation) {
|
||||
try {
|
||||
this.analyticsApi.captureEvent({
|
||||
...globalEvents.mostRecentGatheredNavigation,
|
||||
...globalEvents.mostRecentRoutableExtensionRender,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Error during analytics event capture. %o', e);
|
||||
}
|
||||
|
||||
// Clear the global stores.
|
||||
globalEvents.mostRecentGatheredNavigation = undefined;
|
||||
globalEvents.mostRecentRoutableExtensionRender = undefined;
|
||||
}
|
||||
|
||||
// Never directly fire a navigation event on a gathered route with default
|
||||
// contextual details.
|
||||
if (
|
||||
action === 'navigate' &&
|
||||
_routeNodeType === 'gathered' &&
|
||||
context.pluginId === 'root'
|
||||
) {
|
||||
// Instead, set it on the global store.
|
||||
globalEvents.mostRecentGatheredNavigation = {
|
||||
action,
|
||||
subject,
|
||||
value,
|
||||
attributes,
|
||||
context,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.analyticsApi.captureEvent({
|
||||
action,
|
||||
subject,
|
||||
value,
|
||||
attributes,
|
||||
context: this.context,
|
||||
context,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -14,14 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { AnalyticsContext } from '../analytics';
|
||||
import React, { lazy, Suspense, useEffect } from 'react';
|
||||
import { AnalyticsContext, useAnalytics } from '../analytics';
|
||||
import { useApp } from '../app';
|
||||
import { RouteRef, useRouteRef } from '../routing';
|
||||
import { attachComponentData } from './componentData';
|
||||
import { Extension, BackstagePlugin } from '../plugin';
|
||||
import { PluginErrorBoundary } from './PluginErrorBoundary';
|
||||
import { PluginProvider } from '../plugin-options';
|
||||
import { routableExtensionRenderedEvent } from '../analytics/Tracker';
|
||||
|
||||
/**
|
||||
* Lazy or synchronous retrieving of extension components.
|
||||
@@ -82,6 +83,8 @@ export function createRoutableExtension<
|
||||
component().then(
|
||||
InnerComponent => {
|
||||
const RoutableExtensionWrapper: any = (props: any) => {
|
||||
const analytics = useAnalytics();
|
||||
|
||||
// Validate that the routing is wired up correctly in the App.tsx
|
||||
try {
|
||||
useRouteRef(mountPoint);
|
||||
@@ -101,6 +104,15 @@ export function createRoutableExtension<
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// This event, never exposed to end-users of the analytics API,
|
||||
// helps inform which extension metadata gets associated with a
|
||||
// navigation event when the route navigated to is a gathered
|
||||
// mountpoint.
|
||||
useEffect(() => {
|
||||
analytics.captureEvent(routableExtensionRenderedEvent, '');
|
||||
}, [analytics]);
|
||||
|
||||
return <InnerComponent {...props} />;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user