1- import { Injectable , Inject , Optional , NgZone , OnDestroy , InjectionToken } from '@angular/core' ;
1+ import { Injectable , Optional , NgZone , OnDestroy } from '@angular/core' ;
22import { Subscription , from , Observable , empty , of } from 'rxjs' ;
33import { filter , withLatestFrom , switchMap , map , tap , pairwise , startWith , groupBy , mergeMap } from 'rxjs/operators' ;
44import { Router , NavigationEnd , ActivationEnd } from '@angular/router' ;
5- import { runOutsideAngular , _lazySDKProxy , _firebaseAppFactory } from '@angular/fire' ;
5+ import { runOutsideAngular } from '@angular/fire' ;
66import { AngularFireAnalytics } from './analytics' ;
77import { User } from 'firebase/app' ;
8+ import { Title } from '@angular/platform-browser' ;
89
9- export const APP_VERSION = new InjectionToken < string > ( 'angularfire2.analytics.appVersion' ) ;
10- export const APP_NAME = new InjectionToken < string > ( 'angularfire2.analytics.appName' ) ;
10+ // Gold seems to take page_title and screen_path but the v2 protocol doesn't seem
11+ // to allow any class name, obviously v2 was designed for the basic web. I'm still
12+ // sending firebase_screen_class (largely for BQ compatability) but the Firebase Console
13+ // doesn't appear to be consuming the event properties.
14+ // FWIW I'm seeing notes that firebase_* is depreciated in favor of ga_* in GMS... so IDK
15+ const SCREEN_NAME_KEY = 'screen_name' ;
16+ const PAGE_PATH_KEY = 'page_path' ;
17+ const EVENT_ORIGIN_KEY = 'event_origin' ;
18+ const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen' ;
19+ const SCREEN_CLASS_KEY = 'firebase_screen_class' ;
20+ const OUTLET_KEY = 'outlet' ;
21+ const PAGE_TITLE_KEY = 'page_title' ;
22+ const PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class' ;
23+ const PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id' ;
24+ const PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen' ;
25+ const SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id' ;
1126
12- const DEFAULT_APP_VERSION = '?' ;
13- const DEFAULT_APP_NAME = 'Angular App' ;
27+ const SCREEN_VIEW_EVENT = 'screen_view' ;
28+ const EVENT_ORIGIN_AUTO = 'auto' ;
29+ const DEFAULT_SCREEN_CLASS = '???' ;
30+ const NG_PRIMARY_OUTLET = 'primary' ;
31+ const SCREEN_INSTANCE_DELIMITER = '#' ;
1432
15- type AngularFireAnalyticsEventParams = {
16- app_name : string ;
17- firebase_screen_class : string | undefined ;
18- firebase_screen : string ;
19- app_version : string ;
20- screen_name : string ;
21- outlet : string ;
22- url : string ;
23- } ;
24-
25- @Injectable ( {
26- providedIn : 'root'
27- } )
33+ @Injectable ( )
2834export class ScreenTrackingService implements OnDestroy {
2935
3036 private disposable : Subscription | undefined ;
3137
3238 constructor (
3339 analytics : AngularFireAnalytics ,
3440 @Optional ( ) router :Router ,
35- @Optional ( ) @Inject ( APP_VERSION ) providedAppVersion :string | null ,
36- @Optional ( ) @Inject ( APP_NAME ) providedAppName :string | null ,
41+ @Optional ( ) title :Title ,
3742 zone : NgZone
3843 ) {
3944 if ( ! router ) { return this }
40- const app_name = providedAppName || DEFAULT_APP_NAME ;
41- const app_version = providedAppVersion || DEFAULT_APP_VERSION ;
4245 const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
4346 const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
4447 this . disposable = navigationEndEvents . pipe (
4548 withLatestFrom ( activationEndEvents ) ,
4649 switchMap ( ( [ navigationEnd , activationEnd ] ) => {
47- const url = navigationEnd . url ;
48- const screen_name = activationEnd . snapshot . routeConfig && activationEnd . snapshot . routeConfig . path || url ;
49- const params : AngularFireAnalyticsEventParams = {
50- app_name, app_version, screen_name, url,
51- firebase_screen_class : undefined ,
52- firebase_screen : screen_name ,
53- outlet : activationEnd . snapshot . outlet
50+ // SEMVER: start using optional chains and nullish coalescing once we support newer typescript
51+ const page_path = navigationEnd . url ;
52+ const screen_name = activationEnd . snapshot . routeConfig && activationEnd . snapshot . routeConfig . path || page_path ;
53+ const params = {
54+ [ SCREEN_NAME_KEY ] : screen_name ,
55+ [ PAGE_PATH_KEY ] : page_path ,
56+ [ EVENT_ORIGIN_KEY ] : EVENT_ORIGIN_AUTO ,
57+ [ FIREBASE_SCREEN_NAME_KEY ] : screen_name ,
58+ [ OUTLET_KEY ] : activationEnd . snapshot . outlet
5459 } ;
60+ if ( title ) { params [ PAGE_TITLE_KEY ] = title . getTitle ( ) }
5561 const component = activationEnd . snapshot . component ;
5662 const routeConfig = activationEnd . snapshot . routeConfig ;
63+ // TODO maybe not lean on _loadedConfig...
5764 const loadedConfig = routeConfig && ( routeConfig as any ) . _loadedConfig ;
5865 const loadChildren = routeConfig && routeConfig . loadChildren ;
5966 if ( component ) {
60- return of ( { ...params , firebase_screen_class : nameOrToString ( component ) } ) ;
67+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( component ) } ) ;
6168 } else if ( loadedConfig && loadedConfig . module && loadedConfig . module . _moduleType ) {
62- return of ( { ...params , firebase_screen_class : nameOrToString ( loadedConfig . module . _moduleType ) } ) ;
69+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( loadedConfig . module . _moduleType ) } ) ;
6370 } else if ( typeof loadChildren === "string" ) {
64- // TODO is this an older lazy loading style parse
65- return of ( { ...params , firebase_screen_class : loadChildren } ) ;
71+ // TODO is the an older lazy loading style? parse, if so
72+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : loadChildren } ) ;
6673 } else if ( loadChildren ) {
6774 // TODO look into the other return types here
68- return from ( loadChildren ( ) as Promise < any > ) . pipe ( map ( child => ( { ...params , firebase_screen_class : nameOrToString ( child ) } ) ) ) ;
75+ return from ( loadChildren ( ) as Promise < any > ) . pipe ( map ( child => ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( child ) } ) ) ) ;
6976 } else {
7077 // TODO figure out what forms of router events I might be missing
71- return of ( params ) ;
78+ return of ( { ... params , [ SCREEN_CLASS_KEY ] : DEFAULT_SCREEN_CLASS } ) ;
7279 }
7380 } ) ,
7481 tap ( params => {
7582 // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
76- if ( params . outlet == "primary" ) {
77- // TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id ?
78- // also shouldn't these be computed in the setCurrentScreen function? prior too?
79- // do we want to be logging screen name or class?
80- analytics . setCurrentScreen ( params . screen_name , { global : true } )
83+ if ( params [ OUTLET_KEY ] == NG_PRIMARY_OUTLET ) {
84+ // TODO do we want to track the firebase_ attributes ?
85+ analytics . setCurrentScreen ( params . screen_name ) ;
86+ analytics . updateConfig ( { [ PAGE_PATH_KEY ] : params [ PAGE_PATH_KEY ] } ) ;
87+ if ( title ) { analytics . updateConfig ( { [ PAGE_TITLE_KEY ] : params [ PAGE_TITLE_KEY ] } ) }
8188 }
8289 } ) ,
83- map ( params => ( { firebase_screen_id : nextScreenId ( params ) , ...params } ) ) ,
84- groupBy ( params => params . outlet ) ,
90+ map ( params => ( { [ SCREEN_INSTANCE_ID_KEY ] : getScreenInstanceID ( params ) , ...params } ) ) ,
91+ groupBy ( params => params [ OUTLET_KEY ] ) ,
8592 mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
8693 map ( ( [ prior , current ] ) => prior ? {
87- firebase_previous_class : prior . firebase_screen_class ,
88- firebase_previous_screen : prior . firebase_screen ,
89- firebase_previous_id : prior . firebase_screen_id ,
94+ [ PREVIOUS_SCREEN_CLASS_KEY ] : prior [ SCREEN_CLASS_KEY ] ,
95+ [ PREVIOUS_SCREEN_NAME_KEY ] : prior [ SCREEN_NAME_KEY ] ,
96+ [ PREVIOUS_SCREEN_INSTANCE_ID_KEY ] : prior [ SCREEN_INSTANCE_ID_KEY ] ,
9097 ...current !
9198 } : current ! ) ,
92- tap ( params => analytics . logEvent ( 'screen_view' , params ) ) ,
99+ tap ( params => analytics . logEvent ( SCREEN_VIEW_EVENT , params ) ) ,
93100 runOutsideAngular ( zone )
94101 ) . subscribe ( ) ;
95102 }
@@ -100,9 +107,7 @@ export class ScreenTrackingService implements OnDestroy {
100107
101108}
102109
103- @Injectable ( {
104- providedIn : 'root'
105- } )
110+ @Injectable ( )
106111export class UserTrackingService implements OnDestroy {
107112
108113 private disposable : Subscription | undefined ;
@@ -116,7 +121,7 @@ export class UserTrackingService implements OnDestroy {
116121 // TODO can I hook into auth being loaded...
117122 map ( app => app . auth ( ) ) ,
118123 switchMap ( auth => auth ? new Observable < User > ( auth . onAuthStateChanged . bind ( auth ) ) : empty ( ) ) ,
119- switchMap ( user => analytics . setUserId ( user ? user . uid : null ! , { global : true } ) ) ,
124+ switchMap ( user => analytics . setUserId ( user ? user . uid : null ! ) ) ,
120125 runOutsideAngular ( zone )
121126 ) . subscribe ( ) ;
122127 }
@@ -126,18 +131,22 @@ export class UserTrackingService implements OnDestroy {
126131 }
127132}
128133
129- // firebase_screen_id is an INT64 but use INT32 cause javascript
130- const randomInt32 = ( ) => Math . floor ( Math . random ( ) * ( 2 ** 32 - 1 ) ) - 2 ** 31 ;
134+ // this is an INT64 in iOS/Android but use INT32 cause javascript
135+ let nextScreenInstanceID = Math . floor ( Math . random ( ) * ( 2 ** 32 - 1 ) ) - 2 ** 31 ;
131136
132- const currentScreenIds : { [ key :string ] : number } = { } ;
137+ const knownScreenInstanceIDs : { [ key :string ] : number } = { } ;
133138
134- const nextScreenId = ( params :AngularFireAnalyticsEventParams ) => {
135- const scope = params . outlet ;
136- if ( currentScreenIds . hasOwnProperty ( scope ) ) {
137- return ++ currentScreenIds [ scope ] ;
139+ const getScreenInstanceID = ( params :{ [ key :string ] : any } ) => {
140+ // unique the screen class against the outlet name
141+ const screenInstanceKey = [
142+ params [ SCREEN_CLASS_KEY ] ,
143+ params [ OUTLET_KEY ]
144+ ] . join ( SCREEN_INSTANCE_DELIMITER ) ;
145+ if ( knownScreenInstanceIDs . hasOwnProperty ( screenInstanceKey ) ) {
146+ return knownScreenInstanceIDs [ screenInstanceKey ] ;
138147 } else {
139- const ret = randomInt32 ( ) ;
140- currentScreenIds [ scope ] = ret ;
148+ const ret = nextScreenInstanceID ++ ;
149+ knownScreenInstanceIDs [ screenInstanceKey ] = ret ;
141150 return ret ;
142151 }
143152}
0 commit comments