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 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 03/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 04/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 05/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 06/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 07/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 08/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 09/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 adf577b8ed34aca7f28b36035d7ca6114e9e2def Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Sun, 26 Sep 2021 21:21:05 -0700 Subject: [PATCH 10/11] refactor: adding only few properties --- projects/common/src/telemetry/track/track.directive.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index b93f0de7f..2c37d4745 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -58,8 +58,12 @@ export class TrackDirective implements OnInit, OnChanges, OnDestroy { } private trackUserEvent(userEvent: string, eventObj: MouseEvent): void { + const targetElement = eventObj.target as HTMLElement; this.userTelemetryImplService.trackEvent(`${userEvent}: ${this.trackedEventLabel}`, { - ...(eventObj.target as HTMLElement), + tagName: targetElement.tagName, + innerHTML: targetElement.innerHTML, + textContent: targetElement.textContent, + className: targetElement.className, type: userEvent }); } From 42bddcadce7cdc310978cc776a0eb0c61c0a8ce9 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Mon, 27 Sep 2021 09:31:37 -0700 Subject: [PATCH 11/11] refactor: addressing review comments --- projects/common/src/telemetry/track/track.directive.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/projects/common/src/telemetry/track/track.directive.ts b/projects/common/src/telemetry/track/track.directive.ts index 2c37d4745..561170699 100644 --- a/projects/common/src/telemetry/track/track.directive.ts +++ b/projects/common/src/telemetry/track/track.directive.ts @@ -61,8 +61,6 @@ export class TrackDirective implements OnInit, OnChanges, OnDestroy { const targetElement = eventObj.target as HTMLElement; this.userTelemetryImplService.trackEvent(`${userEvent}: ${this.trackedEventLabel}`, { tagName: targetElement.tagName, - innerHTML: targetElement.innerHTML, - textContent: targetElement.textContent, className: targetElement.className, type: userEvent });