From 93b5e38f8b9d8331a7584ce52147cedad5426465 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 25 Mar 2025 16:46:10 +0100 Subject: [PATCH 1/5] Initial implementation of an extension-based default analytics API Signed-off-by: Eric Peterson --- .changeset/mira-looking-ostrich.md | 18 ++ .changeset/nej-inte-ostrich.md | 5 + docs/plugins/analytics.md | 30 ++++ packages/frontend-plugin-api/report.api.md | 41 ++++- .../src/apis/definitions/AnalyticsApi.ts | 28 +++ .../src/blueprints/AnalyticsBlueprint.test.ts | 56 ++++++ .../src/blueprints/AnalyticsBlueprint.ts | 55 ++++++ .../src/blueprints/index.ts | 4 + plugins/app/report.api.md | 19 +- plugins/app/src/defaultApis.ts | 13 +- .../app/src/extensions/AnalyticsApi.test.ts | 162 ++++++++++++++++++ plugins/app/src/extensions/AnalyticsApi.ts | 82 +++++++++ 12 files changed, 498 insertions(+), 15 deletions(-) create mode 100644 .changeset/mira-looking-ostrich.md create mode 100644 .changeset/nej-inte-ostrich.md create mode 100644 packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts create mode 100644 packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts create mode 100644 plugins/app/src/extensions/AnalyticsApi.test.ts create mode 100644 plugins/app/src/extensions/AnalyticsApi.ts diff --git a/.changeset/mira-looking-ostrich.md b/.changeset/mira-looking-ostrich.md new file mode 100644 index 0000000000..2d75fe2ce8 --- /dev/null +++ b/.changeset/mira-looking-ostrich.md @@ -0,0 +1,18 @@ +--- +'@backstage/frontend-plugin-api': patch +--- + +Plugins should now use the new `AnalyticsBlueprint` to define and provide concrete analytics implementations. For example: + +```ts +import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; + +const AcmeAnalytics = AnalyticsBlueprint.make({ + name: 'acme-analytics', + params: define => + define({ + deps: { config: configApiRef }, + factory: ({ config }) => AcmeAnalyticsImpl.fromConfig(config), + }), +}); +``` diff --git a/.changeset/nej-inte-ostrich.md b/.changeset/nej-inte-ostrich.md new file mode 100644 index 0000000000..db97567b54 --- /dev/null +++ b/.changeset/nej-inte-ostrich.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app': patch +--- + +The default implementation of the Analytics API now collects and instantiates analytics implementations exposed via `AnalyticsBlueprint` extensions. If no such extensions are discovered, the API continues to do nothing with analytics events fired within Backstage. If multiple such extensions are discovered, every discovered implementation automatically receives analytics events. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 0a6bac81a0..037eb3d92d 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -101,6 +101,24 @@ export const apis: AnyApiFactory[] = [ }, }), ]; + +// Or, when building for the new frontend system: +import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; + +export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: {}, + factory() { + return { + captureEvent: event => { + window._AcmeAnalyticsQ.push(event); + }, + }; + }, + }), +}); ``` In reality, you would likely want to encapsulate instantiation logic and pull @@ -140,6 +158,18 @@ export const apis: AnyApiFactory[] = [ factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), }), ]; + +// Or, when building for the new frontend system: +import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; + +export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), + }), +}); ``` If you are integrating with an analytics service (as opposed to an internal diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 5ea43c7449..30c7d0d612 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -99,7 +99,7 @@ export { alertApiRef }; export { AlertMessage }; -// @public +// @public @deprecated export type AnalyticsApi = { captureEvent(event: AnalyticsEvent): void; }; @@ -107,6 +107,30 @@ export type AnalyticsApi = { // @public export const analyticsApiRef: ApiRef; +// @public +export const AnalyticsBlueprint: ExtensionBlueprint<{ + kind: 'analytics'; + name: undefined; + params: ( + params: AnalyticsImplementationFactory, + ) => ExtensionBlueprintParams>; + output: ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + factory: ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + }; +}>; + // @public export const AnalyticsContext: (options: { attributes: Partial; @@ -135,6 +159,21 @@ export type AnalyticsEventAttributes = { [attribute in string]: string | boolean | number; }; +// @public +export type AnalyticsImplementation = { + captureEvent(event: AnalyticsEvent): void; +}; + +// @public (undocumented) +export type AnalyticsImplementationFactory< + Deps extends { + [name in string]: unknown; + } = {}, +> = { + deps: TypesToApiRefs; + factory(deps: Deps): AnalyticsImplementation; +}; + // @public export type AnalyticsTracker = { captureEvent: ( diff --git a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts index 15b09482b7..9752d0bd6f 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -16,6 +16,7 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; import { AnalyticsContextValue } from '../../analytics/types'; +import type { AnalyticsBlueprint } from '../../blueprints/'; /** * Represents an event worth tracking in an analytics system that could inform @@ -102,6 +103,26 @@ export type AnalyticsTracker = { ) => void; }; +/** + * Analytics implementations are used to track user behavior in a Backstage + * instance. + * + * @remarks + * + * To instrument your App or Plugin, retrieve an analytics tracker using the + * `useAnalytics()` hook. This will return a pre-configured `AnalyticsTracker` + * with relevant methods for instrumentation. + * + * @public + */ +export type AnalyticsImplementation = { + /** + * Primary event handler responsible for compiling and forwarding events to + * an analytics system. + */ + captureEvent(event: AnalyticsEvent): void; +}; + /** * The Analytics API is used to track user behavior in a Backstage instance. * @@ -112,6 +133,8 @@ export type AnalyticsTracker = { * with relevant methods for instrumentation. * * @public + * @deprecated This type is now deprecated and will be removed in an upcoming + * release. Use the {@link AnalyticsImplementation} type instead. */ export type AnalyticsApi = { /** @@ -124,6 +147,11 @@ export type AnalyticsApi = { /** * The API reference of {@link AnalyticsApi}. * + * @remarks + * + * To define a concrete Analytics Implementation, use {@link AnalyticsBlueprint} + * instead. + * * @public */ export const analyticsApiRef: ApiRef = createApiRef({ diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts new file mode 100644 index 0000000000..1618c38335 --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2025 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 { AnalyticsBlueprint } from './AnalyticsBlueprint'; + +describe('AnalyticsBlueprint', () => { + it('should create an extension with sensible defaults', () => { + const factory = { + deps: {}, + factory: () => ({ captureEvent: () => {} }), + }; + + const extension = AnalyticsBlueprint.make({ + params: define => define(factory), + name: 'test', + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "T": undefined, + "attachTo": [ + { + "id": "api:app/analytics", + "input": "analyticsImplementations", + }, + ], + "configSchema": undefined, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "analytics", + "name": "test", + "output": [ + [Function], + ], + "override": [Function], + "toString": [Function], + "version": "v2", + } + `); + }); +}); diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts new file mode 100644 index 0000000000..8bd4c0526e --- /dev/null +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2025 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 { AnalyticsImplementation, TypesToApiRefs } from '../apis'; +import { + createExtensionBlueprint, + createExtensionBlueprintParams, + createExtensionDataRef, +} from '../wiring'; + +/** @public */ +export type AnalyticsImplementationFactory< + Deps extends { [name in string]: unknown } = {}, +> = { + deps: TypesToApiRefs; + factory(deps: Deps): AnalyticsImplementation; +}; + +const factoryDataRef = + createExtensionDataRef().with({ + id: 'core.analytics.factory', + }); + +/** + * Creates analytics implementations. + * + * @public + */ +export const AnalyticsBlueprint = createExtensionBlueprint({ + kind: 'analytics', + attachTo: [{ id: 'api:app/analytics', input: 'analyticsImplementations' }], + output: [factoryDataRef], + dataRefs: { + factory: factoryDataRef, + }, + defineParams: ( + params: AnalyticsImplementationFactory, + ) => createExtensionBlueprintParams(params as AnalyticsImplementationFactory), + *factory(params) { + yield factoryDataRef(params); + }, +}); diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index 047b007226..29aa410e80 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +export { + AnalyticsBlueprint, + type AnalyticsImplementationFactory, +} from './AnalyticsBlueprint'; export { ApiBlueprint } from './ApiBlueprint'; export { AppRootElementBlueprint } from './AppRootElementBlueprint'; export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 70b1e39651..9ddf7df9ed 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AnalyticsImplementationFactory } from '@backstage/frontend-plugin-api'; import { AnyApiFactory } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; import { ApiFactory } from '@backstage/frontend-plugin-api'; @@ -229,8 +230,6 @@ const appPlugin: FrontendPlugin< ) => ExtensionBlueprintParams; }>; 'api:app/analytics': ExtensionDefinition<{ - kind: 'api'; - name: 'analytics'; config: {}; configInput: {}; output: ConfigurableExtensionDataRef< @@ -238,7 +237,21 @@ const appPlugin: FrontendPlugin< 'core.api.factory', {} >; - inputs: {}; + inputs: { + analyticsImplementations: ExtensionInput< + ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >, + { + singleton: false; + optional: false; + } + >; + }; + kind: 'api'; + name: 'analytics'; params: < TApi, TImpl extends TApi, diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index c8f68232d2..b3d130fb09 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -17,7 +17,6 @@ // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { AlertApiForwarder, - NoOpAnalyticsApi, ErrorApiForwarder, ErrorAlerter, GoogleAuth, @@ -40,7 +39,6 @@ import { import { alertApiRef, - analyticsApiRef, errorApiRef, discoveryApiRef, fetchApiRef, @@ -70,6 +68,7 @@ import { IdentityPermissionApi, } from '@backstage/plugin-permission-react'; import { DefaultDialogApi } from './apis/DefaultDialogApi'; +import { AnalyticsApi } from './extensions/AnalyticsApi'; export const apis = [ ApiBlueprint.make({ @@ -102,15 +101,7 @@ export const apis = [ factory: () => new AlertApiForwarder(), }), }), - ApiBlueprint.make({ - name: 'analytics', - params: defineParams => - defineParams({ - api: analyticsApiRef, - deps: {}, - factory: () => new NoOpAnalyticsApi(), - }), - }), + AnalyticsApi, ApiBlueprint.make({ name: 'error', params: defineParams => diff --git a/plugins/app/src/extensions/AnalyticsApi.test.ts b/plugins/app/src/extensions/AnalyticsApi.test.ts new file mode 100644 index 0000000000..814f41b616 --- /dev/null +++ b/plugins/app/src/extensions/AnalyticsApi.test.ts @@ -0,0 +1,162 @@ +/* + * Copyright 2025 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 { createExtensionTester } from '@backstage/frontend-test-utils'; +import { AnalyticsApi } from './AnalyticsApi'; +import { + AnalyticsBlueprint, + AnalyticsImplementation, + ApiBlueprint, + configApiRef, + createExtension, + identityApiRef, +} from '@backstage/frontend-plugin-api'; + +describe('AnalyticsApi', () => { + const mockEvent = { + action: '', + subject: '', + context: { + pluginId: '', + extensionId: '', + routeRef: '', + }, + }; + const captureEventSpy1 = jest.fn(); + const MockImplementation1 = createExtension({ + name: 'mock-implementation-1', + attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, + output: [AnalyticsBlueprint.dataRefs.factory], + factory() { + return [ + AnalyticsBlueprint.dataRefs.factory({ + deps: { configApi: configApiRef }, + factory: deps => ({ + captureEvent: event => captureEventSpy1(event, deps), + }), + }), + ]; + }, + }); + const captureEventSpy2 = jest.fn(); + const MockImplementation2 = createExtension({ + name: 'mock-implementation-2', + attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, + output: [AnalyticsBlueprint.dataRefs.factory], + factory() { + return [ + AnalyticsBlueprint.dataRefs.factory({ + deps: { config: configApiRef, identityApi: identityApiRef }, + factory: deps => ({ + captureEvent: event => captureEventSpy2(event, deps), + }), + }), + ]; + }, + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('wires up a single AnalyticsImplementationFactory', async () => { + // Given the Analytics API and a single mock implementation + const tester = createExtensionTester(AnalyticsApi).add(MockImplementation1); + const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); + + // Then the API's deps should contain the mock implementation's deps. + expect(apiFactory.deps).toMatchObject({ + 'core.config': expect.anything(), + }); + + // And the returned instance should be an Analytics Implementation + const concreteImplementation = apiFactory.factory({ + 'core.config': 'ConfigApiImplementation', + }); + expect(concreteImplementation).toHaveProperty('captureEvent'); + + // When the resulting API's captureEvent method is called + (concreteImplementation as AnalyticsImplementation).captureEvent(mockEvent); + + // Then the mock implementation's captureEvent should have been called + expect(captureEventSpy1.mock.calls[0][0]).toEqual(mockEvent); + + // And the mock's factory should have resolved the expected deps + expect(captureEventSpy1.mock.calls[0][1]).toEqual({ + configApi: 'ConfigApiImplementation', + }); + }); + + it('wires up more than one AnalyticsImplementationFactory', () => { + // Given the Analytics API and two mock implementations + const tester = createExtensionTester(AnalyticsApi) + .add(MockImplementation1) + .add(MockImplementation2); + const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); + + // Then the API's deps should contain the mock implementations' deps. + expect(apiFactory.deps).toMatchObject({ + 'core.config': expect.anything(), + 'core.identity': expect.anything(), + }); + + // And the returned instance should be an Analytics Implementation + const concreteImplementation = apiFactory.factory({ + 'core.config': 'ConfigApiImplementation', + 'core.identity': 'IdentityApiImplementation', + }); + expect(concreteImplementation).toHaveProperty('captureEvent'); + + // When the resulting API's captureEvent method is called + (concreteImplementation as AnalyticsImplementation).captureEvent(mockEvent); + + // Then the 1st mock implementation's captureEvent should have been called + expect(captureEventSpy1.mock.calls[0][0]).toEqual(mockEvent); + + // And the 1st mock's factory should have resolved the expected deps + expect(captureEventSpy1.mock.calls[0][1]).toEqual({ + configApi: 'ConfigApiImplementation', + }); + + // And the 2nd mock implementation's captureEvent should have been called + expect(captureEventSpy2.mock.calls[0][0]).toEqual(mockEvent); + + // And the 2nd mock's factory should have resolved the expected deps + expect(captureEventSpy2.mock.calls[0][1]).toEqual({ + config: 'ConfigApiImplementation', + identityApi: 'IdentityApiImplementation', + }); + }); + + it('works fine with no AnalyticsImplementationFactory instances provided', () => { + // Given the Analytics API and no mock implementations + const tester = createExtensionTester(AnalyticsApi); + const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); + + // Then the API's deps should be empty + expect(apiFactory.deps).toEqual({}); + + // And the returned instance should be an Analytics Implementation + const concreteImplementation = apiFactory.factory({}); + expect(concreteImplementation).toHaveProperty('captureEvent'); + + // Invoking the API's captureEvent method should result in no errors + expect(() => + (concreteImplementation as AnalyticsImplementation).captureEvent( + mockEvent, + ), + ).not.toThrow(); + }); +}); diff --git a/plugins/app/src/extensions/AnalyticsApi.ts b/plugins/app/src/extensions/AnalyticsApi.ts new file mode 100644 index 0000000000..5e64e5abcf --- /dev/null +++ b/plugins/app/src/extensions/AnalyticsApi.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2025 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 { + analyticsApiRef, + AnalyticsBlueprint, + ApiBlueprint, + ApiRef, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; + +export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ + name: 'analytics', + inputs: { + analyticsImplementations: createExtensionInput([ + AnalyticsBlueprint.dataRefs.factory, + ]), + }, + factory(originalFactory, { inputs }) { + // Pull out and aggregate deps from every implementation input into an + // object keyed by the apiRef ID to be passed to this API implementation as + // if they were its own deps. + const aggregatedDeps = inputs.analyticsImplementations + .flatMap>(impls => + Object.values(impls.get(AnalyticsBlueprint.dataRefs.factory).deps), + ) + .reduce<{ [x: string]: ApiRef }>((accum, ref) => { + accum[ref.id] = ref; + return accum; + }, {}); + + return originalFactory(defineParams => + defineParams({ + api: analyticsApiRef, + deps: aggregatedDeps, + factory: analyticsApiDeps => { + const actualApis = inputs.analyticsImplementations + .map(impl => impl.get(AnalyticsBlueprint.dataRefs.factory)) + .map(({ factory, deps }) => + factory( + // Reconstruct a deps argument to pass to this analytics + // implementation factory from those passed into ours. + Object.keys(deps).reduce<{ [x: string]: ApiRef }>( + (accum, dep) => { + accum[dep] = analyticsApiDeps[ + (deps as { [x: string]: ApiRef })[dep].id + ] as ApiRef; + return accum; + }, + {}, + ), + ), + ); + return { + captureEvent: event => { + actualApis.forEach(api => { + try { + api.captureEvent(event); + } catch { + /* ignored */ + } + }); + }, + }; + }, + }), + ); + }, +}); From 23676c1add2098ec928095f54c64d989e534d4bb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 30 Jul 2025 09:20:55 +0200 Subject: [PATCH 2/5] AnalyticsBlueprint -> AnalyticsImplementationBlueprint Signed-off-by: Eric Peterson --- .changeset/mira-looking-ostrich.md | 6 +-- .changeset/nej-inte-ostrich.md | 2 +- docs/plugins/analytics.md | 50 ++++++++++--------- packages/frontend-plugin-api/report.api.md | 48 +++++++++--------- .../src/apis/definitions/AnalyticsApi.ts | 6 +-- ... AnalyticsImplementationBlueprint.test.ts} | 4 +- ...ts => AnalyticsImplementationBlueprint.ts} | 2 +- .../src/blueprints/index.ts | 4 +- .../app/src/extensions/AnalyticsApi.test.ts | 10 ++-- plugins/app/src/extensions/AnalyticsApi.ts | 12 +++-- 10 files changed, 75 insertions(+), 69 deletions(-) rename packages/frontend-plugin-api/src/blueprints/{AnalyticsBlueprint.test.ts => AnalyticsImplementationBlueprint.test.ts} (90%) rename packages/frontend-plugin-api/src/blueprints/{AnalyticsBlueprint.ts => AnalyticsImplementationBlueprint.ts} (95%) diff --git a/.changeset/mira-looking-ostrich.md b/.changeset/mira-looking-ostrich.md index 2d75fe2ce8..fb85b10d0d 100644 --- a/.changeset/mira-looking-ostrich.md +++ b/.changeset/mira-looking-ostrich.md @@ -2,12 +2,12 @@ '@backstage/frontend-plugin-api': patch --- -Plugins should now use the new `AnalyticsBlueprint` to define and provide concrete analytics implementations. For example: +Plugins should now use the new `AnalyticsImplementationBlueprint` to define and provide concrete analytics implementations. For example: ```ts -import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; -const AcmeAnalytics = AnalyticsBlueprint.make({ +const AcmeAnalytics = AnalyticsImplementationBlueprint.make({ name: 'acme-analytics', params: define => define({ diff --git a/.changeset/nej-inte-ostrich.md b/.changeset/nej-inte-ostrich.md index db97567b54..434708e2e4 100644 --- a/.changeset/nej-inte-ostrich.md +++ b/.changeset/nej-inte-ostrich.md @@ -2,4 +2,4 @@ '@backstage/plugin-app': patch --- -The default implementation of the Analytics API now collects and instantiates analytics implementations exposed via `AnalyticsBlueprint` extensions. If no such extensions are discovered, the API continues to do nothing with analytics events fired within Backstage. If multiple such extensions are discovered, every discovered implementation automatically receives analytics events. +The default implementation of the Analytics API now collects and instantiates analytics implementations exposed via `AnalyticsImplementationBlueprint` extensions. If no such extensions are discovered, the API continues to do nothing with analytics events fired within Backstage. If multiple such extensions are discovered, every discovered implementation automatically receives analytics events. diff --git a/docs/plugins/analytics.md b/docs/plugins/analytics.md index 037eb3d92d..f383e8881c 100644 --- a/docs/plugins/analytics.md +++ b/docs/plugins/analytics.md @@ -103,22 +103,23 @@ export const apis: AnyApiFactory[] = [ ]; // Or, when building for the new frontend system: -import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; -export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({ - name: 'acme', - params: define => - define({ - deps: {}, - factory() { - return { - captureEvent: event => { - window._AcmeAnalyticsQ.push(event); - }, - }; - }, - }), -}); +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: {}, + factory() { + return { + captureEvent: event => { + window._AcmeAnalyticsQ.push(event); + }, + }; + }, + }), + }); ``` In reality, you would likely want to encapsulate instantiation logic and pull @@ -160,16 +161,17 @@ export const apis: AnyApiFactory[] = [ ]; // Or, when building for the new frontend system: -import { AnalyticsBlueprint } from '@backstage/frontend-plugin-api'; +import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; -export const acmeAnalyticsImplementation = AnalyticsBlueprint.make({ - name: 'acme', - params: define => - define({ - deps: { configApi: configApiRef }, - factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), - }), -}); +export const acmeAnalyticsImplementation = + AnalyticsImplementationBlueprint.make({ + name: 'acme', + params: define => + define({ + deps: { configApi: configApiRef }, + factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), + }), + }); ``` If you are integrating with an analytics service (as opposed to an internal diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 30c7d0d612..3d65591c57 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -107,30 +107,6 @@ export type AnalyticsApi = { // @public export const analyticsApiRef: ApiRef; -// @public -export const AnalyticsBlueprint: ExtensionBlueprint<{ - kind: 'analytics'; - name: undefined; - params: ( - params: AnalyticsImplementationFactory, - ) => ExtensionBlueprintParams>; - output: ConfigurableExtensionDataRef< - AnalyticsImplementationFactory<{}>, - 'core.analytics.factory', - {} - >; - inputs: {}; - config: {}; - configInput: {}; - dataRefs: { - factory: ConfigurableExtensionDataRef< - AnalyticsImplementationFactory<{}>, - 'core.analytics.factory', - {} - >; - }; -}>; - // @public export const AnalyticsContext: (options: { attributes: Partial; @@ -164,6 +140,30 @@ export type AnalyticsImplementation = { captureEvent(event: AnalyticsEvent): void; }; +// @public +export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{ + kind: 'analytics'; + name: undefined; + params: ( + params: AnalyticsImplementationFactory, + ) => ExtensionBlueprintParams>; + output: ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + inputs: {}; + config: {}; + configInput: {}; + dataRefs: { + factory: ConfigurableExtensionDataRef< + AnalyticsImplementationFactory<{}>, + 'core.analytics.factory', + {} + >; + }; +}>; + // @public (undocumented) export type AnalyticsImplementationFactory< Deps extends { diff --git a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts index 9752d0bd6f..c6ec4478f4 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -16,7 +16,7 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; import { AnalyticsContextValue } from '../../analytics/types'; -import type { AnalyticsBlueprint } from '../../blueprints/'; +import type { AnalyticsImplementationBlueprint } from '../../blueprints/'; /** * Represents an event worth tracking in an analytics system that could inform @@ -149,8 +149,8 @@ export type AnalyticsApi = { * * @remarks * - * To define a concrete Analytics Implementation, use {@link AnalyticsBlueprint} - * instead. + * To define a concrete Analytics Implementation, use + * {@link AnalyticsImplementationBlueprint} instead. * * @public */ diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts similarity index 90% rename from packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts rename to packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts index 1618c38335..4a9e21a13d 100644 --- a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { AnalyticsBlueprint } from './AnalyticsBlueprint'; +import { AnalyticsImplementationBlueprint } from './AnalyticsImplementationBlueprint'; describe('AnalyticsBlueprint', () => { it('should create an extension with sensible defaults', () => { @@ -23,7 +23,7 @@ describe('AnalyticsBlueprint', () => { factory: () => ({ captureEvent: () => {} }), }; - const extension = AnalyticsBlueprint.make({ + const extension = AnalyticsImplementationBlueprint.make({ params: define => define(factory), name: 'test', }); diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts similarity index 95% rename from packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts rename to packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts index 8bd4c0526e..260a58a941 100644 --- a/packages/frontend-plugin-api/src/blueprints/AnalyticsBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts @@ -39,7 +39,7 @@ const factoryDataRef = * * @public */ -export const AnalyticsBlueprint = createExtensionBlueprint({ +export const AnalyticsImplementationBlueprint = createExtensionBlueprint({ kind: 'analytics', attachTo: [{ id: 'api:app/analytics', input: 'analyticsImplementations' }], output: [factoryDataRef], diff --git a/packages/frontend-plugin-api/src/blueprints/index.ts b/packages/frontend-plugin-api/src/blueprints/index.ts index 29aa410e80..0060571ca2 100644 --- a/packages/frontend-plugin-api/src/blueprints/index.ts +++ b/packages/frontend-plugin-api/src/blueprints/index.ts @@ -15,9 +15,9 @@ */ export { - AnalyticsBlueprint, + AnalyticsImplementationBlueprint, type AnalyticsImplementationFactory, -} from './AnalyticsBlueprint'; +} from './AnalyticsImplementationBlueprint'; export { ApiBlueprint } from './ApiBlueprint'; export { AppRootElementBlueprint } from './AppRootElementBlueprint'; export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; diff --git a/plugins/app/src/extensions/AnalyticsApi.test.ts b/plugins/app/src/extensions/AnalyticsApi.test.ts index 814f41b616..d0763de9d5 100644 --- a/plugins/app/src/extensions/AnalyticsApi.test.ts +++ b/plugins/app/src/extensions/AnalyticsApi.test.ts @@ -16,7 +16,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils'; import { AnalyticsApi } from './AnalyticsApi'; import { - AnalyticsBlueprint, + AnalyticsImplementationBlueprint, AnalyticsImplementation, ApiBlueprint, configApiRef, @@ -38,10 +38,10 @@ describe('AnalyticsApi', () => { const MockImplementation1 = createExtension({ name: 'mock-implementation-1', attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, - output: [AnalyticsBlueprint.dataRefs.factory], + output: [AnalyticsImplementationBlueprint.dataRefs.factory], factory() { return [ - AnalyticsBlueprint.dataRefs.factory({ + AnalyticsImplementationBlueprint.dataRefs.factory({ deps: { configApi: configApiRef }, factory: deps => ({ captureEvent: event => captureEventSpy1(event, deps), @@ -54,10 +54,10 @@ describe('AnalyticsApi', () => { const MockImplementation2 = createExtension({ name: 'mock-implementation-2', attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, - output: [AnalyticsBlueprint.dataRefs.factory], + output: [AnalyticsImplementationBlueprint.dataRefs.factory], factory() { return [ - AnalyticsBlueprint.dataRefs.factory({ + AnalyticsImplementationBlueprint.dataRefs.factory({ deps: { config: configApiRef, identityApi: identityApiRef }, factory: deps => ({ captureEvent: event => captureEventSpy2(event, deps), diff --git a/plugins/app/src/extensions/AnalyticsApi.ts b/plugins/app/src/extensions/AnalyticsApi.ts index 5e64e5abcf..f121e5a02e 100644 --- a/plugins/app/src/extensions/AnalyticsApi.ts +++ b/plugins/app/src/extensions/AnalyticsApi.ts @@ -16,7 +16,7 @@ import { analyticsApiRef, - AnalyticsBlueprint, + AnalyticsImplementationBlueprint, ApiBlueprint, ApiRef, createExtensionInput, @@ -26,7 +26,7 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ name: 'analytics', inputs: { analyticsImplementations: createExtensionInput([ - AnalyticsBlueprint.dataRefs.factory, + AnalyticsImplementationBlueprint.dataRefs.factory, ]), }, factory(originalFactory, { inputs }) { @@ -35,7 +35,9 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ // if they were its own deps. const aggregatedDeps = inputs.analyticsImplementations .flatMap>(impls => - Object.values(impls.get(AnalyticsBlueprint.dataRefs.factory).deps), + Object.values( + impls.get(AnalyticsImplementationBlueprint.dataRefs.factory).deps, + ), ) .reduce<{ [x: string]: ApiRef }>((accum, ref) => { accum[ref.id] = ref; @@ -48,7 +50,9 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ deps: aggregatedDeps, factory: analyticsApiDeps => { const actualApis = inputs.analyticsImplementations - .map(impl => impl.get(AnalyticsBlueprint.dataRefs.factory)) + .map(impl => + impl.get(AnalyticsImplementationBlueprint.dataRefs.factory), + ) .map(({ factory, deps }) => factory( // Reconstruct a deps argument to pass to this analytics From f692aed0c17a47180587357a31ed5362d38468fb Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 30 Jul 2025 09:31:26 +0200 Subject: [PATCH 3/5] input analyticsImplementations -> implementations Signed-off-by: Eric Peterson --- .../src/blueprints/AnalyticsImplementationBlueprint.test.ts | 2 +- .../src/blueprints/AnalyticsImplementationBlueprint.ts | 2 +- plugins/app/report.api.md | 2 +- plugins/app/src/extensions/AnalyticsApi.test.ts | 4 ++-- plugins/app/src/extensions/AnalyticsApi.ts | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts index 4a9e21a13d..b297b3a3a9 100644 --- a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.test.ts @@ -35,7 +35,7 @@ describe('AnalyticsBlueprint', () => { "attachTo": [ { "id": "api:app/analytics", - "input": "analyticsImplementations", + "input": "implementations", }, ], "configSchema": undefined, diff --git a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts index 260a58a941..91a0f27092 100644 --- a/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts +++ b/packages/frontend-plugin-api/src/blueprints/AnalyticsImplementationBlueprint.ts @@ -41,7 +41,7 @@ const factoryDataRef = */ export const AnalyticsImplementationBlueprint = createExtensionBlueprint({ kind: 'analytics', - attachTo: [{ id: 'api:app/analytics', input: 'analyticsImplementations' }], + attachTo: [{ id: 'api:app/analytics', input: 'implementations' }], output: [factoryDataRef], dataRefs: { factory: factoryDataRef, diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 9ddf7df9ed..8e514dbdcb 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -238,7 +238,7 @@ const appPlugin: FrontendPlugin< {} >; inputs: { - analyticsImplementations: ExtensionInput< + implementations: ExtensionInput< ConfigurableExtensionDataRef< AnalyticsImplementationFactory<{}>, 'core.analytics.factory', diff --git a/plugins/app/src/extensions/AnalyticsApi.test.ts b/plugins/app/src/extensions/AnalyticsApi.test.ts index d0763de9d5..dabebaecee 100644 --- a/plugins/app/src/extensions/AnalyticsApi.test.ts +++ b/plugins/app/src/extensions/AnalyticsApi.test.ts @@ -37,7 +37,7 @@ describe('AnalyticsApi', () => { const captureEventSpy1 = jest.fn(); const MockImplementation1 = createExtension({ name: 'mock-implementation-1', - attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, + attachTo: { id: 'api:analytics', input: 'implementations' }, output: [AnalyticsImplementationBlueprint.dataRefs.factory], factory() { return [ @@ -53,7 +53,7 @@ describe('AnalyticsApi', () => { const captureEventSpy2 = jest.fn(); const MockImplementation2 = createExtension({ name: 'mock-implementation-2', - attachTo: { id: 'api:analytics', input: 'analyticsImplementations' }, + attachTo: { id: 'api:analytics', input: 'implementations' }, output: [AnalyticsImplementationBlueprint.dataRefs.factory], factory() { return [ diff --git a/plugins/app/src/extensions/AnalyticsApi.ts b/plugins/app/src/extensions/AnalyticsApi.ts index f121e5a02e..3feab282c0 100644 --- a/plugins/app/src/extensions/AnalyticsApi.ts +++ b/plugins/app/src/extensions/AnalyticsApi.ts @@ -25,7 +25,7 @@ import { export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ name: 'analytics', inputs: { - analyticsImplementations: createExtensionInput([ + implementations: createExtensionInput([ AnalyticsImplementationBlueprint.dataRefs.factory, ]), }, @@ -33,7 +33,7 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ // Pull out and aggregate deps from every implementation input into an // object keyed by the apiRef ID to be passed to this API implementation as // if they were its own deps. - const aggregatedDeps = inputs.analyticsImplementations + const aggregatedDeps = inputs.implementations .flatMap>(impls => Object.values( impls.get(AnalyticsImplementationBlueprint.dataRefs.factory).deps, @@ -49,7 +49,7 @@ export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ api: analyticsApiRef, deps: aggregatedDeps, factory: analyticsApiDeps => { - const actualApis = inputs.analyticsImplementations + const actualApis = inputs.implementations .map(impl => impl.get(AnalyticsImplementationBlueprint.dataRefs.factory), ) From f54b03250319c7333ded86827f31259fb82d17bd Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Wed, 30 Jul 2025 09:38:28 +0200 Subject: [PATCH 4/5] Clean up analyticsApi def in app plugin Signed-off-by: Eric Peterson --- plugins/app/src/defaultApis.ts | 4 ++-- plugins/app/src/extensions/AnalyticsApi.test.ts | 10 +++++----- plugins/app/src/extensions/AnalyticsApi.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/app/src/defaultApis.ts b/plugins/app/src/defaultApis.ts index b3d130fb09..0337f6c4f4 100644 --- a/plugins/app/src/defaultApis.ts +++ b/plugins/app/src/defaultApis.ts @@ -68,7 +68,7 @@ import { IdentityPermissionApi, } from '@backstage/plugin-permission-react'; import { DefaultDialogApi } from './apis/DefaultDialogApi'; -import { AnalyticsApi } from './extensions/AnalyticsApi'; +import { analyticsApi } from './extensions/AnalyticsApi'; export const apis = [ ApiBlueprint.make({ @@ -101,7 +101,7 @@ export const apis = [ factory: () => new AlertApiForwarder(), }), }), - AnalyticsApi, + analyticsApi, ApiBlueprint.make({ name: 'error', params: defineParams => diff --git a/plugins/app/src/extensions/AnalyticsApi.test.ts b/plugins/app/src/extensions/AnalyticsApi.test.ts index dabebaecee..68796895d2 100644 --- a/plugins/app/src/extensions/AnalyticsApi.test.ts +++ b/plugins/app/src/extensions/AnalyticsApi.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { createExtensionTester } from '@backstage/frontend-test-utils'; -import { AnalyticsApi } from './AnalyticsApi'; +import { analyticsApi } from './AnalyticsApi'; import { AnalyticsImplementationBlueprint, AnalyticsImplementation, @@ -24,7 +24,7 @@ import { identityApiRef, } from '@backstage/frontend-plugin-api'; -describe('AnalyticsApi', () => { +describe('analyticsApi', () => { const mockEvent = { action: '', subject: '', @@ -73,7 +73,7 @@ describe('AnalyticsApi', () => { it('wires up a single AnalyticsImplementationFactory', async () => { // Given the Analytics API and a single mock implementation - const tester = createExtensionTester(AnalyticsApi).add(MockImplementation1); + const tester = createExtensionTester(analyticsApi).add(MockImplementation1); const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); // Then the API's deps should contain the mock implementation's deps. @@ -101,7 +101,7 @@ describe('AnalyticsApi', () => { it('wires up more than one AnalyticsImplementationFactory', () => { // Given the Analytics API and two mock implementations - const tester = createExtensionTester(AnalyticsApi) + const tester = createExtensionTester(analyticsApi) .add(MockImplementation1) .add(MockImplementation2); const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); @@ -142,7 +142,7 @@ describe('AnalyticsApi', () => { it('works fine with no AnalyticsImplementationFactory instances provided', () => { // Given the Analytics API and no mock implementations - const tester = createExtensionTester(AnalyticsApi); + const tester = createExtensionTester(analyticsApi); const apiFactory = tester.get(ApiBlueprint.dataRefs.factory); // Then the API's deps should be empty diff --git a/plugins/app/src/extensions/AnalyticsApi.ts b/plugins/app/src/extensions/AnalyticsApi.ts index 3feab282c0..e969649262 100644 --- a/plugins/app/src/extensions/AnalyticsApi.ts +++ b/plugins/app/src/extensions/AnalyticsApi.ts @@ -22,7 +22,7 @@ import { createExtensionInput, } from '@backstage/frontend-plugin-api'; -export const AnalyticsApi = ApiBlueprint.makeWithOverrides({ +export const analyticsApi = ApiBlueprint.makeWithOverrides({ name: 'analytics', inputs: { implementations: createExtensionInput([ From 22abcfb067f9669736db779c65f2971519dec1dc Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Mon, 4 Aug 2025 17:23:37 +0200 Subject: [PATCH 5/5] Undo deprecations. Signed-off-by: Eric Peterson --- packages/frontend-plugin-api/report.api.md | 3 +-- .../frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/frontend-plugin-api/report.api.md b/packages/frontend-plugin-api/report.api.md index 3d65591c57..d0bb500319 100644 --- a/packages/frontend-plugin-api/report.api.md +++ b/packages/frontend-plugin-api/report.api.md @@ -99,7 +99,7 @@ export { alertApiRef }; export { AlertMessage }; -// @public @deprecated +// @public export type AnalyticsApi = { captureEvent(event: AnalyticsEvent): void; }; @@ -143,7 +143,6 @@ export type AnalyticsImplementation = { // @public export const AnalyticsImplementationBlueprint: ExtensionBlueprint<{ kind: 'analytics'; - name: undefined; params: ( params: AnalyticsImplementationFactory, ) => ExtensionBlueprintParams>; diff --git a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts index c6ec4478f4..2040966464 100644 --- a/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/frontend-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -133,8 +133,6 @@ export type AnalyticsImplementation = { * with relevant methods for instrumentation. * * @public - * @deprecated This type is now deprecated and will be removed in an upcoming - * release. Use the {@link AnalyticsImplementation} type instead. */ export type AnalyticsApi = { /**