Simplify Analytics API from analytics module POV

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2021-08-13 16:44:15 +02:00
parent edd5293d68
commit dffa1892cd
9 changed files with 100 additions and 86 deletions
+1 -5
View File
@@ -38,11 +38,7 @@ export type AlertMessage = {
//
// @public
export type AnalyticsApi = {
getDecoratedTracker({
domain,
}: {
domain: AnalyticsDomainValue;
}): AnalyticsTracker;
captureEvent(event: DomainDecoratedAnalyticsEvent): void;
};
// Warning: (ae-missing-release-tag) "analyticsApiRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -35,27 +35,27 @@ describe('useAnalytics', () => {
});
it('returns tracker from defined analytics api', () => {
const expectedFunction = 'capture function';
const getDecoratedTracker = jest.fn().mockReturnValue({
captureEvent: expectedFunction,
});
const captureEvent = jest.fn();
// Simulate useApi returning a valid tracker.
mocked(useApi).mockReturnValue({ getDecoratedTracker });
mocked(useApi).mockReturnValue({ captureEvent });
// The getDecoratedTracker method of the underlying implementation should
// have been called with the domain provided.
// Calling the captureEvent method of the underlying implementation should
// pass along the given event as well as the default domain.
const { result } = renderHook(() => useAnalytics());
expect(getDecoratedTracker).toHaveBeenCalledWith({
result.current.captureEvent('a verb', 'a noun', 42, { some: 'value' });
expect(captureEvent).toHaveBeenCalledWith({
verb: 'a verb',
noun: 'a noun',
value: 42,
context: {
some: 'value',
},
domain: {
componentName: 'App',
pluginId: 'root',
routeRef: 'unknown',
},
});
// And the returned tracker's captureEvent should have come from the API
// implementation.
expect(result.current.captureEvent).toBe(expectedFunction);
});
});
@@ -24,7 +24,21 @@ import { useApi } from '../apis';
function useTracker(): AnalyticsTracker {
const analyticsApi = useApi(analyticsApiRef);
const domain = useAnalyticsDomain();
return analyticsApi.getDecoratedTracker({ domain });
return {
captureEvent: (verb, noun, value, context) => {
try {
analyticsApi.captureEvent({
verb,
noun,
value,
context,
domain,
});
} catch {
// Just don't throw on an instrumentation problem.
}
},
};
}
/**
@@ -109,13 +109,10 @@ export type AnalyticsTracker = {
*/
export type AnalyticsApi = {
/**
* Retrieves a tracker decorated with a given analytics domain.
* Primary event handler responsible for compiling and forwarding events to
* an analytics system.
*/
getDecoratedTracker({
domain,
}: {
domain: AnalyticsDomainValue;
}): AnalyticsTracker;
captureEvent(event: DomainDecoratedAnalyticsEvent): void;
};
export const analyticsApiRef: ApiRef<AnalyticsApi> = createApiRef({
+6 -6
View File
@@ -4,8 +4,6 @@
```ts
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsDomainValue } from '@backstage/core-plugin-api';
import { AnalyticsTracker } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { DomainDecoratedAnalyticsEvent } from '@backstage/core-plugin-api';
import { ErrorApi } from '@backstage/core-plugin-api';
@@ -24,11 +22,13 @@ import { StorageValueChange } from '@backstage/core-plugin-api';
// @public (undocumented)
export class MockAnalyticsApi implements AnalyticsApi {
// (undocumented)
getDecoratedTracker({
captureEvent({
verb,
noun,
value,
context,
domain,
}: {
domain: AnalyticsDomainValue;
}): AnalyticsTracker;
}: DomainDecoratedAnalyticsEvent): void;
// (undocumented)
getEvents(): DomainDecoratedAnalyticsEvent[];
}
@@ -23,11 +23,16 @@ describe('MockAnalyticsApi', () => {
it('should collect events', () => {
const api = new MockAnalyticsApi();
const tracker = api.getDecoratedTracker({ domain });
tracker.captureEvent('verb-1', 'noun-1');
tracker.captureEvent('verb-2', 'noun-2', 42);
tracker.captureEvent('verb-3', 'noun-3', 1337, { some: 'context' });
api.captureEvent({ verb: 'verb-1', noun: 'noun-1', domain });
api.captureEvent({ verb: 'verb-2', noun: 'noun-2', value: 42, domain });
api.captureEvent({
verb: 'verb-3',
noun: 'noun-3',
value: 1337,
context: { some: 'context' },
domain,
});
expect(api.getEvents()[0]).toMatchObject({
noun: 'noun-1',
@@ -16,30 +16,26 @@
import {
AnalyticsApi,
AnalyticsDomainValue,
AnalyticsTracker,
DomainDecoratedAnalyticsEvent,
} from '@backstage/core-plugin-api';
export class MockAnalyticsApi implements AnalyticsApi {
private events: DomainDecoratedAnalyticsEvent[] = [];
getDecoratedTracker({
captureEvent({
verb,
noun,
value,
context,
domain,
}: {
domain: AnalyticsDomainValue;
}): AnalyticsTracker {
return {
captureEvent: (verb, noun, value, context) => {
this.events.push({
verb,
noun,
domain,
...(value !== undefined ? { value } : {}),
...(context !== undefined ? { context } : {}),
});
},
};
}: DomainDecoratedAnalyticsEvent) {
this.events.push({
verb,
noun,
domain,
...(value !== undefined ? { value } : {}),
...(context !== undefined ? { context } : {}),
});
}
getEvents(): DomainDecoratedAnalyticsEvent[] {
@@ -38,7 +38,7 @@ describe('GoogleAnalytics', () => {
it('returns implementation', () => {
const api = GoogleAnalytics.fromConfig(basicValidConfig);
expect(api.getDecoratedTracker).toBeDefined();
expect(api.captureEvent).toBeDefined();
// Initializes GA with tracking ID.
expect(ReactGA.testModeAPI.calls[0]).toEqual([
@@ -94,8 +94,11 @@ describe('GoogleAnalytics', () => {
it('tracks basic pageview', () => {
const api = GoogleAnalytics.fromConfig(basicValidConfig);
const tracker = api.getDecoratedTracker({ domain });
tracker.captureEvent('navigate', '/');
api.captureEvent({
verb: 'navigate',
noun: '/',
domain,
});
const [command, data] = ReactGA.testModeAPI.calls[1];
expect(command).toBe('send');
@@ -107,12 +110,16 @@ describe('GoogleAnalytics', () => {
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);
api.captureEvent({
verb: expectedAction,
noun: expectedLabel,
value: expectedValue,
domain,
});
const [command, data] = ReactGA.testModeAPI.calls[1];
expect(command).toBe('send');
@@ -127,8 +134,11 @@ describe('GoogleAnalytics', () => {
it('captures configured custom dimensions/metrics on pageviews', () => {
const api = GoogleAnalytics.fromConfig(advancedConfig);
const tracker = api.getDecoratedTracker({ domain });
tracker.captureEvent('navigate', '/a-page');
api.captureEvent({
verb: 'navigate',
noun: '/a-page',
domain,
});
// Expect a set command first.
const [setCommand, setData] = ReactGA.testModeAPI.calls[1];
@@ -149,14 +159,19 @@ describe('GoogleAnalytics', () => {
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,
api.captureEvent({
verb: expectedAction,
noun: expectedLabel,
value: expectedValue,
context: {
extraDimension: false,
extraMetric: 0,
},
domain,
});
const [command, data] = ReactGA.testModeAPI.calls[1];
@@ -176,10 +191,14 @@ describe('GoogleAnalytics', () => {
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',
api.captureEvent({
verb: 'verb',
noun: 'noun',
context: {
extraMetric: 'not a number',
},
domain,
});
const [, data] = ReactGA.testModeAPI.calls[1];
@@ -17,9 +17,9 @@
import ReactGA from 'react-ga';
import {
AnalyticsApi,
AnalyticsTracker,
AnalyticsDomainValue,
AnalyticsEventContext,
DomainDecoratedAnalyticsEvent,
} from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
@@ -92,31 +92,18 @@ export class GoogleAnalytics implements AnalyticsApi {
});
}
/**
* 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,
) {
captureEvent({
domain,
verb,
noun,
value,
context,
}: DomainDecoratedAnalyticsEvent) {
const customMetadata = this.getCustomDimensionMetrics(domain, context);
if (verb === 'navigate' && domain?.componentName === 'App') {