From e9d40ebf5490717b254be73f0e5ba28ba3aa7731 Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 23 Aug 2022 15:56:48 +0200 Subject: [PATCH 1/2] Provide analyticsApi implementation to forward to multiple services Signed-off-by: Eric Peterson --- .changeset/veka-fingrar-drar.md | 28 ++++++++ packages/core-app-api/api-report.md | 6 ++ .../AnalyticsApi/MultipleAnalyticsApi.test.ts | 64 +++++++++++++++++ .../AnalyticsApi/MultipleAnalyticsApi.ts | 69 +++++++++++++++++++ .../implementations/AnalyticsApi/index.ts | 1 + 5 files changed, 168 insertions(+) create mode 100644 .changeset/veka-fingrar-drar.md create mode 100644 packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts create mode 100644 packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts diff --git a/.changeset/veka-fingrar-drar.md b/.changeset/veka-fingrar-drar.md new file mode 100644 index 0000000000..8f5281632d --- /dev/null +++ b/.changeset/veka-fingrar-drar.md @@ -0,0 +1,28 @@ +--- +'@backstage/core-app-api': patch +--- + +If you'd like to send analytics events to multiple implementations, you may now +do so using the `MultipleAnalyticsApi` implementation provided by this package. + +```tsx +import { MultipleAnalyticsApi } from '@backstage/core-app-api'; +import { + analyticsApiRef, + configApiRef, + storageApiRef, + identityApiRef, +} from '@internal/backstage/core-plugin-api'; +import { CustomAnalyticsApi } from '@internal/analytics'; +import { VendorAnalyticsApi } from '@vendor/analytics'; + +createApiFactory({ + api: analyticsApiRef, + deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, + factory: ({ configApi, identityApi, storageApi }) => + MultipleAnalyticsApi.withApis([ + VendorAnalyticsApi.fromConfig(configApi, { identityApi }), + CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), + ]), +}), +``` diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 3cc1f4a8a8..9d528b6bef 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -409,6 +409,12 @@ export class MicrosoftAuth { static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T; } +// @public +export class MultipleAnalyticsApi implements AnalyticsApi { + captureEvent(event: AnalyticsEvent): void; + static withApis(actualApis?: AnalyticsApi[]): MultipleAnalyticsApi; +} + // @public export class NoOpAnalyticsApi implements AnalyticsApi { // (undocumented) diff --git a/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts new file mode 100644 index 0000000000..e020c7b073 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2022 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.withApis([ + 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/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts new file mode 100644 index 0000000000..72f04dc242 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2022 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'; + +/** + * 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.withApis([ + * VendorAnalyticsApi.fromConfig(configApi, { identityApi }), + * CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), + * ]), + * }); + * ``` + */ +export class MultipleAnalyticsApi implements AnalyticsApi { + private constructor(private readonly actualApis: AnalyticsApi[]) {} + + /** + * Create an AnalyticsApi implementation from an array of concrete + * implementations. + * + * @example + * + * ```jsx + * MultipleAnalyticsApi.withApis([ + * SomeAnalyticsApi.fromConfig(configApi), + * new CustomAnalyticsApi(), + * ]); + * ``` + */ + static withApis(actualApis: AnalyticsApi[] = []) { + return new MultipleAnalyticsApi(actualApis); + } + + /** + * Forward the event to all configured analytics API implementations. + */ + captureEvent(event: AnalyticsEvent): void { + this.actualApis.forEach(analyticsApi => { + /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ + try { + analyticsApi.captureEvent(event); + } catch {} + }); + } +} diff --git a/packages/core-app-api/src/apis/implementations/AnalyticsApi/index.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/index.ts index 6bb27dd6d0..0a137ee422 100644 --- a/packages/core-app-api/src/apis/implementations/AnalyticsApi/index.ts +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { MultipleAnalyticsApi } from './MultipleAnalyticsApi'; export { NoOpAnalyticsApi } from './NoOpAnalyticsApi'; From a5610bee0c0ac552d806bd323508a1778ea0e23a Mon Sep 17 00:00:00 2001 From: Eric Peterson Date: Tue, 30 Aug 2022 13:49:47 +0200 Subject: [PATCH 2/2] withApis -> fromApis, non-optional actualApis argument, etc Signed-off-by: Eric Peterson --- .changeset/veka-fingrar-drar.md | 2 +- packages/core-app-api/api-report.md | 2 +- .../AnalyticsApi/MultipleAnalyticsApi.test.ts | 2 +- .../AnalyticsApi/MultipleAnalyticsApi.ts | 11 ++++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.changeset/veka-fingrar-drar.md b/.changeset/veka-fingrar-drar.md index 8f5281632d..cff2f45919 100644 --- a/.changeset/veka-fingrar-drar.md +++ b/.changeset/veka-fingrar-drar.md @@ -20,7 +20,7 @@ createApiFactory({ api: analyticsApiRef, deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, factory: ({ configApi, identityApi, storageApi }) => - MultipleAnalyticsApi.withApis([ + MultipleAnalyticsApi.fromApis([ VendorAnalyticsApi.fromConfig(configApi, { identityApi }), CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), ]), diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 9d528b6bef..f2bc4b6665 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -412,7 +412,7 @@ export class MicrosoftAuth { // @public export class MultipleAnalyticsApi implements AnalyticsApi { captureEvent(event: AnalyticsEvent): void; - static withApis(actualApis?: AnalyticsApi[]): MultipleAnalyticsApi; + static fromApis(actualApis: AnalyticsApi[]): MultipleAnalyticsApi; } // @public diff --git a/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts index e020c7b073..ebbff04b92 100644 --- a/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.test.ts @@ -18,7 +18,7 @@ import { MultipleAnalyticsApi } from './MultipleAnalyticsApi'; describe('MultipleAnalyticsApi', () => { const analyticsApiOne = { captureEvent: jest.fn() }; const analyticsApiTwo = { captureEvent: jest.fn() }; - const multipleApis = MultipleAnalyticsApi.withApis([ + const multipleApis = MultipleAnalyticsApi.fromApis([ analyticsApiOne, analyticsApiTwo, ]); diff --git a/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts index 72f04dc242..91ac53f62a 100644 --- a/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts +++ b/packages/core-app-api/src/apis/implementations/AnalyticsApi/MultipleAnalyticsApi.ts @@ -28,7 +28,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; * api: analyticsApiRef, * deps: { configApi: configApiRef, identityApi: identityApiRef, storageApi: storageApiRef }, * factory: ({ configApi, identityApi, storageApi }) => - * MultipleAnalyticsApi.withApis([ + * MultipleAnalyticsApi.fromApis([ * VendorAnalyticsApi.fromConfig(configApi, { identityApi }), * CustomAnalyticsApi.fromConfig(configApi, { identityApi, storageApi }), * ]), @@ -45,13 +45,13 @@ export class MultipleAnalyticsApi implements AnalyticsApi { * @example * * ```jsx - * MultipleAnalyticsApi.withApis([ + * MultipleAnalyticsApi.fromApis([ * SomeAnalyticsApi.fromConfig(configApi), * new CustomAnalyticsApi(), * ]); * ``` */ - static withApis(actualApis: AnalyticsApi[] = []) { + static fromApis(actualApis: AnalyticsApi[]) { return new MultipleAnalyticsApi(actualApis); } @@ -60,10 +60,11 @@ export class MultipleAnalyticsApi implements AnalyticsApi { */ captureEvent(event: AnalyticsEvent): void { this.actualApis.forEach(analyticsApi => { - /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ try { analyticsApi.captureEvent(event); - } catch {} + } catch { + /* ignored */ + } }); } }