Use standard event names for page_view
Remove fnSend and send all captured events through ga4-events Change the version to minor Fix package.json Changes to test to make it pass Signed-off-by: sriram ramakrishnan <sramakr@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-analytics-module-ga4': major
|
||||
'@backstage/plugin-analytics-module-ga4': minor
|
||||
---
|
||||
|
||||
Plugin provides Backstage Analytics API for Google Analytics 4. Once installed and configured, analytics events will be sent to GA4 as your users navigate and use your Backstage instance
|
||||
|
||||
@@ -1,47 +1 @@
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, {
|
||||
plugins: ['jsdoc'],
|
||||
rules: {
|
||||
'jsdoc/check-access': 1,
|
||||
'jsdoc/check-alignment': 1,
|
||||
'jsdoc/check-param-names': ['warn', { checkDestructured: false }],
|
||||
'jsdoc/check-property-names': 1,
|
||||
'jsdoc/check-tag-names': ['warn', { definedTags: ['visibility'] }],
|
||||
'jsdoc/check-types': 1,
|
||||
'jsdoc/check-values': 1,
|
||||
'jsdoc/empty-tags': 1,
|
||||
'jsdoc/implements-on-classes': 1,
|
||||
'jsdoc/multiline-blocks': 1,
|
||||
'jsdoc/no-multi-asterisks': 1,
|
||||
'jsdoc/no-types': 1,
|
||||
'jsdoc/no-undefined-types': 1,
|
||||
'jsdoc/require-asterisk-prefix': 1,
|
||||
'jsdoc/require-description': 1,
|
||||
'jsdoc/require-jsdoc': [
|
||||
'warn',
|
||||
{
|
||||
publicOnly: true,
|
||||
require: {
|
||||
FunctionDeclaration: true,
|
||||
MethodDefinition: true,
|
||||
ClassDeclaration: true,
|
||||
ArrowFunctionExpression: true,
|
||||
FunctionExpression: true,
|
||||
},
|
||||
contexts: ['ExportNamedDeclaration > VariableDeclaration'],
|
||||
},
|
||||
],
|
||||
'jsdoc/require-param': ['warn', { checkDestructured: false }],
|
||||
'jsdoc/require-param-description': 1,
|
||||
'jsdoc/require-param-name': 1,
|
||||
'jsdoc/require-property': 1,
|
||||
'jsdoc/require-property-description': 1,
|
||||
'jsdoc/require-property-name': 1,
|
||||
'jsdoc/require-property-type': 1,
|
||||
'jsdoc/valid-types': 1,
|
||||
},
|
||||
settings: {
|
||||
jsdoc: {
|
||||
tagNamePreference: { 'tag constructor': 'constructor' },
|
||||
},
|
||||
},
|
||||
});
|
||||
module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
|
||||
|
||||
@@ -62,9 +62,8 @@ backend:
|
||||
|
||||
## Configuration
|
||||
|
||||
In order to be able to analyze usage of your Backstage instance _by plugin_, we
|
||||
strongly recommend configuring at least one [custom dimension][what-is-a-custom-dimension]
|
||||
to capture Plugin IDs associated with events, including page views.
|
||||
In order to be able to analyze usage of your Backstage instance by plugin, we recommend configuring [a content grouping](#enabling-content-grouping).
|
||||
Additional dimensional data can be captured using custom dimensions, like this:
|
||||
|
||||
1. First, [configure the custom dimension in GA] [configure-custom-dimension].
|
||||
Be sure to set the Scope to `Event`, and name it `dimension1`.
|
||||
|
||||
+2
-34
@@ -46,38 +46,6 @@ export interface Config {
|
||||
*/
|
||||
identity?: 'disabled' | 'optional' | 'required';
|
||||
|
||||
/**
|
||||
* Controls whether to send virtual pageviews on `search` events.
|
||||
* Can be used to enable Site Search in GA.
|
||||
*/
|
||||
virtualSearchPageView?: {
|
||||
/**
|
||||
* - `disabled`: (Default) no virtual pageviews are sent
|
||||
* - `only`: Sends virtual pageview _instead_ of the `search` event
|
||||
* - `both`: Sends both the `search` event _and_ the virtual pageview
|
||||
* @visibility frontend
|
||||
*/
|
||||
mode?: 'disabled' | 'only' | 'both';
|
||||
/**
|
||||
* Specifies on which path the main Search page is mounted.
|
||||
* Defaults to `/search`.
|
||||
* @visibility frontend
|
||||
*/
|
||||
mountPath?: string;
|
||||
/**
|
||||
* Specifies which query param is used for the term query in the virtual pageview URL.
|
||||
* Defaults to `query`.
|
||||
* @visibility frontend
|
||||
*/
|
||||
searchQuery?: string;
|
||||
/**
|
||||
* Specifies which query param is used for the category query in the virtual pageview URL.
|
||||
* Skipped by default.
|
||||
* @visibility frontend
|
||||
*/
|
||||
categoryQuery?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether to log analytics debug statements to the console.
|
||||
* Defaults to false.
|
||||
@@ -114,14 +82,14 @@ export interface Config {
|
||||
* context-name will be prefixed by c_, for example, pluginId will be c_pluginId in the event.
|
||||
*
|
||||
*/
|
||||
allowedContexts?: Array<string>;
|
||||
allowedContexts?: string[] | ['*'];
|
||||
/**
|
||||
*
|
||||
* Attributes that will be sent as parameters in the event
|
||||
* attribute-name will be prefixed by a_, for example , testAttribute will be c_testAttribute in the event.
|
||||
*
|
||||
*/
|
||||
allowedAttributes?: Array<string>;
|
||||
allowedAttributes?: string[] | ['*'];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"name": "@backstage/plugin-analytics-module-ga4",
|
||||
"version": "0.1.19-next.1",
|
||||
"version": "0.0.1",
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
@@ -18,37 +17,40 @@
|
||||
"start": "backstage-cli package start",
|
||||
"lint": "backstage-cli package lint",
|
||||
"test": "backstage-cli package test",
|
||||
"diff": "backstage-cli plugin:diff",
|
||||
"prepack": "backstage-cli package prepack",
|
||||
"postpack": "backstage-cli package postpack",
|
||||
"clean": "backstage-cli package clean"
|
||||
},
|
||||
"dependencies": {
|
||||
"@backstage/config": "^1.0.6",
|
||||
"@backstage/core-components": "^0.12.4",
|
||||
"@backstage/core-plugin-api": "^1.4.0",
|
||||
"@backstage/theme": "^0.2.16",
|
||||
"@backstage/config": "workspace:^",
|
||||
"@backstage/core-components": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/theme": "workspace:^",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.57",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"react-ga4": "^2.0.0",
|
||||
"react-use": "^17.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.13.1 || ^17.0.0"
|
||||
"react": "^16.13.1 || ^17.0.0",
|
||||
"react-dom": "^16.13.1 || ^17.0.0",
|
||||
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^0.22.3",
|
||||
"@backstage/core-app-api": "^1.5.0",
|
||||
"@backstage/dev-utils": "^1.0.12",
|
||||
"@backstage/test-utils": "^1.2.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/dom": "^8.0.0",
|
||||
"@testing-library/jest-dom": "^5.10.1",
|
||||
"@testing-library/react": "^12.1.3",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"@types/react": "^16.13.1 || ^17.0.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"msw": "^0.44.0"
|
||||
"msw": "^1.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
+133
-57
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 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 { IdentityApi } from '@backstage/core-plugin-api';
|
||||
import ReactGA from 'react-ga4';
|
||||
@@ -19,12 +34,6 @@ fnSet.mockImplementation((fieldObject: any) => {
|
||||
return;
|
||||
});
|
||||
|
||||
const fnSend = jest.spyOn(ReactGA, 'send');
|
||||
// @ts-ignore
|
||||
fnSend.mockImplementation((fieldObject: any) => {
|
||||
return;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -38,7 +47,9 @@ describe('GoogleAnalytics4', () => {
|
||||
};
|
||||
const measurementId = 'G-000000-0';
|
||||
const basicValidConfig = new ConfigReader({
|
||||
app: { analytics: { ga4: { measurementId: measurementId, testMode: true } } },
|
||||
app: {
|
||||
analytics: { ga4: { measurementId: measurementId, testMode: true } },
|
||||
},
|
||||
});
|
||||
|
||||
describe('fromConfig', () => {
|
||||
@@ -59,9 +70,10 @@ describe('GoogleAnalytics4', () => {
|
||||
subject: '/',
|
||||
context,
|
||||
});
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/',
|
||||
category: 'App',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -107,6 +119,19 @@ describe('GoogleAnalytics4', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const allowAllContextsAndAttrsConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
allowedContexts: ['*'],
|
||||
allowedAttributes: ['*'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('testing content grouping', () => {
|
||||
const api = GoogleAnalytics4.fromConfig(configWithContentGrouping);
|
||||
api.captureEvent({
|
||||
@@ -115,9 +140,11 @@ describe('GoogleAnalytics4', () => {
|
||||
context,
|
||||
});
|
||||
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/a-page',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/a-page',
|
||||
category: 'App',
|
||||
value: undefined,
|
||||
content_group: context.pluginId,
|
||||
});
|
||||
});
|
||||
@@ -133,12 +160,11 @@ describe('GoogleAnalytics4', () => {
|
||||
value: expectedValue,
|
||||
context,
|
||||
});
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'event',
|
||||
eventAction: 'search',
|
||||
eventCategory: 'App',
|
||||
eventLabel: 'search-term',
|
||||
eventValue: 42,
|
||||
expect(fnEvent).toHaveBeenCalledWith('search', {
|
||||
action: 'search',
|
||||
category: 'App',
|
||||
label: 'search-term',
|
||||
value: 42,
|
||||
search_term: 'search-term',
|
||||
});
|
||||
});
|
||||
@@ -156,12 +182,11 @@ describe('GoogleAnalytics4', () => {
|
||||
context,
|
||||
});
|
||||
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'event',
|
||||
eventCategory: context.extension,
|
||||
eventAction: expectedAction,
|
||||
eventLabel: expectedLabel,
|
||||
eventValue: expectedValue,
|
||||
expect(fnEvent).toHaveBeenCalledWith('click', {
|
||||
action: 'click',
|
||||
category: context.extension,
|
||||
label: 'on something',
|
||||
value: expectedValue,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,14 +198,41 @@ describe('GoogleAnalytics4', () => {
|
||||
context,
|
||||
});
|
||||
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/a-page',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/a-page',
|
||||
category: 'App',
|
||||
value: undefined,
|
||||
c_pluginId: context.pluginId,
|
||||
c_releaseNum: context.releaseNum,
|
||||
});
|
||||
});
|
||||
|
||||
it('captures all dimensions/metrics on pageviews', () => {
|
||||
const api = GoogleAnalytics4.fromConfig(allowAllContextsAndAttrsConfig);
|
||||
api.captureEvent({
|
||||
action: 'navigate',
|
||||
subject: '/a-page',
|
||||
context,
|
||||
attributes: {
|
||||
'attr-1': 'attr-value-1',
|
||||
'attr-2': 'attr-value-2',
|
||||
},
|
||||
});
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
category: 'App',
|
||||
label: '/a-page',
|
||||
value: undefined,
|
||||
c_pluginId: context.pluginId,
|
||||
c_releaseNum: context.releaseNum,
|
||||
c_routeRef: context.routeRef,
|
||||
c_extension: context.extension,
|
||||
'a_attr-1': 'attr-value-1',
|
||||
'a_attr-2': 'attr-value-2',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures configured custom dimensions/metrics on events', () => {
|
||||
const api = GoogleAnalytics4.fromConfig(advancedConfig);
|
||||
|
||||
@@ -198,12 +250,11 @@ describe('GoogleAnalytics4', () => {
|
||||
context,
|
||||
});
|
||||
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'event',
|
||||
eventCategory: context.extension,
|
||||
eventAction: expectedAction,
|
||||
eventLabel: expectedLabel,
|
||||
eventValue: expectedValue,
|
||||
expect(fnEvent).toHaveBeenCalledWith('search', {
|
||||
action: 'search',
|
||||
category: context.extension,
|
||||
label: expectedLabel,
|
||||
value: expectedValue,
|
||||
c_pluginId: context.pluginId,
|
||||
c_releaseNum: context.releaseNum,
|
||||
search_term: expectedLabel,
|
||||
@@ -223,9 +274,9 @@ describe('GoogleAnalytics4', () => {
|
||||
});
|
||||
|
||||
expect(fnEvent).not.toHaveBeenCalledWith({
|
||||
eventCategory: context.extension,
|
||||
eventAction: 'verb',
|
||||
eventLabel: 'noun',
|
||||
category: context.extension,
|
||||
action: 'verb',
|
||||
label: 'noun',
|
||||
c_pluginId: context.pluginId,
|
||||
c_releaseNum: context.releaseNum,
|
||||
c_extraMetric: 'not a number',
|
||||
@@ -262,7 +313,11 @@ describe('GoogleAnalytics4', () => {
|
||||
const optionalConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: { measurementId: measurementId, testMode: true, identity: 'optional' },
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
identity: 'optional',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -288,7 +343,11 @@ describe('GoogleAnalytics4', () => {
|
||||
const optionalConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: { measurementId: measurementId, testMode: true, identity: 'optional' },
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
identity: 'optional',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -317,7 +376,11 @@ describe('GoogleAnalytics4', () => {
|
||||
const disabledConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: { measurementId: measurementId, testMode: true, identity: 'disabled' },
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
identity: 'disabled',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -332,9 +395,11 @@ describe('GoogleAnalytics4', () => {
|
||||
await new Promise(resolve => setTimeout(resolve));
|
||||
|
||||
// A pageview should have been fired immediately.
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/',
|
||||
category: 'App',
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
// There should not have been a UserID set.
|
||||
@@ -346,7 +411,11 @@ describe('GoogleAnalytics4', () => {
|
||||
const requiredConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: { measurementId: measurementId, testMode: true, identity: 'required' },
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
identity: 'required',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -359,7 +428,11 @@ describe('GoogleAnalytics4', () => {
|
||||
const requiredConfig = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga4: { measurementId: measurementId, testMode: true, identity: 'required' },
|
||||
ga4: {
|
||||
measurementId: measurementId,
|
||||
testMode: true,
|
||||
identity: 'required',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -387,20 +460,21 @@ describe('GoogleAnalytics4', () => {
|
||||
});
|
||||
|
||||
// Then a pageview should have been fired with a queue time.
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/',
|
||||
timestamp_micros: expect.any(Number),
|
||||
value: undefined,
|
||||
category: 'App',
|
||||
});
|
||||
|
||||
// Then an event should have been fired with a queue time.
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'event',
|
||||
expect(fnEvent).toHaveBeenCalledWith('test', {
|
||||
action: 'test',
|
||||
timestamp_micros: expect.any(Number),
|
||||
eventAction: 'test',
|
||||
eventCategory: 'App',
|
||||
eventLabel: 'some label',
|
||||
eventValue: undefined,
|
||||
label: 'some label',
|
||||
category: 'App',
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
// And subsequent hits should not have a queue time.
|
||||
@@ -410,9 +484,11 @@ describe('GoogleAnalytics4', () => {
|
||||
context,
|
||||
});
|
||||
|
||||
expect(fnSend).toHaveBeenCalledWith({
|
||||
hitType: 'pageview',
|
||||
page: '/page-2',
|
||||
expect(fnEvent).toHaveBeenCalledWith('page_view', {
|
||||
action: 'page_view',
|
||||
label: '/page-2',
|
||||
category: 'App',
|
||||
value: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+41
-10
@@ -1,3 +1,18 @@
|
||||
/*
|
||||
* Copyright 2023 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-ga4';
|
||||
import {
|
||||
AnalyticsApi,
|
||||
@@ -7,7 +22,7 @@ import {
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DeferredCapture } from '../../../util';
|
||||
import { DeferredCapture } from '../../../util/DeferredCapture';
|
||||
|
||||
/**
|
||||
* Google Analytics API provider for the Backstage Analytics API.
|
||||
@@ -21,7 +36,6 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
private readonly contentGroupBy?: string;
|
||||
private readonly allowedContexts?: string[];
|
||||
private readonly allowedAttributes?: string[];
|
||||
|
||||
/**
|
||||
* Instantiate the implementation and initialize ReactGA.
|
||||
* @param options initializes Google Analytics module with the config
|
||||
@@ -60,9 +74,8 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
});
|
||||
|
||||
this.contentGroupBy = contentGroupBy;
|
||||
this.allowedContexts = allowedContexts;
|
||||
this.allowedAttributes = allowedAttributes;
|
||||
|
||||
this.allowedContexts = allowedContexts;
|
||||
// If identity is required, defer event capture until identity is known.
|
||||
this.capture = new DeferredCapture({ defer: identity === 'required' });
|
||||
|
||||
@@ -82,8 +95,8 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
|
||||
/**
|
||||
* Instantiate a fully configured GA Analytics API implementation.
|
||||
* @param config - Config object from app config
|
||||
* @param options - options with identityApi and userIdTransform config
|
||||
* @param config Config object from app config
|
||||
* @param options options with identityApi and userIdTransform config
|
||||
*/
|
||||
static fromConfig(
|
||||
config: Config,
|
||||
@@ -135,7 +148,7 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
* 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).
|
||||
* @param event - AnalyticsEvent type captured
|
||||
* @param event AnalyticsEvent type captured
|
||||
*/
|
||||
captureEvent(event: AnalyticsEvent) {
|
||||
const { context, action, subject, value, attributes } = event;
|
||||
@@ -145,7 +158,15 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
}
|
||||
|
||||
if (action === 'navigate' && context.extension === 'App') {
|
||||
this.capture.pageview(subject, customEventData);
|
||||
this.capture.event(
|
||||
{
|
||||
category: context.extension || 'App',
|
||||
action: 'page_view',
|
||||
label: subject,
|
||||
value,
|
||||
},
|
||||
customEventData,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,17 +199,27 @@ export class GoogleAnalytics4 implements AnalyticsApi {
|
||||
[x: string]: string | number | boolean | undefined;
|
||||
} = {};
|
||||
|
||||
this.allowedContexts?.forEach(ctx => {
|
||||
const contextKeys =
|
||||
this.allowedContexts?.join('') === '*'
|
||||
? Object.keys(context)
|
||||
: this.allowedContexts;
|
||||
|
||||
contextKeys?.forEach(ctx => {
|
||||
if (context[ctx]) {
|
||||
customEventParameters[`c_${ctx}`] = context[ctx];
|
||||
}
|
||||
});
|
||||
|
||||
this.allowedAttributes?.forEach(attr => {
|
||||
const attrKeys =
|
||||
this.allowedAttributes?.join('') === '*'
|
||||
? Object.keys(attributes)
|
||||
: this.allowedAttributes;
|
||||
attrKeys?.forEach(attr => {
|
||||
if (attributes[attr]) {
|
||||
customEventParameters[`a_${attr}`] = attributes[attr];
|
||||
}
|
||||
});
|
||||
|
||||
return customEventParameters;
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 { GoogleAnalytics4 } from './GoogleAnalytics4';
|
||||
|
||||
@@ -1,2 +1,16 @@
|
||||
export { analyticsModuleGA4 } from './plugin';
|
||||
/*
|
||||
* Copyright 2023 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 * from './apis/implementations/AnalyticsApi';
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* 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 { analyticsModuleGA4 } from './plugin';
|
||||
|
||||
describe('google-analytics', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(analyticsModuleGA4).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,26 +0,0 @@
|
||||
/*
|
||||
* 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 { createPlugin } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* @deprecated Importing and including this plugin in an app has no effect.
|
||||
* This will be removed in a future release.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const analyticsModuleGA4 = createPlugin({
|
||||
id: 'analytics-provider-ga4',
|
||||
});
|
||||
@@ -1,16 +1,29 @@
|
||||
/*
|
||||
* Copyright 2023 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-ga4';
|
||||
|
||||
import { UaEventOptions } from 'react-ga4/types/ga4';
|
||||
|
||||
type Hit = {
|
||||
hitType: string;
|
||||
data: {
|
||||
hitType: 'pageview' | 'event';
|
||||
[x: string]: any;
|
||||
};
|
||||
};
|
||||
|
||||
const PageViewEvent = 'pageview';
|
||||
|
||||
/**
|
||||
* A wrapper around ReactGA that can optionally handle latent capture logic.
|
||||
*
|
||||
@@ -46,32 +59,6 @@ export class DeferredCapture {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Either forwards the pageview directly to GA, or (if configured) enqueues
|
||||
* the pageview hit to be captured when ready.
|
||||
* @param path pageview path
|
||||
* @param metadata any object that can be passed as additional parameter to the event
|
||||
*/
|
||||
pageview(path: string, metadata: any = {}) {
|
||||
if (this.queue) {
|
||||
this.queue.push({
|
||||
data: {
|
||||
hitType: PageViewEvent,
|
||||
timestamp_micros: Date.now() * 1000,
|
||||
page: path,
|
||||
...metadata,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
ReactGA.send({
|
||||
hitType: PageViewEvent,
|
||||
page: path,
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Either forwards the event directly to GA, or (if configured) enqueues the
|
||||
* event hit to be captured when ready.
|
||||
@@ -80,15 +67,12 @@ export class DeferredCapture {
|
||||
*/
|
||||
event(eventDetails: UaEventOptions, metadata: any = {}) {
|
||||
const data = {
|
||||
hitType: 'event',
|
||||
eventCategory: eventDetails.category,
|
||||
eventLabel: eventDetails.label!,
|
||||
eventAction: eventDetails.action,
|
||||
eventValue: eventDetails.value,
|
||||
...eventDetails,
|
||||
...metadata,
|
||||
};
|
||||
if (this.queue) {
|
||||
this.queue.push({
|
||||
hitType: eventDetails.action,
|
||||
data: {
|
||||
...data,
|
||||
timestamp_micros: Date.now() * 1000,
|
||||
@@ -96,7 +80,7 @@ export class DeferredCapture {
|
||||
});
|
||||
return;
|
||||
}
|
||||
ReactGA.send(data);
|
||||
ReactGA.event(eventDetails.action, data);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,7 +89,7 @@ export class DeferredCapture {
|
||||
*/
|
||||
private sendDeferred(hit: Hit) {
|
||||
// Send the hit with the appropriate queue time (`qt`).
|
||||
ReactGA.send({
|
||||
ReactGA.event(hit.hitType, {
|
||||
...hit.data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +1,16 @@
|
||||
/*
|
||||
* Copyright 2023 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 { DeferredCapture } from './DeferredCapture';
|
||||
|
||||
Reference in New Issue
Block a user