1
- import { Injectable , Inject , Optional , NgZone , OnDestroy , InjectionToken } from '@angular/core' ;
1
+ import { Injectable , Optional , NgZone , OnDestroy } from '@angular/core' ;
2
2
import { Subscription , from , Observable , empty , of } from 'rxjs' ;
3
3
import { filter , withLatestFrom , switchMap , map , tap , pairwise , startWith , groupBy , mergeMap } from 'rxjs/operators' ;
4
4
import { Router , NavigationEnd , ActivationEnd } from '@angular/router' ;
5
- import { runOutsideAngular , _lazySDKProxy , _firebaseAppFactory } from '@angular/fire' ;
5
+ import { runOutsideAngular } from '@angular/fire' ;
6
6
import { AngularFireAnalytics } from './analytics' ;
7
7
import { User } from 'firebase/app' ;
8
+ import { Title } from '@angular/platform-browser' ;
8
9
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' ;
11
26
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 = '#' ;
14
32
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 ( )
28
34
export class ScreenTrackingService implements OnDestroy {
29
35
30
36
private disposable : Subscription | undefined ;
31
37
32
38
constructor (
33
39
analytics : AngularFireAnalytics ,
34
40
@Optional ( ) router :Router ,
35
- @Optional ( ) @Inject ( APP_VERSION ) providedAppVersion :string | null ,
36
- @Optional ( ) @Inject ( APP_NAME ) providedAppName :string | null ,
41
+ @Optional ( ) title :Title ,
37
42
zone : NgZone
38
43
) {
39
44
if ( ! router ) { return this }
40
- const app_name = providedAppName || DEFAULT_APP_NAME ;
41
- const app_version = providedAppVersion || DEFAULT_APP_VERSION ;
42
45
const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
43
46
const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
44
47
this . disposable = navigationEndEvents . pipe (
45
48
withLatestFrom ( activationEndEvents ) ,
46
49
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
54
59
} ;
60
+ if ( title ) { params [ PAGE_TITLE_KEY ] = title . getTitle ( ) }
55
61
const component = activationEnd . snapshot . component ;
56
62
const routeConfig = activationEnd . snapshot . routeConfig ;
63
+ // TODO maybe not lean on _loadedConfig...
57
64
const loadedConfig = routeConfig && ( routeConfig as any ) . _loadedConfig ;
58
65
const loadChildren = routeConfig && routeConfig . loadChildren ;
59
66
if ( component ) {
60
- return of ( { ...params , firebase_screen_class : nameOrToString ( component ) } ) ;
67
+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( component ) } ) ;
61
68
} 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 ) } ) ;
63
70
} 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 } ) ;
66
73
} else if ( loadChildren ) {
67
74
// 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 ) } ) ) ) ;
69
76
} else {
70
77
// 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 } ) ;
72
79
}
73
80
} ) ,
74
81
tap ( params => {
75
82
// 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 ] } ) }
81
88
}
82
89
} ) ,
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 ] ) ,
85
92
mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
86
93
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 ] ,
90
97
...current !
91
98
} : current ! ) ,
92
- tap ( params => analytics . logEvent ( 'screen_view' , params ) ) ,
99
+ tap ( params => analytics . logEvent ( SCREEN_VIEW_EVENT , params ) ) ,
93
100
runOutsideAngular ( zone )
94
101
) . subscribe ( ) ;
95
102
}
@@ -100,9 +107,7 @@ export class ScreenTrackingService implements OnDestroy {
100
107
101
108
}
102
109
103
- @Injectable ( {
104
- providedIn : 'root'
105
- } )
110
+ @Injectable ( )
106
111
export class UserTrackingService implements OnDestroy {
107
112
108
113
private disposable : Subscription | undefined ;
@@ -116,7 +121,7 @@ export class UserTrackingService implements OnDestroy {
116
121
// TODO can I hook into auth being loaded...
117
122
map ( app => app . auth ( ) ) ,
118
123
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 ! ) ) ,
120
125
runOutsideAngular ( zone )
121
126
) . subscribe ( ) ;
122
127
}
@@ -126,18 +131,22 @@ export class UserTrackingService implements OnDestroy {
126
131
}
127
132
}
128
133
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 ;
131
136
132
- const currentScreenIds : { [ key :string ] : number } = { } ;
137
+ const knownScreenInstanceIDs : { [ key :string ] : number } = { } ;
133
138
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 ] ;
138
147
} else {
139
- const ret = randomInt32 ( ) ;
140
- currentScreenIds [ scope ] = ret ;
148
+ const ret = nextScreenInstanceID ++ ;
149
+ knownScreenInstanceIDs [ screenInstanceKey ] = ret ;
141
150
return ret ;
142
151
}
143
152
}
0 commit comments