Merge pull request #22482 from backstage/camilaibs/fix-analytics-context

[DI] Add analytics api  backward compatibility
This commit is contained in:
Patrik Oldsberg
2024-01-30 13:51:28 +01:00
committed by GitHub
34 changed files with 555 additions and 45 deletions
+4 -2
View File
@@ -4,7 +4,9 @@
```ts
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api';
import { AnalyticsEvent } from '@backstage/core-plugin-api';
import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
@@ -13,8 +15,8 @@ import { IdentityApi } from '@backstage/core-plugin-api';
export const analyticsModuleGA: BackstagePlugin<{}, {}>;
// @public
export class GoogleAnalytics implements AnalyticsApi {
captureEvent(event: AnalyticsEvent): void;
export class GoogleAnalytics implements AnalyticsApi, AnalyticsApi_2 {
captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void;
static fromConfig(
config: Config,
options?: {
+1
View File
@@ -32,6 +32,7 @@
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"react-ga": "^3.3.0"
},
"peerDependencies": {
@@ -508,4 +508,133 @@ describe('GoogleAnalytics', () => {
expect(lastData.queueTime).toBeUndefined();
});
});
describe('api backward compatibility', () => {
it('continue working with legacy App category', () => {
const api = GoogleAnalytics.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context,
});
let [command, data] = ReactGA.testModeAPI.calls[1];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'pageview',
page: '/',
});
api.captureEvent({
action: 'click',
subject: 'on something',
value: 42,
context,
});
[command, data] = ReactGA.testModeAPI.calls[2];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'event',
// expect to use the legacy default category
eventCategory: 'App',
eventAction: 'click',
eventLabel: 'on something',
eventValue: 42,
});
});
it('use lowercase app as the new default category', () => {
const api = GoogleAnalytics.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context: {
...context,
extensionId: '',
extension: '',
},
});
let [command, data] = ReactGA.testModeAPI.calls[1];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'pageview',
page: '/',
});
api.captureEvent({
action: 'click',
subject: 'on something',
value: 42,
context: {
...context,
extensionId: '',
extension: '',
},
});
[command, data] = ReactGA.testModeAPI.calls[2];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'event',
// expect to use the new default category
eventCategory: 'App',
eventAction: 'click',
eventLabel: 'on something',
eventValue: 42,
});
});
it('prioritize new context extension id over old extension property', () => {
const api = GoogleAnalytics.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context: {
...context,
extensionId: 'app',
extension: '',
},
});
let [command, data] = ReactGA.testModeAPI.calls[1];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'pageview',
page: '/',
});
api.captureEvent({
action: 'click',
subject: 'on something',
value: 42,
context: {
...context,
extensionId: 'page:index',
extension: '',
},
});
[command, data] = ReactGA.testModeAPI.calls[2];
expect(command).toBe('send');
expect(data).toMatchObject({
hitType: 'event',
// expect use the new context extension id
eventCategory: 'page:index',
eventAction: 'click',
eventLabel: 'on something',
eventValue: 42,
});
});
});
});
@@ -22,6 +22,11 @@ import {
AnalyticsEventAttributes,
IdentityApi,
} from '@backstage/core-plugin-api';
import {
AnalyticsApi as NewAnalyticsApi,
AnalyticsEvent as NewAnalyticsEvent,
AnalyticsContextValue as NewAnalyticsContextValue,
} from '@backstage/frontend-plugin-api';
import { Config } from '@backstage/config';
import { DeferredCapture } from '../../../util';
import {
@@ -40,7 +45,7 @@ type CustomDimensionOrMetricConfig = {
* Google Analytics API provider for the Backstage Analytics API.
* @public
*/
export class GoogleAnalytics implements AnalyticsApi {
export class GoogleAnalytics implements AnalyticsApi, NewAnalyticsApi {
private readonly cdmConfig: CustomDimensionOrMetricConfig[];
private customUserIdTransform?: (userEntityRef: string) => Promise<string>;
private readonly capture: DeferredCapture;
@@ -157,11 +162,18 @@ export class GoogleAnalytics implements AnalyticsApi {
* pageview and the rest as custom events. All custom dimensions/metrics are
* applied as they should be (set on pageview, merged object on events).
*/
captureEvent(event: AnalyticsEvent) {
captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) {
const { context, action, subject, value, attributes } = event;
const customMetadata = this.getCustomDimensionMetrics(context, attributes);
if (action === 'navigate' && context.extension === 'App') {
const extensionId = context.extensionId || context.extension;
const category = extensionId ? String(extensionId) : 'App';
// The legacy default extension was 'App' and the new one is 'app'
if (
action === 'navigate' &&
category.toLocaleLowerCase('en-US').startsWith('app')
) {
this.capture.pageview(subject, customMetadata);
return;
}
@@ -184,7 +196,7 @@ export class GoogleAnalytics implements AnalyticsApi {
}
this.capture.event({
category: context.extension || 'App',
category,
action,
label: subject,
value,
@@ -197,7 +209,7 @@ export class GoogleAnalytics implements AnalyticsApi {
* Event Attributes, e.g. { dimension1: "some value", metric8: 42 }
*/
private getCustomDimensionMetrics(
context: AnalyticsContextValue,
context: AnalyticsContextValue | NewAnalyticsContextValue,
attributes: AnalyticsEventAttributes = {},
) {
const customDimensionsMetrics: { [x: string]: string | number | boolean } =
+4 -2
View File
@@ -4,13 +4,15 @@
```ts
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api';
import { AnalyticsEvent } from '@backstage/core-plugin-api';
import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api';
import { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
// @public
export class GoogleAnalytics4 implements AnalyticsApi {
captureEvent(event: AnalyticsEvent): void;
export class GoogleAnalytics4 implements AnalyticsApi, AnalyticsApi_2 {
captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void;
static fromConfig(
config: Config,
options?: {
@@ -32,6 +32,7 @@
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"react-ga4": "^2.0.0"
},
"peerDependencies": {
@@ -492,4 +492,60 @@ describe('GoogleAnalytics4', () => {
});
});
});
describe('api backward compatibility', () => {
it('continue working with legacy App category', () => {
const api = GoogleAnalytics4.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context,
});
expect(fnEvent).toHaveBeenCalledWith('page_view', {
action: 'page_view',
label: '/',
category: 'App',
});
});
it('use lowercase app as the new default category', () => {
const api = GoogleAnalytics4.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context: { ...context, extensionId: '', extension: '' },
});
expect(fnEvent).toHaveBeenCalledWith('page_view', {
action: 'page_view',
label: '/',
category: 'App',
});
});
it('prioritize new context extension id over old extension property', () => {
const api = GoogleAnalytics4.fromConfig(basicValidConfig);
expect(api.captureEvent).toBeDefined();
api.captureEvent({
action: 'navigate',
subject: '/',
context: { ...context, extensionId: 'app', extension: '' },
});
expect(fnEvent).toHaveBeenCalledWith('page_view', {
action: 'page_view',
label: '/',
category: 'app',
});
});
});
});
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import ReactGA from 'react-ga4';
import {
AnalyticsApi,
@@ -21,6 +22,11 @@ import {
AnalyticsEvent,
IdentityApi,
} from '@backstage/core-plugin-api';
import {
AnalyticsApi as NewAnalyticsApi,
AnalyticsEvent as NewAnalyticsEvent,
AnalyticsContextValue as NewAnalyticsContextValue,
} from '@backstage/frontend-plugin-api';
import { Config } from '@backstage/config';
import { DeferredCapture } from '../../../util/DeferredCapture';
@@ -28,7 +34,7 @@ import { DeferredCapture } from '../../../util/DeferredCapture';
* Google Analytics API provider for the Backstage Analytics API.
* @public
*/
export class GoogleAnalytics4 implements AnalyticsApi {
export class GoogleAnalytics4 implements AnalyticsApi, NewAnalyticsApi {
private readonly customUserIdTransform?: (
userEntityRef: string,
) => Promise<string>;
@@ -157,17 +163,24 @@ export class GoogleAnalytics4 implements AnalyticsApi {
* applied as they should be (set on pageview, merged object on events).
* @param event - AnalyticsEvent type captured
*/
captureEvent(event: AnalyticsEvent) {
captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) {
const { context, action, subject, value, attributes } = event;
const customEventData = this.setEventParameters(context, attributes);
if (this.contentGroupBy) {
customEventData.content_group = context[this.contentGroupBy]!;
}
if (action === 'navigate' && context.extension === 'App') {
const extensionId = context.extensionId || context.extension;
const category = extensionId ? String(extensionId) : 'App';
// The legacy default extension was 'App' and the new one is 'app'
if (
action === 'navigate' &&
category.toLocaleLowerCase('en-US').startsWith('app')
) {
this.capture.event(
{
category: context.extension || 'App',
category,
action: 'page_view',
label: subject,
value,
@@ -183,7 +196,7 @@ export class GoogleAnalytics4 implements AnalyticsApi {
this.capture.event(
{
category: context.extension || 'App',
category,
action,
label: subject,
value,
@@ -199,7 +212,7 @@ export class GoogleAnalytics4 implements AnalyticsApi {
* @param attributes additional analytics event attributes
*/
private setEventParameters(
context: AnalyticsContextValue,
context: AnalyticsContextValue | NewAnalyticsContextValue,
attributes: AnalyticsEventAttributes = {},
) {
const customEventParameters: {
@@ -4,14 +4,16 @@
```ts
import { AnalyticsApi } from '@backstage/core-plugin-api';
import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api';
import { AnalyticsEvent } from '@backstage/core-plugin-api';
import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api';
import { Config } from '@backstage/config';
import { IdentityApi } from '@backstage/core-plugin-api';
// @public
export class NewRelicBrowser implements AnalyticsApi {
export class NewRelicBrowser implements AnalyticsApi, AnalyticsApi_2 {
// (undocumented)
captureEvent(event: AnalyticsEvent): void;
captureEvent(event: AnalyticsEvent | AnalyticsEvent_2): void;
// (undocumented)
static fromConfig(
config: Config,
@@ -26,6 +26,7 @@
"@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@newrelic/browser-agent": "^1.236.0"
},
"peerDependencies": {
@@ -19,6 +19,10 @@ import {
IdentityApi,
AnalyticsEvent,
} from '@backstage/core-plugin-api';
import {
AnalyticsApi as NewAnalyicsApi,
AnalyticsEvent as NewAnalyticsEvent,
} from '@backstage/frontend-plugin-api';
import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent';
import type { setAPI } from '@newrelic/browser-agent/loaders/api/api';
@@ -37,7 +41,7 @@ type NewRelicBrowserOptions = {
* New Relic Browser API provider for the Backstage Analytics API.
* @public
*/
export class NewRelicBrowser implements AnalyticsApi {
export class NewRelicBrowser implements AnalyticsApi, NewAnalyicsApi {
private readonly agent: NewRelicAPI;
private constructor(
@@ -121,9 +125,17 @@ export class NewRelicBrowser implements AnalyticsApi {
);
}
captureEvent(event: AnalyticsEvent) {
captureEvent(event: AnalyticsEvent | NewAnalyticsEvent) {
const { context, action, subject, value, attributes } = event;
if (action === 'navigate' && context.extension === 'App') {
const extensionId = context.extensionId || context.extension;
const category = extensionId ? String(extensionId) : 'App';
// The legacy default extension was 'App' and the new one is 'app'
if (
action === 'navigate' &&
category.toLocaleLowerCase('en-US').startsWith('app')
) {
const interaction = this.agent.interaction();
interaction.setName(subject);
if (value) {