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:^"