-
Notifications
You must be signed in to change notification settings - Fork 11
feat: adding user telemetry config and service #1126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
99388e6
feat: adding user telemetry config and service
anandtiwary 0f9cddc
refactor: addresing comments
anandtiwary 1a04418
feat: addressing review comments
anandtiwary 70295db
refactor: addressing review comments
anandtiwary e586488
refactor: addressing review comments
anandtiwary 9e2685e
refactor: formatting fixes
anandtiwary bdbe39d
refactor: fixing lint and adding tests
anandtiwary e0e2a42
revert: reverting package lock changes
anandtiwary 6c42486
refactor: adding console.error
anandtiwary 4d75562
refactor: addressing review comments
anandtiwary 69962ee
Merge branch 'main' into user-telemetry-1
anandtiwary File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { ProviderToken } from '@angular/core'; | ||
| import { Dictionary } from './../utilities/types/types'; | ||
|
|
||
| export interface UserTelemetryRegistrationConfig<TInitConfig> { | ||
| telemetryProvider: ProviderToken<UserTelemetryProvider<TInitConfig>>; | ||
| initConfig: TInitConfig; | ||
| enablePageTracking: boolean; | ||
| enableEventTracking: boolean; | ||
| enableErrorTracking: boolean; | ||
| } | ||
|
|
||
| export interface UserTelemetryProvider<TInitConfig = unknown> { | ||
| initialize(config: TInitConfig): void; | ||
| identify(userTraits: UserTraits): void; | ||
| trackEvent?(name: string, eventData: Dictionary<unknown>): void; | ||
| trackPage?(url: string, eventData: Dictionary<unknown>): void; | ||
| trackError?(error: string, eventData: Dictionary<unknown>): void; | ||
| shutdown?(): void; | ||
| } | ||
|
|
||
| export interface TelemetryProviderConfig { | ||
| orgId: string; | ||
| } | ||
|
|
||
| export interface UserTraits extends Dictionary<unknown> { | ||
| email?: string; | ||
| companyName?: string; | ||
| name?: string; | ||
| displayName?: string; | ||
| } |
221 changes: 221 additions & 0 deletions
221
projects/common/src/telemetry/user-telemetry-helper.service.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| import { InjectionToken } from '@angular/core'; | ||
| 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'; | ||
|
|
||
| describe('User Telemetry helper service', () => { | ||
| const injectionToken = new InjectionToken('test-token'); | ||
| let telemetryProvider: UserTelemetryProvider; | ||
| let registrationConfig: UserTelemetryRegistrationConfig<TelemetryProviderConfig>; | ||
|
|
||
| const createService = createServiceFactory({ | ||
| service: UserTelemetryHelperService, | ||
| providers: [ | ||
| mockProvider(Router, { | ||
| events: of({}) | ||
| }) | ||
| ] | ||
| }); | ||
|
|
||
| test('should delegate to telemetry provider after registration', () => { | ||
| registrationConfig = { | ||
| telemetryProvider: injectionToken, | ||
| initConfig: { orgId: 'test-id' }, | ||
| enablePageTracking: true, | ||
| enableEventTracking: true, | ||
| enableErrorTracking: true | ||
| }; | ||
|
|
||
| telemetryProvider = { | ||
| initialize: jest.fn(), | ||
| identify: jest.fn(), | ||
| trackEvent: jest.fn(), | ||
| trackPage: jest.fn(), | ||
| trackError: jest.fn(), | ||
| shutdown: jest.fn() | ||
| }; | ||
|
|
||
| const spectator = createService({ | ||
| providers: [ | ||
| { | ||
| provide: injectionToken, | ||
| useValue: telemetryProvider | ||
| } | ||
| ] | ||
| }); | ||
|
|
||
| spectator.service.register(registrationConfig); | ||
|
|
||
| // Initialize | ||
| spectator.service.initialize(); | ||
| expect(telemetryProvider.initialize).toHaveBeenCalledWith({ orgId: 'test-id' }); | ||
|
|
||
| // Identify | ||
| spectator.service.identify({ email: '[email protected]' }); | ||
| expect(telemetryProvider.identify).toHaveBeenCalledWith({ email: '[email protected]' }); | ||
|
|
||
| // TrackEvent | ||
| spectator.service.trackEvent('eventA', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackEvent).toHaveBeenCalledWith('eventA', { target: 'unknown' }); | ||
|
|
||
| // TrackPage | ||
| spectator.service.trackPageEvent('/abs', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackPage).toHaveBeenCalledWith('/abs', { target: 'unknown' }); | ||
|
|
||
| // TrackError | ||
| spectator.service.trackErrorEvent('console error', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackError).toHaveBeenCalledWith('Error: console error', { target: 'unknown' }); | ||
| }); | ||
|
|
||
| test('should not capture events if event tracking is disabled', () => { | ||
| registrationConfig = { | ||
| telemetryProvider: injectionToken, | ||
| initConfig: { orgId: 'test-id' }, | ||
| enablePageTracking: true, | ||
| enableEventTracking: false, | ||
| enableErrorTracking: true | ||
| }; | ||
|
|
||
| telemetryProvider = { | ||
| initialize: jest.fn(), | ||
| identify: jest.fn(), | ||
| trackEvent: jest.fn(), | ||
| trackPage: jest.fn(), | ||
| trackError: jest.fn(), | ||
| shutdown: jest.fn() | ||
| }; | ||
|
|
||
| const spectator = createService({ | ||
| providers: [ | ||
| { | ||
| provide: injectionToken, | ||
| useValue: telemetryProvider | ||
| } | ||
| ] | ||
| }); | ||
|
|
||
| spectator.service.register(registrationConfig); | ||
|
|
||
| // Initialize | ||
| spectator.service.initialize(); | ||
| expect(telemetryProvider.initialize).toHaveBeenCalledWith({ orgId: 'test-id' }); | ||
|
|
||
| // Identify | ||
| spectator.service.identify({ email: '[email protected]' }); | ||
| expect(telemetryProvider.identify).toHaveBeenCalledWith({ email: '[email protected]' }); | ||
|
|
||
| // TrackEvent | ||
| spectator.service.trackEvent('eventA', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackEvent).not.toHaveBeenCalled(); | ||
|
|
||
| // TrackPage | ||
| spectator.service.trackPageEvent('/abs', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackPage).toHaveBeenCalledWith('/abs', { target: 'unknown' }); | ||
|
|
||
| // TrackError | ||
| spectator.service.trackErrorEvent('console error', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackError).toHaveBeenCalledWith('Error: console error', { target: 'unknown' }); | ||
| }); | ||
|
|
||
| test('should not capture page events if page event tracking is disabled', () => { | ||
| registrationConfig = { | ||
| telemetryProvider: injectionToken, | ||
| initConfig: { orgId: 'test-id' }, | ||
| enablePageTracking: false, | ||
| enableEventTracking: true, | ||
| enableErrorTracking: true | ||
| }; | ||
|
|
||
| telemetryProvider = { | ||
| initialize: jest.fn(), | ||
| identify: jest.fn(), | ||
| trackEvent: jest.fn(), | ||
| trackPage: jest.fn(), | ||
| trackError: jest.fn(), | ||
| shutdown: jest.fn() | ||
| }; | ||
|
|
||
| const spectator = createService({ | ||
| providers: [ | ||
| { | ||
| provide: injectionToken, | ||
| useValue: telemetryProvider | ||
| } | ||
| ] | ||
| }); | ||
|
|
||
| spectator.service.register(registrationConfig); | ||
|
|
||
| // Initialize | ||
| spectator.service.initialize(); | ||
| expect(telemetryProvider.initialize).toHaveBeenCalledWith({ orgId: 'test-id' }); | ||
|
|
||
| // Identify | ||
| spectator.service.identify({ email: '[email protected]' }); | ||
| expect(telemetryProvider.identify).toHaveBeenCalledWith({ email: '[email protected]' }); | ||
|
|
||
| // TrackEvent | ||
| spectator.service.trackEvent('eventA', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackEvent).toHaveBeenCalledWith('eventA', { target: 'unknown' }); | ||
|
|
||
| // TrackPage | ||
| spectator.service.trackPageEvent('/abs', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackPage).not.toHaveBeenCalled(); | ||
|
|
||
| // TrackError | ||
| spectator.service.trackErrorEvent('console error', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackError).toHaveBeenCalledWith('Error: console error', { target: 'unknown' }); | ||
| }); | ||
|
|
||
| test('should not capture error events if eror event tracking is disabled', () => { | ||
| registrationConfig = { | ||
| telemetryProvider: injectionToken, | ||
| initConfig: { orgId: 'test-id' }, | ||
| enablePageTracking: true, | ||
| enableEventTracking: true, | ||
| enableErrorTracking: false | ||
| }; | ||
|
|
||
| telemetryProvider = { | ||
| initialize: jest.fn(), | ||
| identify: jest.fn(), | ||
| trackEvent: jest.fn(), | ||
| trackPage: jest.fn(), | ||
| trackError: jest.fn(), | ||
| shutdown: jest.fn() | ||
| }; | ||
|
|
||
| const spectator = createService({ | ||
| providers: [ | ||
| { | ||
| provide: injectionToken, | ||
| useValue: telemetryProvider | ||
| } | ||
| ] | ||
| }); | ||
|
|
||
| spectator.service.register(registrationConfig); | ||
|
|
||
| // Initialize | ||
| spectator.service.initialize(); | ||
| expect(telemetryProvider.initialize).toHaveBeenCalledWith({ orgId: 'test-id' }); | ||
|
|
||
| // Identify | ||
| spectator.service.identify({ email: '[email protected]' }); | ||
| expect(telemetryProvider.identify).toHaveBeenCalledWith({ email: '[email protected]' }); | ||
|
|
||
| // TrackEvent | ||
| spectator.service.trackEvent('eventA', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackEvent).toHaveBeenCalledWith('eventA', { target: 'unknown' }); | ||
|
|
||
| // TrackPage | ||
| spectator.service.trackPageEvent('/abs', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackPage).toHaveBeenCalledWith('/abs', { target: 'unknown' }); | ||
|
|
||
| // TrackError | ||
| spectator.service.trackPageEvent('console error', { target: 'unknown' }); | ||
| expect(telemetryProvider.trackError).not.toHaveBeenCalled(); | ||
| }); | ||
| }); |
82 changes: 82 additions & 0 deletions
82
projects/common/src/telemetry/user-telemetry-helper.service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| import { Injectable, Injector } from '@angular/core'; | ||
| import { NavigationEnd, Router } from '@angular/router'; | ||
| import { filter } from 'rxjs/operators'; | ||
| import { Dictionary } from '../utilities/types/types'; | ||
| import { UserTelemetryProvider, UserTelemetryRegistrationConfig, UserTraits } from './telemetry'; | ||
|
|
||
| @Injectable({ providedIn: 'root' }) | ||
| export class UserTelemetryHelperService { | ||
| private telemetryProviders: UserTelemetryInternalConfig[] = []; | ||
|
|
||
| public constructor(private readonly injector: Injector, private readonly router: Router) { | ||
| this.setupAutomaticPageTracking(); | ||
| } | ||
|
|
||
| public register(...configs: UserTelemetryRegistrationConfig<unknown>[]): void { | ||
| try { | ||
| const providers = configs.map(config => this.buildTelemetryProvider(config)); | ||
| this.telemetryProviders = [...this.telemetryProviders, ...providers]; | ||
| } catch (error) { | ||
| /** | ||
| * Fail silently | ||
| */ | ||
|
|
||
| // tslint:disable-next-line: no-console | ||
| console.error(error); | ||
| } | ||
| } | ||
|
|
||
| public initialize(): void { | ||
| this.telemetryProviders.forEach(provider => provider.telemetryProvider.initialize(provider.initConfig)); | ||
| } | ||
|
|
||
| public identify(userTraits: UserTraits): void { | ||
| this.telemetryProviders.forEach(provider => provider.telemetryProvider.identify(userTraits)); | ||
| } | ||
|
|
||
| public shutdown(): void { | ||
| this.telemetryProviders.forEach(provider => provider.telemetryProvider.shutdown?.()); | ||
| } | ||
|
|
||
| public trackEvent(name: string, data: Dictionary<unknown>): void { | ||
| this.telemetryProviders | ||
| .filter(provider => provider.enableEventTracking) | ||
| .forEach(provider => provider.telemetryProvider.trackEvent?.(name, data)); | ||
| } | ||
|
|
||
| public trackPageEvent(url: string, data: Dictionary<unknown>): void { | ||
| this.telemetryProviders | ||
| .filter(provider => provider.enablePageTracking) | ||
| .forEach(provider => provider.telemetryProvider.trackPage?.(url, data)); | ||
| } | ||
|
|
||
| public trackErrorEvent(error: string, data: Dictionary<unknown>): void { | ||
| this.telemetryProviders | ||
| .filter(provider => provider.enableErrorTracking) | ||
| .forEach(provider => provider.telemetryProvider.trackError?.(`Error: ${error}`, data)); | ||
| } | ||
|
|
||
| private buildTelemetryProvider(config: UserTelemetryRegistrationConfig<unknown>): UserTelemetryInternalConfig { | ||
| const providerInstance = this.injector.get(config.telemetryProvider); | ||
| providerInstance.initialize(config.initConfig); | ||
|
|
||
| return { | ||
| ...config, | ||
| telemetryProvider: providerInstance | ||
| }; | ||
| } | ||
|
|
||
| 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<InitConfig = unknown> { | ||
| telemetryProvider: UserTelemetryProvider<InitConfig>; | ||
| initConfig: InitConfig; | ||
| enablePageTracking: boolean; | ||
| enableEventTracking: boolean; | ||
| enableErrorTracking: boolean; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { Inject, ModuleWithProviders, NgModule, InjectionToken } from '@angular/core'; | ||
| import { UserTelemetryRegistrationConfig } from './telemetry'; | ||
| import { UserTelemetryHelperService } from './user-telemetry-helper.service'; | ||
|
|
||
| @NgModule() | ||
| export class UserTelemetryModule { | ||
| public constructor( | ||
| @Inject(USER_TELEMETRY_PROVIDER_TOKENS) providerConfigs: UserTelemetryRegistrationConfig<unknown>[][], | ||
| userTelemetryInternalService: UserTelemetryHelperService | ||
| ) { | ||
| userTelemetryInternalService.register(...providerConfigs.flat()); | ||
anandtiwary marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| public static forRoot( | ||
| providerConfigs: UserTelemetryRegistrationConfig<unknown>[] | ||
| ): ModuleWithProviders<UserTelemetryModule> { | ||
| return { | ||
| ngModule: UserTelemetryModule, | ||
| providers: [ | ||
| { | ||
| provide: USER_TELEMETRY_PROVIDER_TOKENS, | ||
| useValue: providerConfigs | ||
| } | ||
| ] | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| const USER_TELEMETRY_PROVIDER_TOKENS = new InjectionToken<UserTelemetryRegistrationConfig<unknown>[][]>( | ||
| 'USER_TELEMETRY_PROVIDER_TOKENS' | ||
| ); | ||
28 changes: 28 additions & 0 deletions
28
projects/common/src/telemetry/user-telemetry.service.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| 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: '[email protected]' }); | ||
| expect(helperService.initialize).toHaveBeenCalledWith(); | ||
| expect(helperService.identify).toHaveBeenCalledWith({ email: '[email protected]' }); | ||
|
|
||
| spectator.service.shutdown(); | ||
| expect(helperService.shutdown).toHaveBeenCalledWith(); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.