From 3fe92995fd5bac066ea0210fca5744fb4928dfce Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Jan 2024 09:18:20 +0100 Subject: [PATCH 01/11] feat(analytics-module-ga4): add support to the new analytics api Signed-off-by: Camila Belo --- plugins/analytics-module-ga4/package.json | 1 + .../AnalyticsApi/GoogleAnalytics4.test.ts | 38 +++++++++++++++++++ .../AnalyticsApi/GoogleAnalytics4.ts | 21 +++++++--- yarn.lock | 1 + 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json index 97b610c2bf..89113efe6d 100644 --- a/plugins/analytics-module-ga4/package.json +++ b/plugins/analytics-module-ga4/package.json @@ -32,6 +32,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "react-ga4": "^2.0.0" }, "peerDependencies": { diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts index 16db7075b3..5bae530b2b 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts @@ -492,4 +492,42 @@ describe('GoogleAnalytics4', () => { }); }); }); + + describe('Backward compatibility', () => { + it('fallback category for legacy context extension property', () => { + const api = GoogleAnalytics4.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + expect(fnEvent).toHaveBeenCalledWith('page_view', { + action: 'page_view', + label: '/', + category: 'App', + }); + }); + + it('prioritize new context extension id over old extension property', () => { + const api = GoogleAnalytics4.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context: { ...context, extensionId: 'App', extension: '' }, + }); + + expect(fnEvent).toHaveBeenCalledWith('page_view', { + action: 'page_view', + label: '/', + category: 'App', + }); + }); + }); }); diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts index 9abf4719c6..3d2e633962 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import ReactGA from 'react-ga4'; import { AnalyticsApi, @@ -21,6 +22,11 @@ import { AnalyticsEvent, IdentityApi, } from '@backstage/core-plugin-api'; +import { + AnalyticsApi as NewAnalyticsApi, + AnalyticsEvent as NewAnalyticsEvent, + AnalyticsContextValue as NewAnalyticsContextValue, +} from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { DeferredCapture } from '../../../util/DeferredCapture'; @@ -28,7 +34,7 @@ import { DeferredCapture } from '../../../util/DeferredCapture'; * Google Analytics API provider for the Backstage Analytics API. * @public */ -export class GoogleAnalytics4 implements AnalyticsApi { +export class GoogleAnalytics4 implements AnalyticsApi, NewAnalyticsApi { private readonly customUserIdTransform?: ( userEntityRef: string, ) => Promise; @@ -157,17 +163,20 @@ export class GoogleAnalytics4 implements AnalyticsApi { * applied as they should be (set on pageview, merged object on events). * @param event - AnalyticsEvent type captured */ - captureEvent(event: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) { const { context, action, subject, value, attributes } = event; const customEventData = this.setEventParameters(context, attributes); if (this.contentGroupBy) { customEventData.content_group = context[this.contentGroupBy]!; } - if (action === 'navigate' && context.extension === 'App') { + const extensionId = context.extensionId || context.extension; + const category = extensionId ? String(extensionId) : 'App'; + + if (action === 'navigate' && category === 'App') { this.capture.event( { - category: context.extension || 'App', + category, action: 'page_view', label: subject, value, @@ -183,7 +192,7 @@ export class GoogleAnalytics4 implements AnalyticsApi { this.capture.event( { - category: context.extension || 'App', + category, action, label: subject, value, @@ -199,7 +208,7 @@ export class GoogleAnalytics4 implements AnalyticsApi { * @param attributes additional analytics event attributes */ private setEventParameters( - context: AnalyticsContextValue, + context: AnalyticsContextValue | NewAnalyticsContextValue, attributes: AnalyticsEventAttributes = {}, ) { const customEventParameters: { diff --git a/yarn.lock b/yarn.lock index 79d27b5501..5f26cb9a82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4312,6 +4312,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@testing-library/dom": ^9.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 From 87f3d69dc50bdd5af49c02153a4c7a86daa9bab2 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Jan 2024 09:19:27 +0100 Subject: [PATCH 02/11] fix(frontend-app-api): add global analytics context to the app extension Signed-off-by: Camila Belo --- packages/frontend-app-api/src/extensions/App.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/frontend-app-api/src/extensions/App.tsx b/packages/frontend-app-api/src/extensions/App.tsx index 751a255fa7..82e7600178 100644 --- a/packages/frontend-app-api/src/extensions/App.tsx +++ b/packages/frontend-app-api/src/extensions/App.tsx @@ -14,7 +14,9 @@ * limitations under the License. */ +import React from 'react'; import { + ExtensionBoundary, coreExtensionData, createApiExtension, createComponentExtension, @@ -50,9 +52,13 @@ export const App = createExtension({ output: { root: coreExtensionData.reactElement, }, - factory({ inputs }) { + factory({ node, inputs }) { return { - root: inputs.root.output.element, + root: ( + + {inputs.root.output.element} + + ), }; }, }); From 01abfa9c2b5884c2ccbd1deb3aa729fe6202a3e8 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Jan 2024 09:22:26 +0100 Subject: [PATCH 03/11] feat(frontend-plugin-api): fix default app plugin id Signed-off-by: Camila Belo --- .../frontend-plugin-api/src/analytics/AnalyticsContext.tsx | 2 +- packages/frontend-plugin-api/src/analytics/types.ts | 3 --- .../frontend-plugin-api/src/components/ExtensionBoundary.tsx | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx index 10ef475803..2c022ad8e9 100644 --- a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx @@ -37,7 +37,7 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { // Provide a default value if no value exists. if (theContext === undefined) { return { - pluginId: 'root', + pluginId: 'app', extensionId: 'App', }; } diff --git a/packages/frontend-plugin-api/src/analytics/types.ts b/packages/frontend-plugin-api/src/analytics/types.ts index 21347e2fc7..3e3a9ad5eb 100644 --- a/packages/frontend-plugin-api/src/analytics/types.ts +++ b/packages/frontend-plugin-api/src/analytics/types.ts @@ -25,9 +25,6 @@ export type CommonAnalyticsContext = { */ pluginId: string; - /** - * The nearest known parent extension where the event was captured. - */ /** * The nearest known parent extension where the event was captured. */ diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx index 9a8eb5b750..768352b76e 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.tsx @@ -65,7 +65,7 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) { // Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight const attributes = { extensionId: node.spec.id, - pluginId: node.spec.source?.id, + pluginId: node.spec.source?.id ?? 'app', }; return ( From b1afb710b8cf23c17c112d4161e983419509b547 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 25 Jan 2024 11:04:20 +0100 Subject: [PATCH 04/11] fix(frontend-plugin-api): default api config error Signed-off-by: Camila Belo --- packages/core-plugin-api/package.json | 1 + .../src/apis/system/useApi.tsx | 11 +++++--- .../src/routing/RouteTracker.test.tsx | 8 +++--- .../src/analytics/AnalyticsContext.test.tsx | 10 +++---- .../src/analytics/AnalyticsContext.tsx | 2 +- .../src/analytics/useAnalytics.test.tsx | 4 +-- .../src/analytics/useAnalytics.tsx | 7 +++-- .../AnalyticsApi/GoogleAnalytics4.test.ts | 26 ++++++++++++++++--- .../AnalyticsApi/GoogleAnalytics4.ts | 5 ++-- yarn.lock | 1 + 10 files changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index d676260227..68d5ad68c3 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", "@backstage/types": "workspace:^", "@backstage/version-bridge": "workspace:^", "@types/react": "^16.13.1 || ^17.0.0", diff --git a/packages/core-plugin-api/src/apis/system/useApi.tsx b/packages/core-plugin-api/src/apis/system/useApi.tsx index 03c4fa33ac..2bb1532525 100644 --- a/packages/core-plugin-api/src/apis/system/useApi.tsx +++ b/packages/core-plugin-api/src/apis/system/useApi.tsx @@ -17,6 +17,7 @@ import React, { PropsWithChildren } from 'react'; import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { useVersionedContext } from '@backstage/version-bridge'; +import { NotImplementedError } from '@backstage/errors'; /** * React hook for retrieving {@link ApiHolder}, an API catalog. @@ -26,12 +27,12 @@ import { useVersionedContext } from '@backstage/version-bridge'; export function useApiHolder(): ApiHolder { const versionedHolder = useVersionedContext<{ 1: ApiHolder }>('api-context'); if (!versionedHolder) { - throw new Error('API context is not available'); + throw new NotImplementedError('API context is not available'); } const apiHolder = versionedHolder.atVersion(1); if (!apiHolder) { - throw new Error('ApiContext v1 not available'); + throw new NotImplementedError('ApiContext v1 not available'); } return apiHolder; } @@ -47,7 +48,7 @@ export function useApi(apiRef: ApiRef): T { const api = apiHolder.get(apiRef); if (!api) { - throw new Error(`No implementation available for ${apiRef}`); + throw new NotImplementedError(`No implementation available for ${apiRef}`); } return api; } @@ -73,7 +74,9 @@ export function withApis(apis: TypesToApiRefs) { const api = apiHolder.get(ref); if (!api) { - throw new Error(`No implementation available for ${ref}`); + throw new NotImplementedError( + `No implementation available for ${ref}`, + ); } impls[key] = api; } diff --git a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx index 0e66d0e50d..935449b697 100644 --- a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx @@ -211,8 +211,8 @@ describe('RouteTracker', () => { action: 'navigate', attributes: {}, context: { - extensionId: 'App', - pluginId: 'root', + extensionId: 'app', + pluginId: 'app', }, subject: '/not-routable-extension', value: undefined, @@ -221,8 +221,8 @@ describe('RouteTracker', () => { action: 'click', attributes: undefined, context: { - extensionId: 'App', - pluginId: 'root', + extensionId: 'app', + pluginId: 'app', }, subject: 'test', value: undefined, diff --git a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx index 8e78bdc8c4..bdfdef3005 100644 --- a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx +++ b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.test.tsx @@ -35,8 +35,8 @@ describe('AnalyticsContext', () => { it('returns default values', () => { const { result } = renderHook(() => useAnalyticsContext()); expect(result.current).toEqual({ - extensionId: 'App', - pluginId: 'root', + extensionId: 'app', + pluginId: 'app', }); }); }); @@ -49,8 +49,8 @@ describe('AnalyticsContext', () => { , ); - expect(result.getByTestId('extension-id')).toHaveTextContent('App'); - expect(result.getByTestId('plugin-id')).toHaveTextContent('root'); + expect(result.getByTestId('extension-id')).toHaveTextContent('app'); + expect(result.getByTestId('plugin-id')).toHaveTextContent('app'); }); it('uses provided analytics context', () => { @@ -60,7 +60,7 @@ describe('AnalyticsContext', () => { , ); - expect(result.getByTestId('extension-id')).toHaveTextContent('App'); + expect(result.getByTestId('extension-id')).toHaveTextContent('app'); expect(result.getByTestId('plugin-id')).toHaveTextContent('custom'); }); diff --git a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx index 2c022ad8e9..13047b41d7 100644 --- a/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/frontend-plugin-api/src/analytics/AnalyticsContext.tsx @@ -38,7 +38,7 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { if (theContext === undefined) { return { pluginId: 'app', - extensionId: 'App', + extensionId: 'app', }; } diff --git a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx index beb16b1fbb..6dbcc81f11 100644 --- a/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx +++ b/packages/frontend-plugin-api/src/analytics/useAnalytics.test.tsx @@ -53,8 +53,8 @@ describe('useAnalytics', () => { some: 'value', }, context: { - extensionId: 'App', - pluginId: 'root', + extensionId: 'app', + pluginId: 'app', }, }); }); diff --git a/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx b/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx index f3b5998de4..397906d1d1 100644 --- a/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx +++ b/packages/frontend-plugin-api/src/analytics/useAnalytics.tsx @@ -23,8 +23,11 @@ import { Tracker } from './Tracker'; function useAnalyticsApi(): AnalyticsApi { try { return useApi(analyticsApiRef); - } catch { - return { captureEvent: () => {} }; + } catch (error) { + if (error.name === 'NotImplementedError') { + return { captureEvent: () => {} }; + } + throw error; } } diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts index 5bae530b2b..93a6e6fe6a 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts @@ -493,8 +493,8 @@ describe('GoogleAnalytics4', () => { }); }); - describe('Backward compatibility', () => { - it('fallback category for legacy context extension property', () => { + describe('api backward compatibility', () => { + it('continue working with legacy App category', () => { const api = GoogleAnalytics4.fromConfig(basicValidConfig); expect(api.captureEvent).toBeDefined(); @@ -512,6 +512,24 @@ describe('GoogleAnalytics4', () => { }); }); + it('use lowercase app as the new default category', () => { + const api = GoogleAnalytics4.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context: { ...context, extensionId: '', extension: '' }, + }); + + expect(fnEvent).toHaveBeenCalledWith('page_view', { + action: 'page_view', + label: '/', + category: 'app', + }); + }); + it('prioritize new context extension id over old extension property', () => { const api = GoogleAnalytics4.fromConfig(basicValidConfig); @@ -520,13 +538,13 @@ describe('GoogleAnalytics4', () => { api.captureEvent({ action: 'navigate', subject: '/', - context: { ...context, extensionId: 'App', extension: '' }, + context: { ...context, extensionId: 'app', extension: '' }, }); expect(fnEvent).toHaveBeenCalledWith('page_view', { action: 'page_view', label: '/', - category: 'App', + category: 'app', }); }); }); diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts index 3d2e633962..1f4cbfeee5 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts @@ -171,9 +171,10 @@ export class GoogleAnalytics4 implements AnalyticsApi, NewAnalyticsApi { } const extensionId = context.extensionId || context.extension; - const category = extensionId ? String(extensionId) : 'App'; + const category = extensionId ? String(extensionId) : 'app'; - if (action === 'navigate' && category === 'App') { + // The legacy default extension was 'App' and the new one is 'app' + if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { this.capture.event( { category, diff --git a/yarn.lock b/yarn.lock index 5f26cb9a82..efac8bb495 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3896,6 +3896,7 @@ __metadata: "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/errors": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" "@backstage/version-bridge": "workspace:^" From 405702ba8b04d6065b609897a5dbbe2480ebe739 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Jan 2024 09:22:21 +0100 Subject: [PATCH 05/11] feat: add api backward compatibility for more analytics modules Signed-off-by: Camila Belo --- plugins/analytics-module-ga/package.json | 1 + .../AnalyticsApi/GoogleAnalytics.test.ts | 59 +++++++++++++++++++ .../AnalyticsApi/GoogleAnalytics.ts | 19 ++++-- .../package.json | 1 + .../AnalyticsApi/NewRelicBrowser.ts | 15 ++++- yarn.lock | 2 + 6 files changed, 89 insertions(+), 8 deletions(-) diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 3d367a5889..cabff881ad 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -32,6 +32,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "react-ga": "^3.3.0" }, "peerDependencies": { diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index 65e9256aa8..1380f9c715 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -508,4 +508,63 @@ describe('GoogleAnalytics', () => { expect(lastData.queueTime).toBeUndefined(); }); }); + + describe('api backward compatibility', () => { + it('continue working with legacy App category', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + }); + + it('use lowercase app as the new default category', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context: { ...context, extensionId: '', extension: '' }, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + }); + + it('prioritize new context extension id over old extension property', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + + expect(api.captureEvent).toBeDefined(); + + api.captureEvent({ + action: 'navigate', + subject: '/', + context: { ...context, extensionId: 'app', extension: '' }, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + }); + }); }); diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 17c5b26be1..045bc94f3a 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -22,6 +22,11 @@ import { AnalyticsEventAttributes, IdentityApi, } from '@backstage/core-plugin-api'; +import { + AnalyticsApi as NewAnalyticsApi, + AnalyticsEvent as NewAnalyticsEvent, + AnalyticsContextValue as NewAnalyticsContextValue, +} from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { DeferredCapture } from '../../../util'; import { @@ -40,7 +45,7 @@ type CustomDimensionOrMetricConfig = { * Google Analytics API provider for the Backstage Analytics API. * @public */ -export class GoogleAnalytics implements AnalyticsApi { +export class GoogleAnalytics implements AnalyticsApi, NewAnalyticsApi { private readonly cdmConfig: CustomDimensionOrMetricConfig[]; private customUserIdTransform?: (userEntityRef: string) => Promise; private readonly capture: DeferredCapture; @@ -157,11 +162,15 @@ export class GoogleAnalytics implements AnalyticsApi { * pageview and the rest as custom events. All custom dimensions/metrics are * applied as they should be (set on pageview, merged object on events). */ - captureEvent(event: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) { const { context, action, subject, value, attributes } = event; const customMetadata = this.getCustomDimensionMetrics(context, attributes); - if (action === 'navigate' && context.extension === 'App') { + const extensionId = context.extensionId || context.extension; + const category = extensionId ? String(extensionId) : 'app'; + + // The legacy default extension was 'App' and the new one is 'app' + if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { this.capture.pageview(subject, customMetadata); return; } @@ -184,7 +193,7 @@ export class GoogleAnalytics implements AnalyticsApi { } this.capture.event({ - category: context.extension || 'App', + category, action, label: subject, value, @@ -197,7 +206,7 @@ export class GoogleAnalytics implements AnalyticsApi { * Event Attributes, e.g. { dimension1: "some value", metric8: 42 } */ private getCustomDimensionMetrics( - context: AnalyticsContextValue, + context: AnalyticsContextValue | NewAnalyticsContextValue, attributes: AnalyticsEventAttributes = {}, ) { const customDimensionsMetrics: { [x: string]: string | number | boolean } = diff --git a/plugins/analytics-module-newrelic-browser/package.json b/plugins/analytics-module-newrelic-browser/package.json index 0815b62b6c..44610dd026 100644 --- a/plugins/analytics-module-newrelic-browser/package.json +++ b/plugins/analytics-module-newrelic-browser/package.json @@ -26,6 +26,7 @@ "@backstage/config": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", + "@backstage/frontend-plugin-api": "workspace:^", "@newrelic/browser-agent": "^1.236.0" }, "peerDependencies": { diff --git a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index f2f35b5765..ba04aea322 100644 --- a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -19,6 +19,10 @@ import { IdentityApi, AnalyticsEvent, } from '@backstage/core-plugin-api'; +import { + AnalyticsApi as NewAnalyicsApi, + AnalyticsEvent as NewAnalyticsEvent, +} from '@backstage/frontend-plugin-api'; import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'; import type { setAPI } from '@newrelic/browser-agent/loaders/api/api'; @@ -37,7 +41,7 @@ type NewRelicBrowserOptions = { * New Relic Browser API provider for the Backstage Analytics API. * @public */ -export class NewRelicBrowser implements AnalyticsApi { +export class NewRelicBrowser implements AnalyticsApi, NewAnalyicsApi { private readonly agent: NewRelicAPI; private constructor( @@ -121,9 +125,14 @@ export class NewRelicBrowser implements AnalyticsApi { ); } - captureEvent(event: AnalyticsEvent) { + captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) { const { context, action, subject, value, attributes } = event; - if (action === 'navigate' && context.extension === 'App') { + + const extensionId = context.extensionId || context.extension; + const category = extensionId ? String(extensionId) : 'app'; + + // The legacy default extension was 'App' and the new one is 'app' + if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { const interaction = this.agent.interaction(); interaction.setName(subject); if (value) { diff --git a/yarn.lock b/yarn.lock index efac8bb495..41ceb79882 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4336,6 +4336,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@testing-library/dom": ^9.0.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 @@ -4357,6 +4358,7 @@ __metadata: "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" + "@backstage/frontend-plugin-api": "workspace:^" "@newrelic/browser-agent": ^1.236.0 "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^14.0.0 From e586f797026c86e44a16a7585cc7e71a4332c521 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Jan 2024 09:39:32 +0100 Subject: [PATCH 06/11] docs: add changeset files Signed-off-by: Camila Belo --- .changeset/blue-keys-do.md | 5 +++++ .changeset/gorgeous-pumas-draw.md | 7 +++++++ .changeset/old-papayas-shave.md | 5 +++++ .changeset/shaggy-trainers-rule.md | 5 +++++ 4 files changed, 22 insertions(+) create mode 100644 .changeset/blue-keys-do.md create mode 100644 .changeset/gorgeous-pumas-draw.md create mode 100644 .changeset/old-papayas-shave.md create mode 100644 .changeset/shaggy-trainers-rule.md diff --git a/.changeset/blue-keys-do.md b/.changeset/blue-keys-do.md new file mode 100644 index 0000000000..c933bac0f2 --- /dev/null +++ b/.changeset/blue-keys-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-plugin-api': minor +--- + +**BREAKING**: Replace default plugin extension and plugin ids to be `app` instead of `root`. diff --git a/.changeset/gorgeous-pumas-draw.md b/.changeset/gorgeous-pumas-draw.md new file mode 100644 index 0000000000..d2ae0370e5 --- /dev/null +++ b/.changeset/gorgeous-pumas-draw.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-analytics-module-newrelic-browser': minor +'@backstage/plugin-analytics-module-ga4': minor +'@backstage/plugin-analytics-module-ga': minor +--- + +**BREAKING**: Add support to the new analytics api and use `app` as default extension and plugin ids. It is breaking only because you will start seeing app as the new default id for extensions and plugins events. diff --git a/.changeset/old-papayas-shave.md b/.changeset/old-papayas-shave.md new file mode 100644 index 0000000000..1372b9ce85 --- /dev/null +++ b/.changeset/old-papayas-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/frontend-app-api': patch +--- + +Wrap the root element with the analytics context to ensure it always exists for all extensions. diff --git a/.changeset/shaggy-trainers-rule.md b/.changeset/shaggy-trainers-rule.md new file mode 100644 index 0000000000..4140a400e1 --- /dev/null +++ b/.changeset/shaggy-trainers-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': patch +--- + +Throw a more specific exception `NotImplementedError` when an API implementation cannot be found. From e92f21f27a964b4677d53a66b10ddacd4f8cbebc Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Jan 2024 10:12:04 +0100 Subject: [PATCH 07/11] docs: update api reports Signed-off-by: Camila Belo --- plugins/analytics-module-ga/api-report.md | 6 ++++-- plugins/analytics-module-ga4/api-report.md | 6 ++++-- plugins/analytics-module-newrelic-browser/api-report.md | 6 ++++-- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md index 1846211e72..5404248938 100644 --- a/plugins/analytics-module-ga/api-report.md +++ b/plugins/analytics-module-ga/api-report.md @@ -4,7 +4,9 @@ ```ts import { AnalyticsApi } from '@backstage/core-plugin-api'; +import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { Config } from '@backstage/config'; import { IdentityApi } from '@backstage/core-plugin-api'; @@ -13,8 +15,8 @@ import { IdentityApi } from '@backstage/core-plugin-api'; export const analyticsModuleGA: BackstagePlugin<{}, {}>; // @public -export class GoogleAnalytics implements AnalyticsApi { - captureEvent(event: AnalyticsEvent): void; +export class GoogleAnalytics implements AnalyticsApi, AnalyticsApi_2 { + captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void; static fromConfig( config: Config, options?: { diff --git a/plugins/analytics-module-ga4/api-report.md b/plugins/analytics-module-ga4/api-report.md index 77f3094210..b8282e834a 100644 --- a/plugins/analytics-module-ga4/api-report.md +++ b/plugins/analytics-module-ga4/api-report.md @@ -4,13 +4,15 @@ ```ts import { AnalyticsApi } from '@backstage/core-plugin-api'; +import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { IdentityApi } from '@backstage/core-plugin-api'; // @public -export class GoogleAnalytics4 implements AnalyticsApi { - captureEvent(event: AnalyticsEvent): void; +export class GoogleAnalytics4 implements AnalyticsApi, AnalyticsApi_2 { + captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void; static fromConfig( config: Config, options?: { diff --git a/plugins/analytics-module-newrelic-browser/api-report.md b/plugins/analytics-module-newrelic-browser/api-report.md index 7556bae317..09fbf8f0f3 100644 --- a/plugins/analytics-module-newrelic-browser/api-report.md +++ b/plugins/analytics-module-newrelic-browser/api-report.md @@ -4,14 +4,16 @@ ```ts import { AnalyticsApi } from '@backstage/core-plugin-api'; +import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { Config } from '@backstage/config'; import { IdentityApi } from '@backstage/core-plugin-api'; // @public -export class NewRelicBrowser implements AnalyticsApi { +export class NewRelicBrowser implements AnalyticsApi, AnalyticsApi_2 { // (undocumented) - captureEvent(event: AnalyticsEvent): void; + captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void; // (undocumented) static fromConfig( config: Config, From 915773bf0a28f4bc9889e4ba95f39a39755666b6 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 26 Jan 2024 13:37:35 +0100 Subject: [PATCH 08/11] test(analytics-module-ga): apply review suggestion Signed-off-by: Camila Belo --- .../AnalyticsApi/GoogleAnalytics.test.ts | 80 +++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index 1380f9c715..056efdb3e6 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -521,12 +521,30 @@ describe('GoogleAnalytics', () => { context, }); - const [command, data] = ReactGA.testModeAPI.calls[1]; + let [command, data] = ReactGA.testModeAPI.calls[1]; expect(command).toBe('send'); expect(data).toMatchObject({ hitType: 'pageview', page: '/', }); + + api.captureEvent({ + action: 'click', + subject: 'on something', + value: 42, + context, + }); + + [command, data] = ReactGA.testModeAPI.calls[2]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'event', + // expect to use the legacy default category + eventCategory: 'App', + eventAction: 'click', + eventLabel: 'on something', + eventValue: 42, + }); }); it('use lowercase app as the new default category', () => { @@ -537,15 +555,41 @@ describe('GoogleAnalytics', () => { api.captureEvent({ action: 'navigate', subject: '/', - context: { ...context, extensionId: '', extension: '' }, + context: { + ...context, + extensionId: '', + extension: '', + }, }); - const [command, data] = ReactGA.testModeAPI.calls[1]; + let [command, data] = ReactGA.testModeAPI.calls[1]; expect(command).toBe('send'); expect(data).toMatchObject({ hitType: 'pageview', page: '/', }); + + api.captureEvent({ + action: 'click', + subject: 'on something', + value: 42, + context: { + ...context, + extensionId: '', + extension: '', + }, + }); + + [command, data] = ReactGA.testModeAPI.calls[2]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'event', + // expect to use the new default category + eventCategory: 'app', + eventAction: 'click', + eventLabel: 'on something', + eventValue: 42, + }); }); it('prioritize new context extension id over old extension property', () => { @@ -556,15 +600,41 @@ describe('GoogleAnalytics', () => { api.captureEvent({ action: 'navigate', subject: '/', - context: { ...context, extensionId: 'app', extension: '' }, + context: { + ...context, + extensionId: 'app', + extension: '', + }, }); - const [command, data] = ReactGA.testModeAPI.calls[1]; + let [command, data] = ReactGA.testModeAPI.calls[1]; expect(command).toBe('send'); expect(data).toMatchObject({ hitType: 'pageview', page: '/', }); + + api.captureEvent({ + action: 'click', + subject: 'on something', + value: 42, + context: { + ...context, + extensionId: 'page:index', + extension: '', + }, + }); + + [command, data] = ReactGA.testModeAPI.calls[2]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'event', + // expect use the new context extension id + eventCategory: 'page:index', + eventAction: 'click', + eventLabel: 'on something', + eventValue: 42, + }); }); }); }); From b5b9b8d98ae74e5db0867f8dfe3902e74d675591 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 11:30:33 +0100 Subject: [PATCH 09/11] fix: analytics catagory locale Signed-off-by: Camila Belo Co-authored-by: Patrik Oldsberg --- .../implementations/AnalyticsApi/GoogleAnalytics.test.ts | 2 +- .../apis/implementations/AnalyticsApi/GoogleAnalytics.ts | 7 +++++-- .../implementations/AnalyticsApi/GoogleAnalytics4.test.ts | 2 +- .../apis/implementations/AnalyticsApi/GoogleAnalytics4.ts | 7 +++++-- .../apis/implementations/AnalyticsApi/NewRelicBrowser.ts | 7 +++++-- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts index 056efdb3e6..48e43c2a45 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -585,7 +585,7 @@ describe('GoogleAnalytics', () => { expect(data).toMatchObject({ hitType: 'event', // expect to use the new default category - eventCategory: 'app', + eventCategory: 'App', eventAction: 'click', eventLabel: 'on something', eventValue: 42, diff --git a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts index 045bc94f3a..d241cd9860 100644 --- a/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts +++ b/plugins/analytics-module-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -167,10 +167,13 @@ export class GoogleAnalytics implements AnalyticsApi, NewAnalyticsApi { const customMetadata = this.getCustomDimensionMetrics(context, attributes); const extensionId = context.extensionId || context.extension; - const category = extensionId ? String(extensionId) : 'app'; + const category = extensionId ? String(extensionId) : 'App'; // The legacy default extension was 'App' and the new one is 'app' - if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { + if ( + action === 'navigate' && + category.toLocaleLowerCase('en-US').startsWith('app') + ) { this.capture.pageview(subject, customMetadata); return; } diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts index 93a6e6fe6a..025462903b 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.test.ts @@ -526,7 +526,7 @@ describe('GoogleAnalytics4', () => { expect(fnEvent).toHaveBeenCalledWith('page_view', { action: 'page_view', label: '/', - category: 'app', + category: 'App', }); }); diff --git a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts index 1f4cbfeee5..43ff47b84e 100644 --- a/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts +++ b/plugins/analytics-module-ga4/src/apis/implementations/AnalyticsApi/GoogleAnalytics4.ts @@ -171,10 +171,13 @@ export class GoogleAnalytics4 implements AnalyticsApi, NewAnalyticsApi { } const extensionId = context.extensionId || context.extension; - const category = extensionId ? String(extensionId) : 'app'; + const category = extensionId ? String(extensionId) : 'App'; // The legacy default extension was 'App' and the new one is 'app' - if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { + if ( + action === 'navigate' && + category.toLocaleLowerCase('en-US').startsWith('app') + ) { this.capture.event( { category, diff --git a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts index ba04aea322..d65d02db64 100644 --- a/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts +++ b/plugins/analytics-module-newrelic-browser/src/apis/implementations/AnalyticsApi/NewRelicBrowser.ts @@ -129,10 +129,13 @@ export class NewRelicBrowser implements AnalyticsApi, NewAnalyicsApi { const { context, action, subject, value, attributes } = event; const extensionId = context.extensionId || context.extension; - const category = extensionId ? String(extensionId) : 'app'; + const category = extensionId ? String(extensionId) : 'App'; // The legacy default extension was 'App' and the new one is 'app' - if (action === 'navigate' && category.toLocaleLowerCase() === 'app') { + if ( + action === 'navigate' && + category.toLocaleLowerCase('en-US').startsWith('app') + ) { const interaction = this.agent.interaction(); interaction.setName(subject); if (value) { From 311dda9e18df096cbffdcecaad08f628d3d154cf Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 11:31:46 +0100 Subject: [PATCH 10/11] feat: add backward compatibility for core anlytics api implementations Signed-off-by: Camila Belo --- .changeset/blue-keys-do.md | 2 +- .changeset/gorgeous-pumas-draw.md | 2 +- packages/frontend-plugin-api/api-report.md | 16 ++++ .../AnalyticsApi/MultipleAnalyticsApi.test.ts | 64 ++++++++++++++++ .../AnalyticsApi/MultipleAnalyticsApi.ts | 76 +++++++++++++++++++ .../AnalyticsApi/NoOpAnalyticsApi.ts | 30 ++++++++ .../implementations/AnalyticsApi/index.ts | 18 +++++ .../src/apis/implementations/index.ts | 20 +++++ .../frontend-plugin-api/src/apis/index.ts | 1 + 9 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts create mode 100644 packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts create mode 100644 packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts create mode 100644 packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts create mode 100644 packages/frontend-plugin-api/src/apis/implementations/index.ts diff --git a/.changeset/blue-keys-do.md b/.changeset/blue-keys-do.md index c933bac0f2..81e79a7975 100644 --- a/.changeset/blue-keys-do.md +++ b/.changeset/blue-keys-do.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': minor --- -**BREAKING**: Replace default plugin extension and plugin ids to be `app` instead of `root`. +**BREAKING**: Replace default plugin extension and plugin ids to be `app` instead of `root` and update the `MultipleAnalyticsApi` to support new Analytics API context type . diff --git a/.changeset/gorgeous-pumas-draw.md b/.changeset/gorgeous-pumas-draw.md index d2ae0370e5..c5b2214829 100644 --- a/.changeset/gorgeous-pumas-draw.md +++ b/.changeset/gorgeous-pumas-draw.md @@ -4,4 +4,4 @@ '@backstage/plugin-analytics-module-ga': minor --- -**BREAKING**: Add support to the new analytics api and use `app` as default extension and plugin ids. It is breaking only because you will start seeing app as the new default id for extensions and plugins events. +Add support to the new analytics api. diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 4116469c2a..21d329fae8 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -8,6 +8,8 @@ import { AlertApi } from '@backstage/core-plugin-api'; import { alertApiRef } from '@backstage/core-plugin-api'; import { AlertMessage } from '@backstage/core-plugin-api'; +import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/core-plugin-api'; +import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/core-plugin-api'; import { AnyApiFactory } from '@backstage/core-plugin-api'; import { AnyApiRef } from '@backstage/core-plugin-api'; import { ApiFactory } from '@backstage/core-plugin-api'; @@ -993,6 +995,20 @@ export { identityApiRef }; export { microsoftAuthApiRef }; +// @public +export class MultipleAnalyticsApi implements AnalyticsApi_2, AnalyticsApi { + captureEvent(event: AnalyticsEvent_2 | AnalyticsEvent): void; + static fromApis( + actualApis: (AnalyticsApi_2 | AnalyticsApi)[], + ): MultipleAnalyticsApi; +} + +// @public +export class NoOpAnalyticsApi implements AnalyticsApi_2, AnalyticsApi { + // (undocumented) + captureEvent(_event: AnalyticsEvent_2 | AnalyticsEvent): void; +} + export { OAuthApi }; export { OAuthRequestApi }; diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts new file mode 100644 index 0000000000..8095ee0020 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { MultipleAnalyticsApi } from './MultipleAnalyticsApi'; + +describe('MultipleAnalyticsApi', () => { + const analyticsApiOne = { captureEvent: jest.fn() }; + const analyticsApiTwo = { captureEvent: jest.fn() }; + const multipleApis = MultipleAnalyticsApi.fromApis([ + analyticsApiOne, + analyticsApiTwo, + ]); + + const event = { + action: 'navivate', + subject: '/path', + context: { + extension: 'App', + pluginId: 'plugin', + routeRef: 'unknown', + }, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('forwards events to all apis', () => { + // When an event is captured + multipleApis.captureEvent(event); + + // Then both underlying APIs should have received the event + expect(analyticsApiOne.captureEvent).toHaveBeenCalledTimes(1); + expect(analyticsApiOne.captureEvent).toHaveBeenCalledWith(event); + expect(analyticsApiTwo.captureEvent).toHaveBeenCalledTimes(1); + expect(analyticsApiTwo.captureEvent).toHaveBeenCalledWith(event); + }); + + it('forwards events to all apis even if one throws an error', () => { + // Given one underlying API that throws on capture + analyticsApiOne.captureEvent.mockImplementation(() => { + throw new Error('!!!'); + }); + + // When an event is captured + multipleApis.captureEvent(event); + + // Then the other underlying API should have still received the event + expect(analyticsApiTwo.captureEvent).toHaveBeenCalledTimes(1); + expect(analyticsApiTwo.captureEvent).toHaveBeenCalledWith(event); + }); +}); diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts new file mode 100644 index 0000000000..b99607a5fb --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; +import { + AnalyticsApi as NewAnalyicsApi, + AnalyticsEvent as NewAnalyicsEvent, +} from '../../definitions'; + +/** + * An implementation of the AnalyticsApi that can be used to forward analytics + * events to multiple concrete implementations. + * + * @public + * + * @example + * + * ```jsx + * createApiFactory({ + * api: analyticsApiRef, + * deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, + * factory: ({ configApi, identityApi, storageApi }) => + * MultipleAnalyticsApi.fromApis([ + * VendorAnalyticsApi.fromConfig(configApi, { identityApi }), + * CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), + * ]), + * }); + * ``` + */ +export class MultipleAnalyticsApi implements AnalyticsApi, NewAnalyicsApi { + private constructor( + private readonly actualApis: (AnalyticsApi | NewAnalyicsApi)[], + ) {} + + /** + * Create an AnalyticsApi implementation from an array of concrete + * implementations. + * + * @example + * + * ```jsx + * MultipleAnalyticsApi.fromApis([ + * SomeAnalyticsApi.fromConfig(configApi), + * new CustomAnalyticsApi(), + * ]); + * ``` + */ + static fromApis(actualApis: (AnalyticsApi | NewAnalyicsApi)[]) { + return new MultipleAnalyticsApi(actualApis); + } + + /** + * Forward the event to all configured analytics API implementations. + */ + captureEvent(event: AnalyticsEvent | NewAnalyicsEvent): void { + this.actualApis.forEach(analyticsApi => { + try { + analyticsApi.captureEvent(event as AnalyticsEvent & NewAnalyicsEvent); + } catch { + /* ignored */ + } + }); + } +} diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts new file mode 100644 index 0000000000..b64c31afaf --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; +import { + AnalyticsApi as NewAnalyicsApi, + AnalyticsEvent as NewAnalyicsEvent, +} from '../../definitions'; + +/** + * Base implementation for the AnalyticsApi that does nothing. + * + * @public + */ +export class NoOpAnalyticsApi implements AnalyticsApi, NewAnalyicsApi { + captureEvent(_event: AnalyticsEvent | NewAnalyicsEvent): void {} +} diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts new file mode 100644 index 0000000000..c9e2008516 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MultipleAnalyticsApi } from './MultipleAnalyticsApi'; +export { NoOpAnalyticsApi } from './NoOpAnalyticsApi'; diff --git a/packages/frontend-plugin-api/src/apis/implementations/index.ts b/packages/frontend-plugin-api/src/apis/implementations/index.ts new file mode 100644 index 0000000000..b3ec1c8513 --- /dev/null +++ b/packages/frontend-plugin-api/src/apis/implementations/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This folder contains implementations for all core APIs. +// Plugins should rely on these APIs for functionality as much as possible. + +export * from './AnalyticsApi'; diff --git a/packages/frontend-plugin-api/src/apis/index.ts b/packages/frontend-plugin-api/src/apis/index.ts index 5def6cb0f9..983497271f 100644 --- a/packages/frontend-plugin-api/src/apis/index.ts +++ b/packages/frontend-plugin-api/src/apis/index.ts @@ -15,4 +15,5 @@ */ export * from './definitions'; +export * from './implementations'; export * from './system'; From 4bc1ca5522726264a238d1fbd8fd7d5098b4251c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Tue, 30 Jan 2024 13:11:39 +0100 Subject: [PATCH 11/11] refactor: move core analytics apis to the compat package Signed-off-by: Camila Belo --- .changeset/blue-keys-do.md | 2 +- .changeset/gorgeous-pumas-draw.md | 1 + packages/core-compat-api/api-report.md | 18 ++++++++++++++++++ .../AnalyticsApi/MultipleAnalyticsApi.test.ts | 0 .../AnalyticsApi/MultipleAnalyticsApi.ts | 2 +- .../AnalyticsApi/NoOpAnalyticsApi.ts | 2 +- .../apis/implementations/AnalyticsApi/index.ts | 0 .../src/apis/implementations/index.ts | 5 +---- packages/core-compat-api/src/apis/index.ts | 17 +++++++++++++++++ packages/core-compat-api/src/index.ts | 2 ++ packages/frontend-plugin-api/api-report.md | 16 ---------------- packages/frontend-plugin-api/src/apis/index.ts | 1 - 12 files changed, 42 insertions(+), 24 deletions(-) rename packages/{frontend-plugin-api => core-compat-api}/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts (100%) rename packages/{frontend-plugin-api => core-compat-api}/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts (98%) rename packages/{frontend-plugin-api => core-compat-api}/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts (96%) rename packages/{frontend-plugin-api => core-compat-api}/src/apis/implementations/AnalyticsApi/index.ts (100%) rename packages/{frontend-plugin-api => core-compat-api}/src/apis/implementations/index.ts (77%) create mode 100644 packages/core-compat-api/src/apis/index.ts diff --git a/.changeset/blue-keys-do.md b/.changeset/blue-keys-do.md index 81e79a7975..c933bac0f2 100644 --- a/.changeset/blue-keys-do.md +++ b/.changeset/blue-keys-do.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': minor --- -**BREAKING**: Replace default plugin extension and plugin ids to be `app` instead of `root` and update the `MultipleAnalyticsApi` to support new Analytics API context type . +**BREAKING**: Replace default plugin extension and plugin ids to be `app` instead of `root`. diff --git a/.changeset/gorgeous-pumas-draw.md b/.changeset/gorgeous-pumas-draw.md index c5b2214829..2e94c61c63 100644 --- a/.changeset/gorgeous-pumas-draw.md +++ b/.changeset/gorgeous-pumas-draw.md @@ -2,6 +2,7 @@ '@backstage/plugin-analytics-module-newrelic-browser': minor '@backstage/plugin-analytics-module-ga4': minor '@backstage/plugin-analytics-module-ga': minor +'@backstage/core-compat-api': minor --- Add support to the new analytics api. diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index a7aae2b5b9..be80ca50e9 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -3,6 +3,10 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnalyticsApi } from '@backstage/core-plugin-api'; +import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; +import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api'; @@ -51,6 +55,20 @@ export function convertLegacyRouteRefs< [KName in keyof TRefs]: ToNewRouteRef; }; +// @public +export class MultipleAnalyticsApi implements AnalyticsApi, AnalyticsApi_2 { + captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void; + static fromApis( + actualApis: (AnalyticsApi | AnalyticsApi_2)[], + ): MultipleAnalyticsApi; +} + +// @public +export class NoOpAnalyticsApi implements AnalyticsApi, AnalyticsApi_2 { + // (undocumented) + captureEvent(_event: AnalyticsEvent | AnalyticsEvent_2): void; +} + // @public export type ToNewRouteRef = T extends RouteRef diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts similarity index 100% rename from packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts rename to packages/core-compat-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts similarity index 98% rename from packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts rename to packages/core-compat-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts index b99607a5fb..888474877d 100644 --- a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts +++ b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts @@ -17,7 +17,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; import { AnalyticsApi as NewAnalyicsApi, AnalyticsEvent as NewAnalyicsEvent, -} from '../../definitions'; +} from '@backstage/frontend-plugin-api'; /** * An implementation of the AnalyticsApi that can be used to forward analytics diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts similarity index 96% rename from packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts rename to packages/core-compat-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts index b64c31afaf..ad57258fdd 100644 --- a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts +++ b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/NoOpAnalyticsApi.ts @@ -18,7 +18,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; import { AnalyticsApi as NewAnalyicsApi, AnalyticsEvent as NewAnalyicsEvent, -} from '../../definitions'; +} from '@backstage/frontend-plugin-api'; /** * Base implementation for the AnalyticsApi that does nothing. diff --git a/packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts b/packages/core-compat-api/src/apis/implementations/AnalyticsApi/index.ts similarity index 100% rename from packages/frontend-plugin-api/src/apis/implementations/AnalyticsApi/index.ts rename to packages/core-compat-api/src/apis/implementations/AnalyticsApi/index.ts diff --git a/packages/frontend-plugin-api/src/apis/implementations/index.ts b/packages/core-compat-api/src/apis/implementations/index.ts similarity index 77% rename from packages/frontend-plugin-api/src/apis/implementations/index.ts rename to packages/core-compat-api/src/apis/implementations/index.ts index b3ec1c8513..82fc567355 100644 --- a/packages/frontend-plugin-api/src/apis/implementations/index.ts +++ b/packages/core-compat-api/src/apis/implementations/index.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * Copyright 2024 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,4 @@ * limitations under the License. */ -// This folder contains implementations for all core APIs. -// Plugins should rely on these APIs for functionality as much as possible. - export * from './AnalyticsApi'; diff --git a/packages/core-compat-api/src/apis/index.ts b/packages/core-compat-api/src/apis/index.ts new file mode 100644 index 0000000000..dd4cc2a930 --- /dev/null +++ b/packages/core-compat-api/src/apis/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './implementations'; diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index b632bf8a57..88e1892eac 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -15,6 +15,8 @@ */ export * from './compatWrapper'; +export * from './apis'; + export { convertLegacyApp } from './convertLegacyApp'; export { convertLegacyRouteRef, diff --git a/packages/frontend-plugin-api/api-report.md b/packages/frontend-plugin-api/api-report.md index 21d329fae8..4116469c2a 100644 --- a/packages/frontend-plugin-api/api-report.md +++ b/packages/frontend-plugin-api/api-report.md @@ -8,8 +8,6 @@ import { AlertApi } from '@backstage/core-plugin-api'; import { alertApiRef } from '@backstage/core-plugin-api'; import { AlertMessage } from '@backstage/core-plugin-api'; -import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/core-plugin-api'; -import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/core-plugin-api'; import { AnyApiFactory } from '@backstage/core-plugin-api'; import { AnyApiRef } from '@backstage/core-plugin-api'; import { ApiFactory } from '@backstage/core-plugin-api'; @@ -995,20 +993,6 @@ export { identityApiRef }; export { microsoftAuthApiRef }; -// @public -export class MultipleAnalyticsApi implements AnalyticsApi_2, AnalyticsApi { - captureEvent(event: AnalyticsEvent_2 | AnalyticsEvent): void; - static fromApis( - actualApis: (AnalyticsApi_2 | AnalyticsApi)[], - ): MultipleAnalyticsApi; -} - -// @public -export class NoOpAnalyticsApi implements AnalyticsApi_2, AnalyticsApi { - // (undocumented) - captureEvent(_event: AnalyticsEvent_2 | AnalyticsEvent): void; -} - export { OAuthApi }; export { OAuthRequestApi }; diff --git a/packages/frontend-plugin-api/src/apis/index.ts b/packages/frontend-plugin-api/src/apis/index.ts index 983497271f..5def6cb0f9 100644 --- a/packages/frontend-plugin-api/src/apis/index.ts +++ b/packages/frontend-plugin-api/src/apis/index.ts @@ -15,5 +15,4 @@ */ export * from './definitions'; -export * from './implementations'; export * from './system';