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"