Add analytics-provider-ga plugin
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
@@ -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.
|
||||
@@ -88,6 +88,7 @@ firehydrant
|
||||
FireHydrant
|
||||
Firekube
|
||||
Fiverr
|
||||
ga
|
||||
gitbeaker
|
||||
GitHub
|
||||
GitLab
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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
|
||||
+86
@@ -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;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Link to="#clicked">Click Here</Link>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Playground = withAnalyticsDomain(DomainlessPlayground, {});
|
||||
@@ -0,0 +1,3 @@
|
||||
app:
|
||||
analytics:
|
||||
provider: ga
|
||||
@@ -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: <Playground />,
|
||||
})
|
||||
.render();
|
||||
@@ -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"
|
||||
}
|
||||
+191
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+175
@@ -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<T>(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);
|
||||
})
|
||||
: {};
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@@ -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';
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user