diff --git a/.changeset/analytics-glimmering-radio-station.md b/.changeset/analytics-glimmering-radio-station.md new file mode 100644 index 0000000000..fb0c404eee --- /dev/null +++ b/.changeset/analytics-glimmering-radio-station.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-analytics-provider-ga': patch +--- + +Initial Google Analytics API provider for the Backstage Analytics API, which: + +- Handles pageview and custom event instrumentation idiomatically. +- Enables custom dimension and metric collection via app config. +- Includes configurable debug/test mode for non-production environments. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index a38baab3dc..7508ab8033 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -88,6 +88,7 @@ firehydrant FireHydrant Firekube Fiverr +ga gitbeaker GitHub GitLab diff --git a/plugins/analytics-provider-ga/.eslintrc.js b/plugins/analytics-provider-ga/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/analytics-provider-ga/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/analytics-provider-ga/README.md b/plugins/analytics-provider-ga/README.md new file mode 100644 index 0000000000..e7a603a34a --- /dev/null +++ b/plugins/analytics-provider-ga/README.md @@ -0,0 +1,131 @@ +# Analytics Provider: Google Analytics + +This plugin provides an opinionated implementation of the Backstage Analytics +API for Google Analytics. Once installed and configured, analytics events will +be sent to GA as your users navigate and use your Backstage instance. + +This plugin contains no (and will never contain any) other functionality. + +## Installation + +1. Install this plugin in your Backstage instance: `yarn add @backstage/plugin-analytics-provider-ga` +2. Register the plugin with your App. In most instances of Backstage, this is + as simple as adding `export { analyticsProviderGA } from '@backstage/plugin-analytics-provider-ga';` + to your `packages/app/src/plugins.ts` file. +3. Configure the plugin (see below). + +## Configuration + +The following is the minimum configuration required to start sending analytics +events to GA. All that's needed is your Universal Analytics tracking ID: + +```yaml +# app-config.yaml +app: + analytics: + provider: ga + ga: + trackingId: UA-0000000-0 +``` + +### Plugin Analytics + +In order to be able to analyze usage of your Backstage instance _by plugin_, we +strongly recommend configuring at least one custom dimension to capture Plugin +IDs associated with events, including page views. + +1. First, [configure the custom dimension in GA][configure-custom-dimension]. + Be sure to set the Scope to `hit`, and name it something like `Plugin`. Note + the index of the dimension you just created (e.g. `1`, if this is the first + custom dimension you've created in your GA property). +2. Then, add a mapping to your `app.analytics.ga` configuration that instructs + the plugin to capture Plugin IDs on the custom dimension you just created. + It should look like this: + +```yaml +app: + analytics: + provider: ga + ga: + trackingId: UA-0000000-0 + customDimensionsMetrics: + - type: dimension + index: 1 + source: domain + attribute: pluginId +``` + +You can configure additional custom dimension and metric collection by adding +more entries to the `customDimensionsMetrics` array: + +```yaml +app: + analytics: + ga: + customDimensionsMetrics: + - type: dimension + index: 1 + source: domain + attribute: pluginId + - type: dimension + index: 2 + source: domain + attribute: routeRef + - type: metric + index: 1 + source: context + attribute: someEventContextAttr +``` + +### Debugging and Testing + +In pre-production environments, you may wish to set additional configurations +to turn off reporting to Analytics and/or print debug statements to the +console. You can do so like this: + +```yaml +app: + analytics: + ga: + testMode: true # Prevents data being sent to GA + debug: true # Logs analytics event to the web console +``` + +## Development + +If you would like to contribute improvements to this plugin, the easiest way to +make and test changes is to do the following: + +1. Clone the main Backstage monorepo `git clone git@github.com:backstage/backstage.git` +2. Install all dependencies `yarn install` +3. If one does not exist, create an `app-config.local.yaml` file in the root of + the monorepo and add config for this plugin (see below) +4. Enter this plugin's working directory: `cd plugins/analytics-provider-ga` +5. Start the plugin in isolation: `yarn start` +6. Navigate to the playground page at [/ga](http://localhost:3000/ga) +7. Open the web console to see events fire when you navigate or when you + interact with instrumented components. + +Code for the isolated version of the plugin can be found inside the [/dev](./dev) +directory. Changes to the plugin are hot-reloaded. + +#### Recommended Dev Config + +Paste this into your `app-config.local.yaml` while developing this plugin: + +```yaml +app: + analytics: + provider: ga + ga: + trackingId: UA-0000000-0 + debug: true + testMode: true + customDimensionsMetrics: + - type: dimension + index: 1 + source: domain + attribute: pluginId +``` + +[configure-custom-dimension]: https://support.google.com/analytics/answer/2709828?hl=en#configuration diff --git a/plugins/analytics-provider-ga/config.d.ts b/plugins/analytics-provider-ga/config.d.ts new file mode 100644 index 0000000000..0b08b75f1e --- /dev/null +++ b/plugins/analytics-provider-ga/config.d.ts @@ -0,0 +1,86 @@ +/* + * 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. + */ + +export interface Config { + app: { + analytics: { + provider: 'ga'; + ga: { + /** + * Google Analytics tracking ID, e.g. UA-000000-0 + * @visibility frontend + */ + trackingId: string; + + /** + * Whether or not to log analytics debug statements to the console. + * Defaults to false. + * + * @visibility frontend + */ + debug?: boolean; + + /** + * Prevents events from actually being sent when set to true. Defaults + * to false. + * + * @visibility frontend + */ + testMode?: boolean; + + /** + * Configuration informing how Analytics Domain and Event Context + * metadata will be captured in Google Analytics. + */ + customDimensionsMetrics?: Array<{ + /** + * Specifies whether the corresponding metadata should be collected + * as a Google Analytics custom dimension or custom metric. + * + * @visibility frontend + */ + type: 'dimension' | 'metric'; + + /** + * The index of the Google Analytics custom dimension or metric that + * the metadata should be written to. + * + * @visibility frontend + */ + index: number; + + /** + * Specifies whether the desired value lives as an attribute on the + * Analytics Domain or the Event's Context. + * + * @visibility frontend + */ + source: 'domain' | 'context'; + + /** + * The attribute on the domain or context that should be captured. + * e.g. to capture the Plugin ID associated with an event, the source + * should be set to "domain" and the attribute should be set to + * pluginId. + * + * @visibility frontend + */ + attribute: string; + }>; + }; + }; + }; +} diff --git a/plugins/analytics-provider-ga/dev/Playground.tsx b/plugins/analytics-provider-ga/dev/Playground.tsx new file mode 100644 index 0000000000..d2817f5ab4 --- /dev/null +++ b/plugins/analytics-provider-ga/dev/Playground.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 React from 'react'; +import { withAnalyticsDomain } from '@backstage/core-plugin-api'; +import { Link } from '@backstage/core-components'; + +const DomainlessPlayground = () => { + return ( + <> + Click Here + + ); +}; + +export const Playground = withAnalyticsDomain(DomainlessPlayground, {}); diff --git a/plugins/analytics-provider-ga/dev/app-config.yaml b/plugins/analytics-provider-ga/dev/app-config.yaml new file mode 100644 index 0000000000..3d5b23a356 --- /dev/null +++ b/plugins/analytics-provider-ga/dev/app-config.yaml @@ -0,0 +1,3 @@ +app: + analytics: + provider: ga diff --git a/plugins/analytics-provider-ga/dev/index.tsx b/plugins/analytics-provider-ga/dev/index.tsx new file mode 100644 index 0000000000..4e4e9082bb --- /dev/null +++ b/plugins/analytics-provider-ga/dev/index.tsx @@ -0,0 +1,28 @@ +/* + * Copyright 2021 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { analyticsProviderGA } from '../src/plugin'; +import { Playground } from './Playground'; + +createDevApp() + .registerPlugin(analyticsProviderGA) + .addPage({ + path: '/ga', + title: 'GA Playground', + element: , + }) + .render(); diff --git a/plugins/analytics-provider-ga/package.json b/plugins/analytics-provider-ga/package.json new file mode 100644 index 0000000000..af60d0cb5c --- /dev/null +++ b/plugins/analytics-provider-ga/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-analytics-provider-ga", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/config": "^0.1.5", + "@backstage/core-components": "^0.1.6", + "@backstage/core-plugin-api": "^0.1.3", + "@backstage/theme": "^0.2.8", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-ga": "^3.3.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.4", + "@backstage/core-app-api": "^0.1.4", + "@backstage/dev-utils": "^0.2.2", + "@backstage/test-utils": "^0.1.14", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" +} diff --git a/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts new file mode 100644 index 0000000000..220ff5261b --- /dev/null +++ b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.test.ts @@ -0,0 +1,191 @@ +/* + * Copyright 2021 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 { ConfigReader } from '@backstage/config'; +import ReactGA from 'react-ga'; +import { GoogleAnalytics } from './GoogleAnalytics'; + +describe('GoogleAnalytics', () => { + const trackingId = 'UA-000000-0'; + const basicValidConfig = new ConfigReader({ + app: { analytics: { ga: { trackingId, testMode: true } } }, + }); + + beforeEach(() => { + ReactGA.testModeAPI.resetCalls(); + }); + + describe('fromConfig', () => { + it('throws when missing trackingId', () => { + const config = new ConfigReader({ app: { analytics: { ga: {} } } }); + expect(() => GoogleAnalytics.fromConfig(config)).toThrowError( + /Missing required config value/, + ); + }); + + it('returns implementation', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + expect(api.getDecoratedTracker).toBeDefined(); + + // Initializes GA with tracking ID. + expect(ReactGA.testModeAPI.calls[0]).toEqual([ + 'create', + trackingId, + 'auto', + ]); + }); + }); + + describe('integration', () => { + const domain = { + componentName: 'App', + pluginId: 'some-plugin', + releaseNum: 1337, + }; + const advancedConfig = new ConfigReader({ + app: { + analytics: { + ga: { + trackingId, + testMode: true, + customDimensionsMetrics: [ + { + type: 'dimension', + index: 1, + source: 'domain', + attribute: 'pluginId', + }, + { + type: 'dimension', + index: 2, + source: 'context', + attribute: 'extraDimension', + }, + { + type: 'metric', + index: 1, + source: 'domain', + attribute: 'releaseNum', + }, + { + type: 'metric', + index: 2, + source: 'context', + attribute: 'extraMetric', + }, + ], + }, + }, + }, + }); + + it('tracks basic pageview', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + const tracker = api.getDecoratedTracker({ domain }); + tracker.captureEvent('navigate', '/'); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'pageview', + page: '/', + }); + }); + + it('tracks basic event', () => { + const api = GoogleAnalytics.fromConfig(basicValidConfig); + const tracker = api.getDecoratedTracker({ domain }); + + const expectedAction = 'click'; + const expectedLabel = 'on something'; + const expectedValue = 42; + tracker.captureEvent(expectedAction, expectedLabel, expectedValue); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'event', + eventCategory: domain.componentName, + eventAction: expectedAction, + eventLabel: expectedLabel, + eventValue: expectedValue, + }); + }); + + it('captures configured custom dimensions/metrics on pageviews', () => { + const api = GoogleAnalytics.fromConfig(advancedConfig); + const tracker = api.getDecoratedTracker({ domain }); + tracker.captureEvent('navigate', '/a-page'); + + // Expect a set command first. + const [setCommand, setData] = ReactGA.testModeAPI.calls[1]; + expect(setCommand).toBe('set'); + expect(setData).toMatchObject({ + dimension1: domain.pluginId, + metric1: domain.releaseNum, + }); + + // Followed by a send command. + const [sendCommand, sendData] = ReactGA.testModeAPI.calls[2]; + expect(sendCommand).toBe('send'); + expect(sendData).toMatchObject({ + hitType: 'pageview', + page: '/a-page', + }); + }); + + it('captures configured custom dimensions/metrics on events', () => { + const api = GoogleAnalytics.fromConfig(advancedConfig); + const tracker = api.getDecoratedTracker({ domain }); + + const expectedAction = 'search'; + const expectedLabel = 'some query'; + const expectedValue = 5; + tracker.captureEvent(expectedAction, expectedLabel, expectedValue, { + extraDimension: false, + extraMetric: 0, + }); + + const [command, data] = ReactGA.testModeAPI.calls[1]; + expect(command).toBe('send'); + expect(data).toMatchObject({ + hitType: 'event', + eventCategory: domain.componentName, + eventAction: expectedAction, + eventLabel: expectedLabel, + eventValue: expectedValue, + dimension1: domain.pluginId, + metric1: domain.releaseNum, + dimension2: false, + metric2: 0, + }); + }); + + it('does not pass non-numeric data on metrics', () => { + const api = GoogleAnalytics.fromConfig(advancedConfig); + const tracker = api.getDecoratedTracker({ domain }); + + tracker.captureEvent('verb', 'noun', undefined, { + extraMetric: 'not a number', + }); + + const [, data] = ReactGA.testModeAPI.calls[1]; + expect(data).not.toMatchObject({ + metric2: 'not a number', + }); + }); + }); +}); diff --git a/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts new file mode 100644 index 0000000000..d02924d138 --- /dev/null +++ b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/GoogleAnalytics.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2021 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 ReactGA from 'react-ga'; +import { + AnalyticsApi, + AnalyticsTracker, + AnalyticsDomainValue, + AnalyticsEventContext, +} from '@backstage/core-plugin-api'; +import { Config } from '@backstage/config'; + +type CDM = { + type: 'dimension' | 'metric'; + index: number; + source: 'domain' | 'context'; + attribute: string; +}; + +/** + * Type guard for emptiness check in array filter. + */ +function notEmpty(value: T | undefined): value is T { + return value !== undefined; +} + +/** + * Google Analytics API provider for the Backstage Analytics API. + */ +export class GoogleAnalytics implements AnalyticsApi { + private readonly cdmConfig: CDM[]; + + /** + * Instantiate the implementation and initialize ReactGA. + */ + private constructor({ + cdmConfig, + trackingId, + testMode, + debug, + }: { + cdmConfig: CDM[]; + trackingId: string; + testMode: boolean; + debug: boolean; + }) { + this.cdmConfig = cdmConfig; + + // Initialize Google Analytics. + ReactGA.initialize(trackingId, { testMode, debug, titleCase: false }); + } + + /** + * Instantiate a fully configured GA Analytics API implementation. + */ + static fromConfig(config: Config) { + // Get all necessary configuration. + const trackingId = config.getString('app.analytics.ga.trackingId'); + const debug = !!config.getOptionalBoolean('app.analytics.ga.debug'); + const testMode = !!config.getOptionalBoolean('app.analytics.ga.testMode'); + const cdmConfig = + config + .getOptionalConfigArray('app.analytics.ga.customDimensionsMetrics') + ?.map(c => { + return { + type: c.getString('type') as CDM['type'], + index: c.getNumber('index'), + source: c.getString('source') as CDM['source'], + attribute: c.getString('attribute'), + }; + }) || []; + + // Return an implementation instance. + return new GoogleAnalytics({ + trackingId, + cdmConfig, + testMode, + debug, + }); + } + + /** + * Returns the Google Analytics tracker to API consumers. + */ + getDecoratedTracker({ + domain, + }: { + domain: AnalyticsDomainValue; + }): AnalyticsTracker { + return { + captureEvent: (...args) => this.captureEvent(domain, ...args), + }; + } + + /** + * Primary event capture implementation. Handles core navigate event as a + * pageview and the rest as custom events. All custom dimensions/metrics are + * applied as they should be (set on pageview, merged object on events). + */ + private captureEvent( + domain: AnalyticsDomainValue, + verb: string, + noun: string, + value?: number, + context?: AnalyticsEventContext, + ) { + const customMetadata = this.getCustomDimensionMetrics(domain, context); + + if (verb === 'navigate' && domain?.componentName === 'App') { + // Set any/all custom dimensions. + if (Object.keys(customMetadata).length) { + ReactGA.set(customMetadata); + } + + ReactGA.pageview(noun); + return; + } + + ReactGA.event({ + category: domain.componentName || 'App', + action: verb, + label: noun, + value, + ...customMetadata, + }); + } + + /** + * Returns an object of dimensions/metrics given an Analytics Domain and an + * Event Context, e.g. { dimension1: "some value", metric8: 42 } + */ + private getCustomDimensionMetrics( + domain: AnalyticsDomainValue, + context: AnalyticsEventContext = {}, + ) { + const dataArray = this.cdmConfig + .map(cdm => { + const value = + cdm.source === 'domain' + ? domain[cdm.attribute] + : context[cdm.attribute]; + + // Never pass a non-numeric value on a metric. + if (cdm.type === 'metric' && typeof value !== 'number') { + return undefined; + } + + return value !== undefined + ? { + [`${cdm.type}${cdm.index}`]: value, + } + : undefined; + }) + .filter(notEmpty); + + return dataArray.length + ? dataArray.reduce((result, cd) => { + return Object.assign(result, cd); + }) + : {}; + } +} diff --git a/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/index.ts b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/index.ts new file mode 100644 index 0000000000..a8d11e4c6c --- /dev/null +++ b/plugins/analytics-provider-ga/src/apis/implementations/AnalyticsApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 { GoogleAnalytics } from './GoogleAnalytics'; diff --git a/plugins/analytics-provider-ga/src/index.ts b/plugins/analytics-provider-ga/src/index.ts new file mode 100644 index 0000000000..8dbf2d6210 --- /dev/null +++ b/plugins/analytics-provider-ga/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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 { analyticsProviderGA } from './plugin'; diff --git a/plugins/analytics-provider-ga/src/plugin.test.ts b/plugins/analytics-provider-ga/src/plugin.test.ts new file mode 100644 index 0000000000..92d12ea6db --- /dev/null +++ b/plugins/analytics-provider-ga/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 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 { analyticsProviderGA } from './plugin'; + +describe('google-analytics', () => { + it('should export plugin', () => { + expect(analyticsProviderGA).toBeDefined(); + }); +}); diff --git a/plugins/analytics-provider-ga/src/plugin.ts b/plugins/analytics-provider-ga/src/plugin.ts new file mode 100644 index 0000000000..e84dbbcd04 --- /dev/null +++ b/plugins/analytics-provider-ga/src/plugin.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 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, + configApiRef, + createApiFactory, + createPlugin, +} from '@backstage/core-plugin-api'; +import { GoogleAnalytics } from './apis/implementations/AnalyticsApi'; + +export const analyticsProviderGA = createPlugin({ + id: 'analytics-provider-ga', + apis: [ + createApiFactory({ + api: analyticsApiRef, + deps: { config: configApiRef }, + factory: ({ config }) => GoogleAnalytics.fromConfig(config), + }), + ], +}); diff --git a/plugins/analytics-provider-ga/src/setupTests.ts b/plugins/analytics-provider-ga/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/analytics-provider-ga/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/yarn.lock b/yarn.lock index 19869e641a..0a97f1435d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -23031,11 +23031,16 @@ react-fast-compare@^2.0.4: resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-fast-compare@^3.0.1, react-fast-compare@^3.1.1, react-fast-compare@^3.2.0: +react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== +react-ga@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/react-ga/-/react-ga-3.3.0.tgz#c91f407198adcb3b49e2bc5c12b3fe460039b3ca" + integrity sha512-o8RScHj6Lb8cwy3GMrVH6NJvL+y0zpJvKtc0+wmH7Bt23rszJmnqEQxRbyrqUzk9DTJIHoP42bfO5rswC9SWBQ== + react-helmet-async@^1.0.7: version "1.0.9" resolved "https://registry.npmjs.org/react-helmet-async/-/react-helmet-async-1.0.9.tgz#5b9ed2059de6b4aab47f769532f9fbcbce16c5ca"