Add support for sending virtual pageviews on search events
Signed-off-by: Nikita Karpukhin <nikita.karpukhin@scout24.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-analytics-module-ga': minor
|
||||
---
|
||||
|
||||
Added support for sending virtual pageviews on `search` events
|
||||
@@ -169,6 +169,31 @@ export const apis: AnyApiFactory[] = [
|
||||
];
|
||||
```
|
||||
|
||||
### Enabling Site Search
|
||||
|
||||
If you wish to see all of the search events in the [Site Search](https://support.google.com/analytics/answer/1012264)
|
||||
section of Google Analytics, you can enable sending virtual pageviews on every `search` event like so:
|
||||
|
||||
```yaml
|
||||
app:
|
||||
analytics:
|
||||
ga:
|
||||
virtualSearchPageView:
|
||||
mode: only # Defaults to 'disabled'
|
||||
mountPath: /virtual-search # Defaults to '/search'
|
||||
queryParam: term # Defaults to 'query'
|
||||
```
|
||||
|
||||
Available `mode`s are:
|
||||
|
||||
- `disabled` - no virtual pageviews are sent, default behavior
|
||||
- `only` - sends virtual pageviews _instead_ of `search` events
|
||||
- `both` - sends both virtual pageviews _and_ `search` events
|
||||
|
||||
Virtual pageviews will be sent to the path specified in the `mountPath`, and the search term will be
|
||||
set as the value for query parameter `queryParam`, e.g. the example config above will result in
|
||||
virtual pageviews being sent to `/virtual-search?term=SearchTermHere`.
|
||||
|
||||
### Debugging and Testing
|
||||
|
||||
In pre-production environments, you may wish to set additional configurations
|
||||
|
||||
+26
@@ -53,6 +53,32 @@ 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 in the virtual pageview URL.
|
||||
* Defaults to `query`.
|
||||
* @visibility frontend
|
||||
*/
|
||||
queryParam?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether or not to log analytics debug statements to the console.
|
||||
* Defaults to false.
|
||||
|
||||
+93
@@ -152,6 +152,99 @@ describe('GoogleAnalytics', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('captures virtual pageviews', () => {
|
||||
const config = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga: {
|
||||
trackingId,
|
||||
testMode: true,
|
||||
virtualSearchPageView: { mode: 'only' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const api = GoogleAnalytics.fromConfig(config);
|
||||
api.captureEvent({
|
||||
action: 'search',
|
||||
subject: 'test search',
|
||||
context,
|
||||
});
|
||||
|
||||
const [command, data] = ReactGA.testModeAPI.calls[1];
|
||||
expect(command).toBe('send');
|
||||
expect(data).toMatchObject({
|
||||
hitType: 'pageview',
|
||||
page: '/search?query=test search',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures virtual pageviews alongside search events', () => {
|
||||
const config = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga: {
|
||||
trackingId,
|
||||
testMode: true,
|
||||
virtualSearchPageView: { mode: 'both' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const api = GoogleAnalytics.fromConfig(config);
|
||||
api.captureEvent({
|
||||
action: 'search',
|
||||
subject: 'test search',
|
||||
context,
|
||||
});
|
||||
|
||||
const [pageviewCommand, pageViewData] = ReactGA.testModeAPI.calls[1];
|
||||
expect(pageviewCommand).toBe('send');
|
||||
expect(pageViewData).toMatchObject({
|
||||
hitType: 'pageview',
|
||||
page: '/search?query=test search',
|
||||
});
|
||||
const [searchCommand, searchData] = ReactGA.testModeAPI.calls[2];
|
||||
expect(searchCommand).toBe('send');
|
||||
expect(searchData).toMatchObject({
|
||||
hitType: 'event',
|
||||
eventCategory: context.extension,
|
||||
eventAction: 'search',
|
||||
eventLabel: 'test search',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures virtual pageviews on custom route with custom query param', () => {
|
||||
const config = new ConfigReader({
|
||||
app: {
|
||||
analytics: {
|
||||
ga: {
|
||||
trackingId,
|
||||
testMode: true,
|
||||
virtualSearchPageView: {
|
||||
mode: 'only',
|
||||
mountPath: '/custom',
|
||||
queryParam: 'term',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const api = GoogleAnalytics.fromConfig(config);
|
||||
api.captureEvent({
|
||||
action: 'search',
|
||||
subject: 'test search',
|
||||
context,
|
||||
});
|
||||
|
||||
const [command, data] = ReactGA.testModeAPI.calls[1];
|
||||
expect(command).toBe('send');
|
||||
expect(data).toMatchObject({
|
||||
hitType: 'pageview',
|
||||
page: '/custom?term=test search',
|
||||
});
|
||||
});
|
||||
|
||||
it('captures configured custom dimensions/metrics on events', () => {
|
||||
const api = GoogleAnalytics.fromConfig(advancedConfig);
|
||||
|
||||
|
||||
+24
-1
@@ -18,12 +18,16 @@ import ReactGA from 'react-ga';
|
||||
import {
|
||||
AnalyticsApi,
|
||||
AnalyticsContextValue,
|
||||
AnalyticsEventAttributes,
|
||||
AnalyticsEvent,
|
||||
AnalyticsEventAttributes,
|
||||
IdentityApi,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { Config } from '@backstage/config';
|
||||
import { DeferredCapture } from '../../../util';
|
||||
import {
|
||||
parseVirtualSearchPageViewConfig,
|
||||
VirtualSearchPageViewConfig,
|
||||
} from '../../../util/VirtualSearchPageView';
|
||||
|
||||
type CustomDimensionOrMetricConfig = {
|
||||
type: 'dimension' | 'metric';
|
||||
@@ -40,6 +44,7 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
private readonly cdmConfig: CustomDimensionOrMetricConfig[];
|
||||
private customUserIdTransform?: (userEntityRef: string) => Promise<string>;
|
||||
private readonly capture: DeferredCapture;
|
||||
private readonly virtualSearchPageView: VirtualSearchPageViewConfig;
|
||||
|
||||
/**
|
||||
* Instantiate the implementation and initialize ReactGA.
|
||||
@@ -51,6 +56,7 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
identity: string;
|
||||
trackingId: string;
|
||||
scriptSrc?: string;
|
||||
virtualSearchPageView: VirtualSearchPageViewConfig;
|
||||
testMode: boolean;
|
||||
debug: boolean;
|
||||
}) {
|
||||
@@ -61,11 +67,13 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
identityApi,
|
||||
userIdTransform = 'sha-256',
|
||||
scriptSrc,
|
||||
virtualSearchPageView,
|
||||
testMode,
|
||||
debug,
|
||||
} = options;
|
||||
|
||||
this.cdmConfig = cdmConfig;
|
||||
this.virtualSearchPageView = virtualSearchPageView;
|
||||
|
||||
// Initialize Google Analytics.
|
||||
ReactGA.initialize(trackingId, {
|
||||
@@ -105,6 +113,9 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
const scriptSrc = config.getOptionalString('app.analytics.ga.scriptSrc');
|
||||
const identity =
|
||||
config.getOptionalString('app.analytics.ga.identity') || 'disabled';
|
||||
const virtualSearchPageView = parseVirtualSearchPageViewConfig(
|
||||
config.getOptionalConfig('app.analytics.ga.virtualSearchPageView'),
|
||||
);
|
||||
const debug = config.getOptionalBoolean('app.analytics.ga.debug') ?? false;
|
||||
const testMode =
|
||||
config.getOptionalBoolean('app.analytics.ga.testMode') ?? false;
|
||||
@@ -134,6 +145,7 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
identity,
|
||||
trackingId,
|
||||
scriptSrc,
|
||||
virtualSearchPageView,
|
||||
cdmConfig,
|
||||
testMode,
|
||||
debug,
|
||||
@@ -154,6 +166,17 @@ export class GoogleAnalytics implements AnalyticsApi {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.virtualSearchPageView.mode !== 'disabled' && action === 'search') {
|
||||
const { mountPath, queryParam } = this.virtualSearchPageView;
|
||||
this.capture.pageview(
|
||||
`${mountPath}?${queryParam}=${subject}`,
|
||||
customMetadata,
|
||||
);
|
||||
if (this.virtualSearchPageView.mode === 'only') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.capture.event({
|
||||
category: context.extension || 'App',
|
||||
action,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { Config } from '@backstage/config';
|
||||
|
||||
type VirtualSearchPageViewType = 'disabled' | 'only' | 'both';
|
||||
|
||||
export type VirtualSearchPageViewConfig = {
|
||||
mode: VirtualSearchPageViewType;
|
||||
mountPath: string;
|
||||
queryParam: string;
|
||||
};
|
||||
|
||||
function isVirtualSearchPageViewType(
|
||||
value: string | undefined,
|
||||
): value is VirtualSearchPageViewType {
|
||||
return value === 'disabled' || value === 'only' || value === 'both';
|
||||
}
|
||||
|
||||
export function parseVirtualSearchPageViewConfig(
|
||||
config: Config | undefined,
|
||||
): VirtualSearchPageViewConfig {
|
||||
const vspvModeString = config?.getOptionalString('mode');
|
||||
return {
|
||||
mode: isVirtualSearchPageViewType(vspvModeString)
|
||||
? vspvModeString
|
||||
: 'disabled',
|
||||
mountPath: config?.getOptionalString('mountPath') ?? '/search',
|
||||
queryParam: config?.getOptionalString('queryParam') ?? 'query',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user