From 99388e65ae32c5ee0fb13e88d180d5953930c62d Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:02:32 -0700 Subject: [PATCH 01/11] feat: adding user telemetry config and service --- package-lock.json | 86 +++++++---- package.json | 2 + projects/common/package.json | 4 +- projects/common/src/telemetry/telemetry.ts | 33 ++++ .../user-telemetry-internal.service.ts | 144 ++++++++++++++++++ .../src/telemetry/user-telemetry.module.ts | 45 ++++++ .../src/telemetry/user-telemetry.service.ts | 16 ++ 7 files changed, 297 insertions(+), 33 deletions(-) create mode 100644 projects/common/src/telemetry/telemetry.ts create mode 100644 projects/common/src/telemetry/user-telemetry-internal.service.ts create mode 100644 projects/common/src/telemetry/user-telemetry.module.ts create mode 100644 projects/common/src/telemetry/user-telemetry.service.ts diff --git a/package-lock.json b/package-lock.json index 9cef9162d..15b28f596 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^12.2.1", "@angular/router": "^12.2.1", "@apollo/client": "^3.4.9", + "@fullstory/browser": "^1.4.9", "@hypertrace/hyperdash": "^1.2.1", "@hypertrace/hyperdash-angular": "^2.6.0", "@types/d3-hierarchy": "^2.0.0", @@ -47,6 +48,7 @@ "graphql-tag": "^2.12.5", "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", + "mixpanel-browser": "^2.41.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", @@ -970,9 +972,9 @@ } }, "node_modules/@apollo/client": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.9.tgz", - "integrity": "sha512-1AlYjRJ/ktDApEUEP2DqHI38tqSyhSlsF/Q3fFb/aCbLHQfcSZ1dCv7ZlC9UXRyDwQYc0w23gYJ7wZde6W8P4A==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.11.tgz", + "integrity": "sha512-+A0z/Vy7sDg1uyijv3t9w1U0ybxn0bSpMUZHpsb2cLg/zM8fEHQ217226buzJ+cPUA1GVfJ8n6JsiN26RchvNA==", "dependencies": { "@graphql-typed-document-node/core": "^3.0.0", "@wry/context": "^0.6.0", @@ -985,7 +987,7 @@ "symbol-observable": "^4.0.0", "ts-invariant": "^0.9.0", "tslib": "^2.3.0", - "zen-observable-ts": "^1.1.0" + "zen-observable-ts": "~1.1.0" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0", @@ -3483,6 +3485,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "node_modules/@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -5821,9 +5828,9 @@ } }, "node_modules/@types/node": { - "version": "16.7.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", - "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -8524,9 +8531,9 @@ } }, "node_modules/commander": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", - "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz", + "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==", "dev": true, "engines": { "node": ">= 12" @@ -9116,9 +9123,9 @@ } }, "node_modules/core-js": { - "version": "3.16.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz", - "integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA==", + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", + "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==", "hasInstallScript": true, "funding": { "type": "opencollective", @@ -13094,9 +13101,9 @@ } }, "node_modules/i18next": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.4.0.tgz", - "integrity": "sha512-89iWWJudmaHJwzIdJ/1eu98GtsJnwBhOUWwlAre70itPMuTE/NTPtgVeaS1CGaB8Q3XrYBGpEqlq4jsScDx9kg==", + "version": "20.6.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.0" @@ -20419,6 +20426,11 @@ "node": ">=0.10.0" } }, + "node_modules/mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -30596,9 +30608,9 @@ } }, "@apollo/client": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.9.tgz", - "integrity": "sha512-1AlYjRJ/ktDApEUEP2DqHI38tqSyhSlsF/Q3fFb/aCbLHQfcSZ1dCv7ZlC9UXRyDwQYc0w23gYJ7wZde6W8P4A==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/@apollo/client/-/client-3.4.11.tgz", + "integrity": "sha512-+A0z/Vy7sDg1uyijv3t9w1U0ybxn0bSpMUZHpsb2cLg/zM8fEHQ217226buzJ+cPUA1GVfJ8n6JsiN26RchvNA==", "requires": { "@graphql-typed-document-node/core": "^3.0.0", "@wry/context": "^0.6.0", @@ -30611,7 +30623,7 @@ "symbol-observable": "^4.0.0", "ts-invariant": "^0.9.0", "tslib": "^2.3.0", - "zen-observable-ts": "^1.1.0" + "zen-observable-ts": "~1.1.0" } }, "@assemblyscript/loader": { @@ -32428,6 +32440,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -34323,9 +34340,9 @@ } }, "@types/node": { - "version": "16.7.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", - "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", + "version": "16.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", + "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", "dev": true }, "@types/normalize-package-data": { @@ -36517,9 +36534,9 @@ } }, "commander": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", - "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz", + "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==", "dev": true }, "commitizen": { @@ -37001,9 +37018,9 @@ } }, "core-js": { - "version": "3.16.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.16.3.tgz", - "integrity": "sha512-lM3GftxzHNtPNUJg0v4pC2RC6puwMd6VZA7vXUczi+SKmCWSf4JwO89VJGMqbzmB7jlK7B5hr3S64PqwFL49cA==" + "version": "3.17.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.3.tgz", + "integrity": "sha512-lyvajs+wd8N1hXfzob1LdOCCHFU4bGMbqqmLn1Q4QlCpDqWPpGf+p0nj+LNrvDDG33j0hZXw2nsvvVpHysxyNw==" }, "core-js-compat": { "version": "3.16.2", @@ -40211,9 +40228,9 @@ "dev": true }, "i18next": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.4.0.tgz", - "integrity": "sha512-89iWWJudmaHJwzIdJ/1eu98GtsJnwBhOUWwlAre70itPMuTE/NTPtgVeaS1CGaB8Q3XrYBGpEqlq4jsScDx9kg==", + "version": "20.6.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", + "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", "dev": true, "requires": { "@babel/runtime": "^7.12.0" @@ -45929,6 +45946,11 @@ "is-extendable": "^1.0.1" } }, + "mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", diff --git a/package.json b/package.json index 4bb44f878..7ffcb6ecb 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,8 @@ "@angular/platform-browser-dynamic": "^12.2.1", "@angular/router": "^12.2.1", "@apollo/client": "^3.4.9", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0", "@hypertrace/hyperdash": "^1.2.1", "@hypertrace/hyperdash-angular": "^2.6.0", "@types/d3-hierarchy": "^2.0.0", diff --git a/projects/common/package.json b/projects/common/package.json index e995da500..a05eaa6a0 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -20,7 +20,9 @@ "zone.js": "~0.11.4", "lodash-es": "^4.17.21", "d3-interpolate": "^2.0.1", - "d3-color": "^1.4.0" + "d3-color": "^1.4.0", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0" }, "devDependencies": { "@hypertrace/test-utils": "^0.0.0" diff --git a/projects/common/src/telemetry/telemetry.ts b/projects/common/src/telemetry/telemetry.ts new file mode 100644 index 000000000..1897be775 --- /dev/null +++ b/projects/common/src/telemetry/telemetry.ts @@ -0,0 +1,33 @@ +import { InjectionToken, ProviderToken } from '@angular/core'; +import { Dictionary } from './../utilities/types/types'; +export interface UserTraits extends Dictionary { + email?: string; + companyName?: string; + name?: string; + displayName?: string; +} + +export interface UserTelemetryRegistrationConfig { + telemetryProvider: ProviderToken>; + initConfig: InitConfig; + enablePageTracking: boolean; + enableEventTracking: boolean; + enableErrorTracking: boolean; +} + +export interface UserTelemetryProvider { + initialize(config: InitConfig): void; + identify(userTraits: UserTraits): void; + trackEvent?(name: string, eventData: Dictionary): void; + trackPage?(url: string, eventData: Dictionary): void; + trackError?(error: string, eventData: Dictionary): void; + shutdown?(): void; +} + +export interface TelemetryProviderConfig { + orgId: string; +} + +export const USER_TELEMETRY_PROVIDER_TOKENS = new InjectionToken[][]>( + 'USER_TELEMETRY_PROVIDER_TOKENS' +); diff --git a/projects/common/src/telemetry/user-telemetry-internal.service.ts b/projects/common/src/telemetry/user-telemetry-internal.service.ts new file mode 100644 index 000000000..75ef9b11e --- /dev/null +++ b/projects/common/src/telemetry/user-telemetry-internal.service.ts @@ -0,0 +1,144 @@ +import { Injectable, Injector } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { Observable, ReplaySubject, Subject } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { Dictionary } from '../utilities/types/types'; +import { UserTelemetryProvider, UserTelemetryRegistrationConfig, UserTraits } from './telemetry'; + +@Injectable({ providedIn: 'root' }) +export class UserTelemetryInternalService { + private telemetryProviders: UserTelemetryInternalConfig[] = []; + private readonly telemetryActionSubject: Subject = new ReplaySubject(); + private readonly telemetryAction$: Observable; + private readonly identifyAction$: Observable; + private readonly trackEventAction$: Observable; + private readonly trackPageAction$: Observable; + private readonly trackErrorAction$: Observable; + + public constructor(private readonly injector: Injector, private readonly router: Router) { + this.telemetryAction$ = this.telemetryActionSubject.asObservable(); + + this.identifyAction$ = this.telemetryAction$.pipe(filter(action => action.type === TelemetryActionType.Identify)); + + this.trackEventAction$ = this.telemetryAction$.pipe( + filter(action => action.type === TelemetryActionType.TrackEvent) + ); + + this.trackPageAction$ = this.telemetryAction$.pipe( + filter(action => action.type === TelemetryActionType.TrackPageView) + ); + + this.trackErrorAction$ = this.telemetryAction$.pipe( + filter(action => action.type === TelemetryActionType.TrackError) + ); + + this.setupAutomaticPageTracking(); + this.setupTelemetryActionHandlers(); + } + + public register(...configs: UserTelemetryRegistrationConfig[]): void { + try { + const providers = configs.map(config => this.buildTelemetryProvider(config)); + this.telemetryProviders = [...this.telemetryProviders, ...providers]; + } catch (error) { + /** + * NoOp + */ + } + } + + public identify(userTraits: UserTraits): void { + this.telemetryActionSubject.next({ type: TelemetryActionType.Identify, data: userTraits }); + } + + public shutdown(): void { + this.telemetryActionSubject.next({ type: TelemetryActionType.Shutdown }); + } + + public trackEvent(name: string, data: Dictionary): void { + this.telemetryActionSubject.next({ type: TelemetryActionType.TrackEvent, name: name, data: data }); + } + + public trackPageEvent(url: string, data: Dictionary): void { + this.telemetryActionSubject.next({ type: TelemetryActionType.TrackPageView, name: url, data: data }); + } + + public trackErrorEvent(error: string, data: Dictionary): void { + this.telemetryActionSubject.next({ type: TelemetryActionType.TrackError, name: error, data: data }); + } + + private buildTelemetryProvider(config: UserTelemetryRegistrationConfig): UserTelemetryInternalConfig { + const providerInstance = this.injector.get(config.telemetryProvider); + providerInstance.initialize(config.initConfig); + + return { + ...config, + telemetryProvider: providerInstance + }; + } + + private setupTelemetryActionHandlers(): void { + this.setupIdentifyHandler(); + this.setupTrackEventHandler(); + this.setupTrackPageHandler(); + this.setupTrackErrorHandler(); + } + + private setupIdentifyHandler(): void { + this.identifyAction$.subscribe(action => + this.telemetryProviders.forEach(provider => provider.telemetryProvider.identify(action.data as UserTraits)) + ); + } + + private setupTrackEventHandler(): void { + this.trackEventAction$.subscribe(action => + this.telemetryProviders + .filter(provider => provider.enableEventTracking) + .forEach(provider => provider.telemetryProvider.trackEvent?.(action.name!, action.data!)) + ); + } + + private setupTrackPageHandler(): void { + this.trackPageAction$.subscribe(action => + this.telemetryProviders + .filter(provider => provider.enablePageTracking) + .forEach(provider => provider.telemetryProvider.trackPage?.(action.name!, action.data!)) + ); + } + + private setupTrackErrorHandler(): void { + this.trackErrorAction$.subscribe(action => + this.telemetryProviders + .filter(provider => provider.enableErrorTracking) + .forEach(provider => provider.telemetryProvider.trackError?.(`Error: ${action.name!}`, action.data!)) + ); + } + + private setupAutomaticPageTracking(): void { + this.router.events + .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) + .subscribe(route => this.trackPageEvent(`Visited: ${route.url}`, { url: route.url })); + } +} + +interface UserTelemetryInternalConfig { + telemetryProvider: UserTelemetryProvider; + initConfig: InitConfig; + enablePageTracking: boolean; + enableEventTracking: boolean; + enableErrorTracking: boolean; +} + +interface TelemetryAction { + type: TelemetryActionType; + name?: string; + data?: Dictionary; +} + +const enum TelemetryActionType { + Identify, + TrackPageView, + TrackEvent, + TrackError, + Shutdown +} diff --git a/projects/common/src/telemetry/user-telemetry.module.ts b/projects/common/src/telemetry/user-telemetry.module.ts new file mode 100644 index 000000000..21d416219 --- /dev/null +++ b/projects/common/src/telemetry/user-telemetry.module.ts @@ -0,0 +1,45 @@ +import { ErrorHandler, Inject, ModuleWithProviders, NgModule } from '@angular/core'; +import { TelemetryGlobalErrorHandler } from './error-handler/telemetry-global-error-handler'; +import { UserTelemetryRegistrationConfig, USER_TELEMETRY_PROVIDER_TOKENS } from './telemetry'; +import { TrackDirective } from './track/track.directive'; +import { UserTelemetryInternalService } from './user-telemetry-internal.service'; + +@NgModule({ + declarations: [TrackDirective], + exports: [TrackDirective], + providers: [ + { + provide: USER_TELEMETRY_PROVIDER_TOKENS, + useValue: [], + multi: true + }, + { + provide: ErrorHandler, + useClass: TelemetryGlobalErrorHandler + } + ] +}) +// tslint:disable-next-line: no-unnecessary-class +export class UserTelemetryModule { + public constructor( + userTelemetryInternalService: UserTelemetryInternalService, + @Inject(USER_TELEMETRY_PROVIDER_TOKENS) providerConfigs: UserTelemetryRegistrationConfig[][] + ) { + userTelemetryInternalService.register(...providerConfigs.flat()); + } + + public static withProviders( + providerConfigs: UserTelemetryRegistrationConfig[] + ): ModuleWithProviders { + return { + ngModule: UserTelemetryModule, + providers: [ + { + provide: USER_TELEMETRY_PROVIDER_TOKENS, + useValue: providerConfigs, + multi: true + } + ] + }; + } +} diff --git a/projects/common/src/telemetry/user-telemetry.service.ts b/projects/common/src/telemetry/user-telemetry.service.ts new file mode 100644 index 000000000..d305785b3 --- /dev/null +++ b/projects/common/src/telemetry/user-telemetry.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { UserTraits } from './telemetry'; +import { UserTelemetryInternalService } from './user-telemetry-internal.service'; + +@Injectable({ providedIn: 'root' }) +export class UserTelemetryService { + public constructor(private readonly userTelemetryInternalService: UserTelemetryInternalService) {} + + public identify(userTraits: UserTraits): void { + this.userTelemetryInternalService.identify(userTraits); + } + + public shutdown(): void { + this.userTelemetryInternalService.shutdown(); + } +} From 76ef6f3486039b6c5bffe8bc1ed54bb555bb474e Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:09:18 -0700 Subject: [PATCH 02/11] feat: adding error handler and a track directive --- .../telemetry-global-error-handler.ts | 26 +++++++++++++++++++ .../src/telemetry/track/track.directive.ts | 17 ++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts create mode 100644 projects/common/src/telemetry/track/track.directive.ts diff --git a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts new file mode 100644 index 000000000..dadbe832e --- /dev/null +++ b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts @@ -0,0 +1,26 @@ +import { LocationStrategy, PathLocationStrategy } from '@angular/common'; +import { ErrorHandler, Injectable, Injector } from '@angular/core'; +import { UserTelemetryInternalService } from '../user-telemetry-internal.service'; + +@Injectable() +export class TelemetryGlobalErrorHandler implements ErrorHandler { + public constructor(private readonly injector: Injector) {} + + public handleError(error: Error): Error { + const telemetryService = this.injector.get(UserTelemetryInternalService); + + const location = this.injector.get(LocationStrategy); + const message = error.message ?? error.toString(); + const url = location instanceof PathLocationStrategy ? location.path() : ''; + + telemetryService.trackErrorEvent(message, { + message: message, + url: url, + stack: error.stack, + name: error.name, + isError: true + }); + + throw error; + } +} diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts new file mode 100644 index 000000000..aef264d40 --- /dev/null +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -0,0 +1,17 @@ +import { Directive, HostListener, Input } from '@angular/core'; +import { UserTelemetryInternalService } from '../user-telemetry-internal.service'; + +@Directive({ + selector: '[htTrack]' +}) +export class TrackDirective { + @Input('htTrack') + public name!: string; + + public constructor(private readonly userTelemetryInternalService: UserTelemetryInternalService) {} + + @HostListener('click', ['$event']) + public trackClick(event: MouseEvent): void { + this.userTelemetryInternalService.trackEvent(`Click: ${this.name}`, { target: event.target, type: event.type }); + } +} From a93db20933958aed99ca1c8ffb8bc18d745aa708 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:11:10 -0700 Subject: [PATCH 03/11] feat: adding ootb providers --- .../freshpaint/freshpaint-provider.ts | 32 ++++++++ .../freshpaint/load-snippet/index.d.ts | 16 ++++ .../freshpaint/load-snippet/index.js | 81 +++++++++++++++++++ .../fullstory/full-story-provider.ts | 43 ++++++++++ .../google-analytics-provider.ts | 13 +++ .../google-analytics/load-snippet/index.d.ts | 3 + .../google-analytics/load-snippet/index.js | 51 ++++++++++++ .../providers/mixpanel/mixpanel-provider.ts | 41 ++++++++++ 8 files changed, 280 insertions(+) create mode 100644 projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts create mode 100644 projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts create mode 100644 projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js create mode 100644 projects/common/src/telemetry/providers/fullstory/full-story-provider.ts create mode 100644 projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts create mode 100644 projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts create mode 100644 projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js create mode 100644 projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts diff --git a/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts new file mode 100644 index 000000000..15b0811ea --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts @@ -0,0 +1,32 @@ +import { Injectable } from '@angular/core'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; +import { Dictionary } from './../../../utilities/types/types'; +import { FreshPaint, loadFreshPaint } from './load-snippet'; + +@Injectable({ providedIn: 'root' }) +export class FreshPaintTelemetry + implements UserTelemetryProvider { + private freshPaint?: FreshPaint; + + public initialize(config: InitConfig): void { + this.freshPaint = loadFreshPaint(); + this.freshPaint.init(config.orgId); + } + + public identify(userTraits: UserTraits): void { + this.freshPaint?.identify(userTraits.email, userTraits); + this.freshPaint?.addEventProperties(userTraits); + } + + public trackEvent(name: string, properties: Dictionary): void { + this.freshPaint?.track(name, properties); + } + + public trackPage(name: string, eventData: Dictionary): void { + this.freshPaint?.page('', name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + this.freshPaint?.track(name, eventData); + } +} diff --git a/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts new file mode 100644 index 000000000..d44b545ec --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.d.ts @@ -0,0 +1,16 @@ +export function loadFreshPaint(): FreshPaint; + +export interface FreshPaint { + init(orgId: string): void; + identify(uid?: string, userVars?: UserVars): void; + identify(userVars?: UserVars): void; + track(eventName: string, properties?: {}): void; + addEventProperties(userVars?: UserVars): void; + page(category?: string, name?: string, userVars?: UserVars): void; +} + +interface UserVars { + displayName?: string; + email?: string; + [key: string]: any; +} diff --git a/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js new file mode 100644 index 000000000..59f89bedb --- /dev/null +++ b/projects/common/src/telemetry/providers/freshpaint/load-snippet/index.js @@ -0,0 +1,81 @@ +// tslint:disable + +export const loadFreshPaint = () => { + if (window.freshpaint) { + return window.freshpaint; + } + (function (c, a) { + if (!a.__SV) { + var b = window; + try { + var d, + m, + j, + k = b.location, + f = k.hash; + d = function (a, b) { + return (m = a.match(RegExp(b + '=([^&]*)'))) ? m[1] : null; + }; + f && + d(f, 'fpState') && + ((j = JSON.parse(decodeURIComponent(d(f, 'fpState')))), + 'fpeditor' === j.action && + (b.sessionStorage.setItem('_fpcehash', f), + history.replaceState(j.desiredHash || '', c.title, k.pathname + k.search))); + } catch (n) {} + var l, h; + window.freshpaint = a; + a._i = []; + a.init = function (b, d, g) { + function c(b, i) { + var a = i.split('.'); + 2 == a.length && ((b = b[a[0]]), (i = a[1])); + b[i] = function () { + b.push([i].concat(Array.prototype.slice.call(arguments, 0))); + }; + } + var e = a; + 'undefined' !== typeof g ? (e = a[g] = []) : (g = 'freshpaint'); + e.people = e.people || []; + e.toString = function (b) { + var a = 'freshpaint'; + 'freshpaint' !== g && (a += '.' + g); + b || (a += ' (stub)'); + return a; + }; + e.people.toString = function () { + return e.toString(1) + '.people (stub)'; + }; + l = 'disable time_event track track_pageview track_links track_forms track_with_groups add_group set_group remove_group register register_once alias unregister identify name_tag set_config reset opt_in_tracking opt_out_tracking has_opted_in_tracking has_opted_out_tracking clear_opt_in_out_tracking people.set people.set_once people.unset people.increment people.append people.union people.track_charge people.clear_charges people.delete_user people.remove people group page alias ready addEventProperties addInitialEventProperties removeEventProperty addPageviewProperties'.split( + ' ' + ); + for (h = 0; h < l.length; h++) c(e, l[h]); + var f = 'set set_once union unset remove delete'.split(' '); + e.get_group = function () { + function a(c) { + b[c] = function () { + call2_args = arguments; + call2 = [c].concat(Array.prototype.slice.call(call2_args, 0)); + e.push([d, call2]); + }; + } + for (var b = {}, d = ['get_group'].concat(Array.prototype.slice.call(arguments, 0)), c = 0; c < f.length; c++) + a(f[c]); + return b; + }; + a._i.push([b, d, g]); + }; + a.__SV = 1.4; + b = c.createElement('script'); + b.type = 'text/javascript'; + b.async = !0; + b.src = + 'undefined' !== typeof FRESHPAINT_CUSTOM_LIB_URL + ? FRESHPAINT_CUSTOM_LIB_URL + : '//perfalytics.com/static/js/freshpaint.js'; + (d = c.getElementsByTagName('script')[0]) ? d.parentNode.insertBefore(b, d) : c.head.appendChild(b); + } + })(document, window.freshpaint || []); + + return freshpaint; +}; diff --git a/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts b/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts new file mode 100644 index 000000000..08d16f2de --- /dev/null +++ b/projects/common/src/telemetry/providers/fullstory/full-story-provider.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import * as FullStory from '@fullstory/browser'; +import { Dictionary } from '../../../utilities/types/types'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from './../../telemetry'; + +@Injectable({ providedIn: 'root' }) +export class FullStoryTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + FullStory.init({ + orgId: config.orgId, + devMode: false + }); + } + + public identify(userTraits: UserTraits): void { + FullStory.setUserVars({ + displayName: userTraits.name ?? `${userTraits.givenName as string} ${userTraits.familyName as string}`, + email: userTraits.email, + companyName: userTraits.companyName, + licenseTier: userTraits.licenseTier, + licenseExpiration: userTraits.licenseExpiration, + isPlayground: userTraits.isPlayground, + accountEmail: userTraits.accountEmail + }); + } + + public trackEvent(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public trackPage(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + FullStory.event(name, eventData); + } + + public shutdown(): void { + FullStory.shutdown(); + } +} diff --git a/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts new file mode 100644 index 000000000..941797b97 --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts @@ -0,0 +1,13 @@ +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; +import { loadGA } from './load-snippet'; + +export class GoogleAnalyticsTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + const ga = loadGA(); + ga('create', config.orgId, 'auto'); + ga('send', 'pageview'); + } + + public identify(_userTraits: UserTraits): void {} +} diff --git a/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts new file mode 100644 index 000000000..3134f0de3 --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.d.ts @@ -0,0 +1,3 @@ +import 'google.analytics'; + +export function loadGA(): UniversalAnalytics.ga; diff --git a/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js new file mode 100644 index 000000000..69a8b6f0f --- /dev/null +++ b/projects/common/src/telemetry/providers/google-analytics/load-snippet/index.js @@ -0,0 +1,51 @@ +// tslint:disable + +export const loadGA = () => { + /** + * Creates a temporary global ga object and loads analytics.js. + * Parameters o, a, and m are all used internally. They could have been + * declared using 'var', instead they are declared as parameters to save + * 4 bytes ('var '). + * + * @param {Window} i The global context object. + * @param {HTMLDocument} s The DOM document object. + * @param {string} o Must be 'script'. + * @param {string} g Protocol relative URL of the analytics.js script. + * @param {string} r Global name of analytics object. Defaults to 'ga'. + * @param {HTMLElement} a Async script tag. + * @param {HTMLElement} m First script tag in document. + */ + + if (window.ga) { + return window.ga; + } + + /** + * Do not modify. Below snippet is from Google + */ + + (function (i, s, o, g, r, a, m) { + i['GoogleAnalyticsObject'] = r; // Acts as a pointer to support renaming. + + // Creates an initial ga() function. + // The queued commands will be executed once analytics.js loads. + (i[r] = + i[r] || + function () { + (i[r].q = i[r].q || []).push(arguments); + }), + // Sets the time (as an integer) this tag was executed. + // Used for timing hits. + (i[r].l = 1 * new Date()); + + // Insert the script tag asynchronously. + // Inserts above current tag to prevent blocking in addition to using the + // async attribute. + (a = s.createElement(o)), (m = s.getElementsByTagName(o)[0]); + a.async = 1; + a.src = g; + m.parentNode.insertBefore(a, m); + })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga'); + + return ga; +}; diff --git a/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts b/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts new file mode 100644 index 000000000..2fac7d2e4 --- /dev/null +++ b/projects/common/src/telemetry/providers/mixpanel/mixpanel-provider.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import mixpanel from 'mixpanel-browser'; +import { Dictionary } from '../../../utilities/types/types'; +import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; + +@Injectable({ providedIn: 'root' }) +export class MixPanelTelemetry + implements UserTelemetryProvider { + public initialize(config: InitConfig): void { + mixpanel.init(config.orgId); + } + + public identify(userTraits: UserTraits): void { + mixpanel.identify(userTraits.email); + mixpanel.people.set({ + displayName: userTraits.name ?? `${String(userTraits.givenName)} ${String(userTraits.familyName)}`, + email: userTraits.email, + companyName: userTraits.companyName, + licenseTier: userTraits.licenseTier, + licenseExpiration: userTraits.licenseExpiration, + isPlayground: userTraits.isPlayground, + accountEmail: userTraits.accountEmail + }); + } + + public trackEvent(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public trackPage(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public trackError(name: string, eventData: Dictionary): void { + mixpanel.track(name, eventData); + } + + public shutdown(): void { + mixpanel.disable(); + } +} From 61616559579484245e8e1e46562169a525e97fc6 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Tue, 21 Sep 2021 16:33:14 -0700 Subject: [PATCH 04/11] refactor: using abstract service approach --- .../telemetry-global-error-handler.ts | 4 +- .../src/telemetry/track/track.directive.ts | 6 +- ...ts => user-telemetry-impl.service.test.ts} | 4 +- ...vice.ts => user-telemetry-impl.service.ts} | 4 +- .../user-telemetry-internal.service.ts | 144 ------------------ .../src/telemetry/user-telemetry.module.ts | 11 +- .../telemetry/user-telemetry.service.test.ts | 28 ---- .../src/telemetry/user-telemetry.service.ts | 18 +-- 8 files changed, 22 insertions(+), 197 deletions(-) rename projects/common/src/telemetry/{user-telemetry-helper.service.test.ts => user-telemetry-impl.service.test.ts} (98%) rename projects/common/src/telemetry/{user-telemetry-helper.service.ts => user-telemetry-impl.service.ts} (95%) delete mode 100644 projects/common/src/telemetry/user-telemetry-internal.service.ts delete mode 100644 projects/common/src/telemetry/user-telemetry.service.test.ts diff --git a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts index dadbe832e..b618beca2 100644 --- a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts +++ b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.ts @@ -1,13 +1,13 @@ import { LocationStrategy, PathLocationStrategy } from '@angular/common'; import { ErrorHandler, Injectable, Injector } from '@angular/core'; -import { UserTelemetryInternalService } from '../user-telemetry-internal.service'; +import { UserTelemetryImplService } from '../user-telemetry-impl.service'; @Injectable() export class TelemetryGlobalErrorHandler implements ErrorHandler { public constructor(private readonly injector: Injector) {} public handleError(error: Error): Error { - const telemetryService = this.injector.get(UserTelemetryInternalService); + const telemetryService = this.injector.get(UserTelemetryImplService); const location = this.injector.get(LocationStrategy); const message = error.message ?? error.toString(); diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index aef264d40..d98d49cb9 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -1,5 +1,5 @@ import { Directive, HostListener, Input } from '@angular/core'; -import { UserTelemetryInternalService } from '../user-telemetry-internal.service'; +import { UserTelemetryImplService } from '../user-telemetry-impl.service'; @Directive({ selector: '[htTrack]' @@ -8,10 +8,10 @@ export class TrackDirective { @Input('htTrack') public name!: string; - public constructor(private readonly userTelemetryInternalService: UserTelemetryInternalService) {} + public constructor(private readonly userTelemetryImplService: UserTelemetryImplService) {} @HostListener('click', ['$event']) public trackClick(event: MouseEvent): void { - this.userTelemetryInternalService.trackEvent(`Click: ${this.name}`, { target: event.target, type: event.type }); + this.userTelemetryImplService.trackEvent(`Click: ${this.name}`, { target: event.target, type: event.type }); } } diff --git a/projects/common/src/telemetry/user-telemetry-helper.service.test.ts b/projects/common/src/telemetry/user-telemetry-impl.service.test.ts similarity index 98% rename from projects/common/src/telemetry/user-telemetry-helper.service.test.ts rename to projects/common/src/telemetry/user-telemetry-impl.service.test.ts index 6530430df..9f1583ef5 100644 --- a/projects/common/src/telemetry/user-telemetry-helper.service.test.ts +++ b/projects/common/src/telemetry/user-telemetry-impl.service.test.ts @@ -3,7 +3,7 @@ import { Router } from '@angular/router'; import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { TelemetryProviderConfig, UserTelemetryProvider, UserTelemetryRegistrationConfig } from './telemetry'; -import { UserTelemetryHelperService } from './user-telemetry-helper.service'; +import { UserTelemetryImplService } from './user-telemetry-impl.service'; describe('User Telemetry helper service', () => { const injectionToken = new InjectionToken('test-token'); @@ -11,7 +11,7 @@ describe('User Telemetry helper service', () => { let registrationConfig: UserTelemetryRegistrationConfig; const createService = createServiceFactory({ - service: UserTelemetryHelperService, + service: UserTelemetryImplService, providers: [ mockProvider(Router, { events: of({}) diff --git a/projects/common/src/telemetry/user-telemetry-helper.service.ts b/projects/common/src/telemetry/user-telemetry-impl.service.ts similarity index 95% rename from projects/common/src/telemetry/user-telemetry-helper.service.ts rename to projects/common/src/telemetry/user-telemetry-impl.service.ts index 6c0d6fd12..83eb269e4 100644 --- a/projects/common/src/telemetry/user-telemetry-helper.service.ts +++ b/projects/common/src/telemetry/user-telemetry-impl.service.ts @@ -3,12 +3,14 @@ import { NavigationEnd, Router } from '@angular/router'; import { filter } from 'rxjs/operators'; import { Dictionary } from '../utilities/types/types'; import { UserTelemetryProvider, UserTelemetryRegistrationConfig, UserTraits } from './telemetry'; +import { UserTelemetryService } from './user-telemetry.service'; @Injectable({ providedIn: 'root' }) -export class UserTelemetryHelperService { +export class UserTelemetryImplService extends UserTelemetryService { private telemetryProviders: UserTelemetryInternalConfig[] = []; public constructor(private readonly injector: Injector, private readonly router: Router) { + super(); this.setupAutomaticPageTracking(); } diff --git a/projects/common/src/telemetry/user-telemetry-internal.service.ts b/projects/common/src/telemetry/user-telemetry-internal.service.ts deleted file mode 100644 index 75ef9b11e..000000000 --- a/projects/common/src/telemetry/user-telemetry-internal.service.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { Injectable, Injector } from '@angular/core'; -import { NavigationEnd, Router } from '@angular/router'; -import { Observable, ReplaySubject, Subject } from 'rxjs'; -import { filter } from 'rxjs/operators'; -import { Dictionary } from '../utilities/types/types'; -import { UserTelemetryProvider, UserTelemetryRegistrationConfig, UserTraits } from './telemetry'; - -@Injectable({ providedIn: 'root' }) -export class UserTelemetryInternalService { - private telemetryProviders: UserTelemetryInternalConfig[] = []; - private readonly telemetryActionSubject: Subject = new ReplaySubject(); - private readonly telemetryAction$: Observable; - private readonly identifyAction$: Observable; - private readonly trackEventAction$: Observable; - private readonly trackPageAction$: Observable; - private readonly trackErrorAction$: Observable; - - public constructor(private readonly injector: Injector, private readonly router: Router) { - this.telemetryAction$ = this.telemetryActionSubject.asObservable(); - - this.identifyAction$ = this.telemetryAction$.pipe(filter(action => action.type === TelemetryActionType.Identify)); - - this.trackEventAction$ = this.telemetryAction$.pipe( - filter(action => action.type === TelemetryActionType.TrackEvent) - ); - - this.trackPageAction$ = this.telemetryAction$.pipe( - filter(action => action.type === TelemetryActionType.TrackPageView) - ); - - this.trackErrorAction$ = this.telemetryAction$.pipe( - filter(action => action.type === TelemetryActionType.TrackError) - ); - - this.setupAutomaticPageTracking(); - this.setupTelemetryActionHandlers(); - } - - public register(...configs: UserTelemetryRegistrationConfig[]): void { - try { - const providers = configs.map(config => this.buildTelemetryProvider(config)); - this.telemetryProviders = [...this.telemetryProviders, ...providers]; - } catch (error) { - /** - * NoOp - */ - } - } - - public identify(userTraits: UserTraits): void { - this.telemetryActionSubject.next({ type: TelemetryActionType.Identify, data: userTraits }); - } - - public shutdown(): void { - this.telemetryActionSubject.next({ type: TelemetryActionType.Shutdown }); - } - - public trackEvent(name: string, data: Dictionary): void { - this.telemetryActionSubject.next({ type: TelemetryActionType.TrackEvent, name: name, data: data }); - } - - public trackPageEvent(url: string, data: Dictionary): void { - this.telemetryActionSubject.next({ type: TelemetryActionType.TrackPageView, name: url, data: data }); - } - - public trackErrorEvent(error: string, data: Dictionary): void { - this.telemetryActionSubject.next({ type: TelemetryActionType.TrackError, name: error, data: data }); - } - - private buildTelemetryProvider(config: UserTelemetryRegistrationConfig): UserTelemetryInternalConfig { - const providerInstance = this.injector.get(config.telemetryProvider); - providerInstance.initialize(config.initConfig); - - return { - ...config, - telemetryProvider: providerInstance - }; - } - - private setupTelemetryActionHandlers(): void { - this.setupIdentifyHandler(); - this.setupTrackEventHandler(); - this.setupTrackPageHandler(); - this.setupTrackErrorHandler(); - } - - private setupIdentifyHandler(): void { - this.identifyAction$.subscribe(action => - this.telemetryProviders.forEach(provider => provider.telemetryProvider.identify(action.data as UserTraits)) - ); - } - - private setupTrackEventHandler(): void { - this.trackEventAction$.subscribe(action => - this.telemetryProviders - .filter(provider => provider.enableEventTracking) - .forEach(provider => provider.telemetryProvider.trackEvent?.(action.name!, action.data!)) - ); - } - - private setupTrackPageHandler(): void { - this.trackPageAction$.subscribe(action => - this.telemetryProviders - .filter(provider => provider.enablePageTracking) - .forEach(provider => provider.telemetryProvider.trackPage?.(action.name!, action.data!)) - ); - } - - private setupTrackErrorHandler(): void { - this.trackErrorAction$.subscribe(action => - this.telemetryProviders - .filter(provider => provider.enableErrorTracking) - .forEach(provider => provider.telemetryProvider.trackError?.(`Error: ${action.name!}`, action.data!)) - ); - } - - private setupAutomaticPageTracking(): void { - this.router.events - .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) - .subscribe(route => this.trackPageEvent(`Visited: ${route.url}`, { url: route.url })); - } -} - -interface UserTelemetryInternalConfig { - telemetryProvider: UserTelemetryProvider; - initConfig: InitConfig; - enablePageTracking: boolean; - enableEventTracking: boolean; - enableErrorTracking: boolean; -} - -interface TelemetryAction { - type: TelemetryActionType; - name?: string; - data?: Dictionary; -} - -const enum TelemetryActionType { - Identify, - TrackPageView, - TrackEvent, - TrackError, - Shutdown -} diff --git a/projects/common/src/telemetry/user-telemetry.module.ts b/projects/common/src/telemetry/user-telemetry.module.ts index f35d2094a..28038eda7 100644 --- a/projects/common/src/telemetry/user-telemetry.module.ts +++ b/projects/common/src/telemetry/user-telemetry.module.ts @@ -1,14 +1,15 @@ import { Inject, ModuleWithProviders, NgModule, InjectionToken } from '@angular/core'; import { UserTelemetryRegistrationConfig } from './telemetry'; -import { UserTelemetryHelperService } from './user-telemetry-helper.service'; +import { UserTelemetryImplService } from './user-telemetry-impl.service'; +import { UserTelemetryService } from './user-telemetry.service'; @NgModule() export class UserTelemetryModule { public constructor( @Inject(USER_TELEMETRY_PROVIDER_TOKENS) providerConfigs: UserTelemetryRegistrationConfig[][], - userTelemetryInternalService: UserTelemetryHelperService + userTelemetryImplService: UserTelemetryImplService ) { - userTelemetryInternalService.register(...providerConfigs.flat()); + userTelemetryImplService.register(...providerConfigs.flat()); } public static forRoot( @@ -20,6 +21,10 @@ export class UserTelemetryModule { { provide: USER_TELEMETRY_PROVIDER_TOKENS, useValue: providerConfigs + }, + { + provide: UserTelemetryService, + useExisting: UserTelemetryImplService } ] }; diff --git a/projects/common/src/telemetry/user-telemetry.service.test.ts b/projects/common/src/telemetry/user-telemetry.service.test.ts deleted file mode 100644 index b6d891e2d..000000000 --- a/projects/common/src/telemetry/user-telemetry.service.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; -import { UserTelemetryHelperService } from './user-telemetry-helper.service'; -import { UserTelemetryService } from './user-telemetry.service'; - -describe('User Telemetry service', () => { - const createService = createServiceFactory({ - service: UserTelemetryService, - providers: [ - mockProvider(UserTelemetryHelperService, { - initialize: jest.fn(), - identify: jest.fn(), - shutdown: jest.fn() - }) - ] - }); - - test('should delegate to helper service', () => { - const spectator = createService(); - const helperService = spectator.inject(UserTelemetryHelperService); - - spectator.service.initialize({ email: 'test@email.com' }); - expect(helperService.initialize).toHaveBeenCalledWith(); - expect(helperService.identify).toHaveBeenCalledWith({ email: 'test@email.com' }); - - spectator.service.shutdown(); - expect(helperService.shutdown).toHaveBeenCalledWith(); - }); -}); diff --git a/projects/common/src/telemetry/user-telemetry.service.ts b/projects/common/src/telemetry/user-telemetry.service.ts index d5adf7aac..caec9ed2f 100644 --- a/projects/common/src/telemetry/user-telemetry.service.ts +++ b/projects/common/src/telemetry/user-telemetry.service.ts @@ -1,17 +1,7 @@ -import { Injectable } from '@angular/core'; import { UserTraits } from './telemetry'; -import { UserTelemetryHelperService } from './user-telemetry-helper.service'; -@Injectable({ providedIn: 'root' }) -export class UserTelemetryService { - public constructor(private readonly userTelemetryHelperService: UserTelemetryHelperService) {} - - public initialize(userTraits: UserTraits): void { - this.userTelemetryHelperService.initialize(); - this.userTelemetryHelperService.identify(userTraits); - } - - public shutdown(): void { - this.userTelemetryHelperService.shutdown(); - } +export abstract class UserTelemetryService { + public abstract initialize(): void; + public abstract identify(userTraits: UserTraits): void; + public abstract shutdown(): void; } From a8b308df738729215e4fc8602fd3efae68b128d0 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:34:11 -0700 Subject: [PATCH 05/11] refactor: updating api for htTrack directive --- projects/common/src/telemetry/telemetry.ts | 5 ++ .../src/telemetry/track/track.directive.ts | 57 ++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/projects/common/src/telemetry/telemetry.ts b/projects/common/src/telemetry/telemetry.ts index 536055c85..f355bdc6c 100644 --- a/projects/common/src/telemetry/telemetry.ts +++ b/projects/common/src/telemetry/telemetry.ts @@ -28,3 +28,8 @@ export interface UserTraits extends Dictionary { name?: string; displayName?: string; } + +export const enum TrackUserEventsType { + Click = 'click', + ContextMenu = 'context-menu' +} diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index d98d49cb9..5e3d84fd6 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -1,17 +1,60 @@ -import { Directive, HostListener, Input } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; +import { Directive, ElementRef, Input, OnChanges, OnDestroy } from '@angular/core'; +import { TypedSimpleChanges } from '../../utilities/types/angular-change-object'; +import { TrackUserEventsType } from '../telemetry'; import { UserTelemetryImplService } from '../user-telemetry-impl.service'; @Directive({ selector: '[htTrack]' }) -export class TrackDirective { +export class TrackDirective implements OnChanges, OnDestroy { @Input('htTrack') - public name!: string; + public userEvents: TrackUserEventsType[] = [TrackUserEventsType.Click]; - public constructor(private readonly userTelemetryImplService: UserTelemetryImplService) {} + @Input('htTrackLabel') + public label?: string; - @HostListener('click', ['$event']) - public trackClick(event: MouseEvent): void { - this.userTelemetryImplService.trackEvent(`Click: ${this.name}`, { target: event.target, type: event.type }); + private readonly subscription: Subscription = new Subscription(); + private trackedEventLabel: string = ''; + + public constructor( + private readonly host: ElementRef, + private readonly userTelemetryImplService: UserTelemetryImplService + ) {} + + public ngOnChanges(changes: TypedSimpleChanges): void { + if (changes.userEvents) { + this.setupListeners(); + } + + if (changes.label) { + this.trackedEventLabel = this.label ?? (this.host.nativeElement as HTMLElement)?.tagName; + } + } + + public ngOnDestroy(): void { + this.clearListeners(); + } + + private setupListeners(): void { + this.clearListeners(); + this.subscription.add( + ...this.userEvents.map(userEvent => + fromEvent(this.host.nativeElement, userEvent).subscribe(eventObj => + this.trackUserEvent(userEvent, eventObj) + ) + ) + ); + } + + private clearListeners(): void { + this.subscription.unsubscribe(); + } + + private trackUserEvent(userEvent: TrackUserEventsType, eventObj: MouseEvent): void { + this.userTelemetryImplService.trackEvent(`${userEvent}: ${this.trackedEventLabel}`, { + target: eventObj.target, + type: userEvent + }); } } From 9ce27b3ad91be20ea08282c241c3a8e321cd4a65 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:41:00 -0700 Subject: [PATCH 06/11] revert: package-lock.json --- package-lock.json | 57 +++++++++++++++-------------------------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 29520121c..711ee037c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -47,7 +47,6 @@ "graphql-tag": "^2.12.5", "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", - "mixpanel-browser": "^2.41.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", @@ -3541,11 +3540,6 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, - "node_modules/@fullstory/browser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", - "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" - }, "node_modules/@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -5884,9 +5878,9 @@ } }, "node_modules/@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "version": "16.7.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", + "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -8587,9 +8581,9 @@ } }, "node_modules/commander": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz", - "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", + "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", "dev": true, "engines": { "node": ">= 12" @@ -13156,9 +13150,9 @@ } }, "node_modules/i18next": { - "version": "20.6.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", - "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.4.0.tgz", + "integrity": "sha512-89iWWJudmaHJwzIdJ/1eu98GtsJnwBhOUWwlAre70itPMuTE/NTPtgVeaS1CGaB8Q3XrYBGpEqlq4jsScDx9kg==", "dev": true, "dependencies": { "@babel/runtime": "^7.12.0" @@ -20481,11 +20475,6 @@ "node": ">=0.10.0" } }, - "node_modules/mixpanel-browser": { - "version": "2.41.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", - "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -32537,11 +32526,6 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, - "@fullstory/browser": { - "version": "1.4.9", - "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", - "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" - }, "@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -34437,9 +34421,9 @@ } }, "@types/node": { - "version": "16.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.1.tgz", - "integrity": "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==", + "version": "16.7.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", + "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==", "dev": true }, "@types/normalize-package-data": { @@ -36631,9 +36615,9 @@ } }, "commander": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.2.0.tgz", - "integrity": "sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", + "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", "dev": true }, "commitizen": { @@ -40324,9 +40308,9 @@ "dev": true }, "i18next": { - "version": "20.6.1", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.6.1.tgz", - "integrity": "sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==", + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-20.4.0.tgz", + "integrity": "sha512-89iWWJudmaHJwzIdJ/1eu98GtsJnwBhOUWwlAre70itPMuTE/NTPtgVeaS1CGaB8Q3XrYBGpEqlq4jsScDx9kg==", "dev": true, "requires": { "@babel/runtime": "^7.12.0" @@ -46042,11 +46026,6 @@ "is-extendable": "^1.0.1" } }, - "mixpanel-browser": { - "version": "2.41.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", - "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" - }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", From 1358760f3075e29a04513e7bc8402bb9a7dc2a02 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:41:52 -0700 Subject: [PATCH 07/11] revert: revert package.json --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index 6b7474bb1..6ed1685f1 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,6 @@ "@angular/platform-browser": "^12.2.1", "@angular/platform-browser-dynamic": "^12.2.1", "@angular/router": "^12.2.1", - "@fullstory/browser": "^1.4.9", - "mixpanel-browser": "^2.41.0", "@apollo/client": "^3.4.13", "@hypertrace/hyperdash": "^1.2.1", "@hypertrace/hyperdash-angular": "^2.6.0", From 83955558a8d95793762b026b616d6defc4120543 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Tue, 21 Sep 2021 17:43:29 -0700 Subject: [PATCH 08/11] revert: revert package.json peer deps --- projects/common/package.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/common/package.json b/projects/common/package.json index a05eaa6a0..e995da500 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -20,9 +20,7 @@ "zone.js": "~0.11.4", "lodash-es": "^4.17.21", "d3-interpolate": "^2.0.1", - "d3-color": "^1.4.0", - "@fullstory/browser": "^1.4.9", - "mixpanel-browser": "^2.41.0" + "d3-color": "^1.4.0" }, "devDependencies": { "@hypertrace/test-utils": "^0.0.0" From 4db25a3578b6160bee397bd597bda8ad1049f1e4 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Fri, 24 Sep 2021 16:55:43 -0700 Subject: [PATCH 09/11] refactor: adding tests and addressing commments --- .../telemetry-global-error-handler.test.ts | 31 +++++++++ .../telemetry/track/track.directive.test.ts | 67 +++++++++++++++++++ .../src/telemetry/track/track.directive.ts | 24 ++++--- .../src/telemetry/user-telemetry.module.ts | 7 +- 4 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts create mode 100644 projects/common/src/telemetry/track/track.directive.test.ts diff --git a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts new file mode 100644 index 000000000..b43289ba5 --- /dev/null +++ b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts @@ -0,0 +1,31 @@ +import { TelemetryGlobalErrorHandler } from './telemetry-global-error-handler'; +import { createServiceFactory } from '@ngneat/spectator/jest'; +import { mockProvider } from '@ngneat/spectator'; +import { UserTelemetryImplService } from '../user-telemetry-impl.service'; + +describe('Telemetry Global Error Handler ', () => { + const createService = createServiceFactory({ + service: TelemetryGlobalErrorHandler, + providers: [ + mockProvider(UserTelemetryImplService, { + trackErrorEvent: jest.fn() + }) + ] + }); + + test('should delegate to telemetry provider after registration', () => { + const spectator = createService(); + try { + spectator.service.handleError(new Error('Test error')); + } catch (_) {} + + expect(spectator.inject(UserTelemetryImplService).trackErrorEvent).toHaveBeenCalledWith( + 'Test error', + expect.objectContaining({ + message: 'Test error', + name: 'Error', + isError: true + }) + ); + }); +}); diff --git a/projects/common/src/telemetry/track/track.directive.test.ts b/projects/common/src/telemetry/track/track.directive.test.ts new file mode 100644 index 000000000..7a999b931 --- /dev/null +++ b/projects/common/src/telemetry/track/track.directive.test.ts @@ -0,0 +1,67 @@ +import { TrackDirective } from './track.directive'; +import { fakeAsync } from '@angular/core/testing'; +import { createDirectiveFactory, SpectatorDirective, mockProvider } from '@ngneat/spectator/jest'; +import { UserTelemetryImplService } from '../user-telemetry-impl.service'; +import { CommonModule } from '@angular/common'; + +describe('Track directive', () => { + let spectator: SpectatorDirective; + + const createDirective = createDirectiveFactory({ + directive: TrackDirective, + imports: [CommonModule], + providers: [ + mockProvider(UserTelemetryImplService, { + trackEvent: jest.fn() + }) + ] + }); + + test('propagates events with default config', fakeAsync(() => { + spectator = createDirective( + ` +
Test Content
+ `, + { + hostProps: { + events: ['click'], + label: 'Content' + } + } + ); + + const telemetryService = spectator.inject(UserTelemetryImplService); + + spectator.click(spectator.element); + spectator.tick(); + + expect(telemetryService.trackEvent).toHaveBeenCalledWith( + 'click: Content', + expect.objectContaining({ type: 'click' }) + ); + })); + + test('propagates events with custom config', fakeAsync(() => { + spectator = createDirective( + ` +
Test Content
+ `, + { + hostProps: { + events: ['mouseover'], + label: 'Content' + } + } + ); + + const telemetryService = spectator.inject(UserTelemetryImplService); + + spectator.dispatchMouseEvent(spectator.element, 'mouseover'); + spectator.tick(); + + expect(telemetryService.trackEvent).toHaveBeenCalledWith( + 'mouseover: Content', + expect.objectContaining({ type: 'mouseover' }) + ); + })); +}); diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index 5e3d84fd6..9de97cf03 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -1,5 +1,5 @@ import { fromEvent, Subscription } from 'rxjs'; -import { Directive, ElementRef, Input, OnChanges, OnDestroy } from '@angular/core'; +import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; import { TypedSimpleChanges } from '../../utilities/types/angular-change-object'; import { TrackUserEventsType } from '../telemetry'; import { UserTelemetryImplService } from '../user-telemetry-impl.service'; @@ -7,14 +7,14 @@ import { UserTelemetryImplService } from '../user-telemetry-impl.service'; @Directive({ selector: '[htTrack]' }) -export class TrackDirective implements OnChanges, OnDestroy { +export class TrackDirective implements OnInit, OnChanges, OnDestroy { @Input('htTrack') - public userEvents: TrackUserEventsType[] = [TrackUserEventsType.Click]; + public userEvents: string[] = [TrackUserEventsType.Click]; @Input('htTrackLabel') public label?: string; - private readonly subscription: Subscription = new Subscription(); + private activeSubscriptions: Subscription = new Subscription(); private trackedEventLabel: string = ''; public constructor( @@ -22,6 +22,10 @@ export class TrackDirective implements OnChanges, OnDestroy { private readonly userTelemetryImplService: UserTelemetryImplService ) {} + public ngOnInit(): void { + this.setupListeners(); + } + public ngOnChanges(changes: TypedSimpleChanges): void { if (changes.userEvents) { this.setupListeners(); @@ -38,8 +42,10 @@ export class TrackDirective implements OnChanges, OnDestroy { private setupListeners(): void { this.clearListeners(); - this.subscription.add( - ...this.userEvents.map(userEvent => + this.activeSubscriptions = new Subscription(); + + this.activeSubscriptions.add( + ...this.userEvents?.map(userEvent => fromEvent(this.host.nativeElement, userEvent).subscribe(eventObj => this.trackUserEvent(userEvent, eventObj) ) @@ -48,12 +54,12 @@ export class TrackDirective implements OnChanges, OnDestroy { } private clearListeners(): void { - this.subscription.unsubscribe(); + this.activeSubscriptions.unsubscribe(); } - private trackUserEvent(userEvent: TrackUserEventsType, eventObj: MouseEvent): void { + private trackUserEvent(userEvent: string, eventObj: MouseEvent): void { this.userTelemetryImplService.trackEvent(`${userEvent}: ${this.trackedEventLabel}`, { - target: eventObj.target, + ...(eventObj.target as HTMLElement), type: userEvent }); } diff --git a/projects/common/src/telemetry/user-telemetry.module.ts b/projects/common/src/telemetry/user-telemetry.module.ts index 28038eda7..5638478d0 100644 --- a/projects/common/src/telemetry/user-telemetry.module.ts +++ b/projects/common/src/telemetry/user-telemetry.module.ts @@ -1,4 +1,5 @@ -import { Inject, ModuleWithProviders, NgModule, InjectionToken } from '@angular/core'; +import { Inject, ModuleWithProviders, NgModule, InjectionToken, ErrorHandler } from '@angular/core'; +import { TelemetryGlobalErrorHandler } from './error-handler/telemetry-global-error-handler'; import { UserTelemetryRegistrationConfig } from './telemetry'; import { UserTelemetryImplService } from './user-telemetry-impl.service'; import { UserTelemetryService } from './user-telemetry.service'; @@ -25,6 +26,10 @@ export class UserTelemetryModule { { provide: UserTelemetryService, useExisting: UserTelemetryImplService + }, + { + provide: ErrorHandler, + useClass: TelemetryGlobalErrorHandler } ] }; From 8abcc0a7d156894f0643102dd481376967a3f539 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Fri, 24 Sep 2021 23:09:59 -0700 Subject: [PATCH 10/11] refactor: fix lint errors --- .../error-handler/telemetry-global-error-handler.test.ts | 9 +++++---- .../common/src/telemetry/track/track.directive.test.ts | 6 +++--- projects/common/src/telemetry/track/track.directive.ts | 2 +- projects/common/src/telemetry/user-telemetry.module.ts | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts index b43289ba5..f0d60790f 100644 --- a/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts +++ b/projects/common/src/telemetry/error-handler/telemetry-global-error-handler.test.ts @@ -1,7 +1,6 @@ -import { TelemetryGlobalErrorHandler } from './telemetry-global-error-handler'; -import { createServiceFactory } from '@ngneat/spectator/jest'; -import { mockProvider } from '@ngneat/spectator'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { UserTelemetryImplService } from '../user-telemetry-impl.service'; +import { TelemetryGlobalErrorHandler } from './telemetry-global-error-handler'; describe('Telemetry Global Error Handler ', () => { const createService = createServiceFactory({ @@ -17,7 +16,9 @@ describe('Telemetry Global Error Handler ', () => { const spectator = createService(); try { spectator.service.handleError(new Error('Test error')); - } catch (_) {} + } catch (_) { + // NoOP + } expect(spectator.inject(UserTelemetryImplService).trackErrorEvent).toHaveBeenCalledWith( 'Test error', diff --git a/projects/common/src/telemetry/track/track.directive.test.ts b/projects/common/src/telemetry/track/track.directive.test.ts index 7a999b931..f3580a0ac 100644 --- a/projects/common/src/telemetry/track/track.directive.test.ts +++ b/projects/common/src/telemetry/track/track.directive.test.ts @@ -1,8 +1,8 @@ -import { TrackDirective } from './track.directive'; +import { CommonModule } from '@angular/common'; import { fakeAsync } from '@angular/core/testing'; -import { createDirectiveFactory, SpectatorDirective, mockProvider } from '@ngneat/spectator/jest'; +import { createDirectiveFactory, mockProvider, SpectatorDirective } from '@ngneat/spectator/jest'; import { UserTelemetryImplService } from '../user-telemetry-impl.service'; -import { CommonModule } from '@angular/common'; +import { TrackDirective } from './track.directive'; describe('Track directive', () => { let spectator: SpectatorDirective; diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index 9de97cf03..b93f0de7f 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -1,5 +1,5 @@ -import { fromEvent, Subscription } from 'rxjs'; import { Directive, ElementRef, Input, OnChanges, OnDestroy, OnInit } from '@angular/core'; +import { fromEvent, Subscription } from 'rxjs'; import { TypedSimpleChanges } from '../../utilities/types/angular-change-object'; import { TrackUserEventsType } from '../telemetry'; import { UserTelemetryImplService } from '../user-telemetry-impl.service'; diff --git a/projects/common/src/telemetry/user-telemetry.module.ts b/projects/common/src/telemetry/user-telemetry.module.ts index 5638478d0..30aa00ab6 100644 --- a/projects/common/src/telemetry/user-telemetry.module.ts +++ b/projects/common/src/telemetry/user-telemetry.module.ts @@ -1,4 +1,4 @@ -import { Inject, ModuleWithProviders, NgModule, InjectionToken, ErrorHandler } from '@angular/core'; +import { ErrorHandler, Inject, InjectionToken, ModuleWithProviders, NgModule } from '@angular/core'; import { TelemetryGlobalErrorHandler } from './error-handler/telemetry-global-error-handler'; import { UserTelemetryRegistrationConfig } from './telemetry'; import { UserTelemetryImplService } from './user-telemetry-impl.service'; From 816baec43f75b78d5d3f1ca6b953a100b0adde8a Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Sun, 26 Sep 2021 12:43:42 -0700 Subject: [PATCH 11/11] refactor: adding packages and modifying code --- package-lock.json | 22 +++++++++++++ package.json | 4 ++- projects/common/package.json | 4 ++- .../freshpaint/freshpaint-provider.ts | 2 +- .../google-analytics-provider.ts | 31 ++++++++++++++++++- 5 files changed, 59 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 711ee037c..8ae51e478 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@angular/platform-browser-dynamic": "^12.2.1", "@angular/router": "^12.2.1", "@apollo/client": "^3.4.13", + "@fullstory/browser": "^1.4.9", "@hypertrace/hyperdash": "^1.2.1", "@hypertrace/hyperdash-angular": "^2.6.0", "@types/d3-hierarchy": "^2.0.0", @@ -47,6 +48,7 @@ "graphql-tag": "^2.12.5", "iso8601-duration": "^1.3.0", "lodash-es": "^4.17.21", + "mixpanel-browser": "^2.41.0", "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", @@ -3540,6 +3542,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "node_modules/@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "node_modules/@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -20475,6 +20482,11 @@ "node": ">=0.10.0" } }, + "node_modules/mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -32526,6 +32538,11 @@ "integrity": "sha512-59SgoZ3EXbkfSX7b63tsou/SDGzwUEK6MuB5sKqgVK1/XE0fxmpsOb9DQI8LXW3KfGnAjImCGhhEb7uPPAUVNA==", "dev": true }, + "@fullstory/browser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@fullstory/browser/-/browser-1.4.9.tgz", + "integrity": "sha512-h8ihrXT8pGemh5n7CKrukkEbbRIuCi0I/GJKI8DJpGyloI4WNTX5SC8Aihec7ScfK6Fi6ZpiLkGP3hogZqoNWw==" + }, "@graphql-typed-document-node/core": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.0.tgz", @@ -46026,6 +46043,11 @@ "is-extendable": "^1.0.1" } }, + "mixpanel-browser": { + "version": "2.41.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.41.0.tgz", + "integrity": "sha512-IEuc9cH44hba9a3KEyulXINLn+gpFqluBDo7xiTk1h3j111dmmsctaE6tUzZYxgGLVqeNhTpsccdliOeX24Wlw==" + }, "mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", diff --git a/package.json b/package.json index 6ed1685f1..cf8af412d 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,9 @@ "rxjs": "~6.6.7", "tslib": "^2.3.1", "uuid": "^8.3.2", - "zone.js": "~0.11.4" + "zone.js": "~0.11.4", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0" }, "devDependencies": { "@angular-builders/jest": "^11.2.0", diff --git a/projects/common/package.json b/projects/common/package.json index e995da500..a05eaa6a0 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -20,7 +20,9 @@ "zone.js": "~0.11.4", "lodash-es": "^4.17.21", "d3-interpolate": "^2.0.1", - "d3-color": "^1.4.0" + "d3-color": "^1.4.0", + "@fullstory/browser": "^1.4.9", + "mixpanel-browser": "^2.41.0" }, "devDependencies": { "@hypertrace/test-utils": "^0.0.0" diff --git a/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts index 15b0811ea..8e212581a 100644 --- a/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts +++ b/projects/common/src/telemetry/providers/freshpaint/freshpaint-provider.ts @@ -23,7 +23,7 @@ export class FreshPaintTelemetry } public trackPage(name: string, eventData: Dictionary): void { - this.freshPaint?.page('', name, eventData); + this.freshPaint?.page(name, name, eventData); } public trackError(name: string, eventData: Dictionary): void { diff --git a/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts index 941797b97..9c005dbb1 100644 --- a/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts +++ b/projects/common/src/telemetry/providers/google-analytics/google-analytics-provider.ts @@ -1,3 +1,4 @@ +import { Dictionary } from './../../../utilities/types/types'; import { TelemetryProviderConfig, UserTelemetryProvider, UserTraits } from '../../telemetry'; import { loadGA } from './load-snippet'; @@ -9,5 +10,33 @@ export class GoogleAnalyticsTelemetry): void { + ga('send', { + hitType: 'event', + eventCategory: 'user-actions', + eventAction: name, + ...eventData + }); + } + + public trackPage(name: string, eventData: Dictionary): void { + ga('send', { + hitType: 'pageview', + page: name, + ...eventData + }); + } + + public trackError(name: string, eventData: Dictionary): void { + ga('send', { + hitType: 'event', + eventCategory: 'error', + eventAction: name, + ...eventData + }); + } }