1
1
import { Injectable , Inject , Optional , NgZone , OnDestroy , InjectionToken } from '@angular/core' ;
2
- import { Subscription , from , Observable , empty } from 'rxjs' ;
3
- import { filter , withLatestFrom , switchMap , map , tap } from 'rxjs/operators' ;
2
+ import { Subscription , from , Observable , empty , of } from 'rxjs' ;
3
+ import { filter , withLatestFrom , switchMap , map , tap , pairwise , startWith , groupBy , mergeMap } from 'rxjs/operators' ;
4
4
import { Router , NavigationEnd , ActivationEnd } from '@angular/router' ;
5
5
import { runOutsideAngular , _lazySDKProxy , _firebaseAppFactory } from '@angular/fire' ;
6
6
import { AngularFireAnalytics } from './analytics' ;
7
7
import { User } from 'firebase/app' ;
8
8
9
- export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken < boolean > ( 'angularfire2.analytics.setCurrentScreen' ) ;
10
- export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken < boolean > ( 'angularfire2.analytics.logScreenViews' ) ;
11
9
export const APP_VERSION = new InjectionToken < string > ( 'angularfire2.analytics.appVersion' ) ;
12
10
export const APP_NAME = new InjectionToken < string > ( 'angularfire2.analytics.appName' ) ;
13
11
14
12
const DEFAULT_APP_VERSION = '?' ;
15
13
const DEFAULT_APP_NAME = 'Angular App' ;
16
14
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
+
17
25
@Injectable ( {
18
26
providedIn : 'root'
19
27
} )
@@ -24,49 +32,66 @@ export class ScreenTrackingService implements OnDestroy {
24
32
constructor (
25
33
analytics : AngularFireAnalytics ,
26
34
@Optional ( ) router :Router ,
27
- @Optional ( ) @Inject ( AUTOMATICALLY_SET_CURRENT_SCREEN ) automaticallySetCurrentScreen :boolean | null ,
28
- @Optional ( ) @Inject ( AUTOMATICALLY_LOG_SCREEN_VIEWS ) automaticallyLogScreenViews :boolean | null ,
29
35
@Optional ( ) @Inject ( APP_VERSION ) providedAppVersion :string | null ,
30
36
@Optional ( ) @Inject ( APP_NAME ) providedAppName :string | null ,
31
37
zone : NgZone
32
38
) {
33
- if ( ! router ) {
34
- // TODO warning about Router
35
- } else if ( automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false ) {
36
- const app_name = providedAppName || DEFAULT_APP_NAME ;
37
- const app_version = providedAppVersion || DEFAULT_APP_VERSION ;
38
- const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
39
- const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
40
- this . disposable = navigationEndEvents . pipe (
41
- withLatestFrom ( activationEndEvents ) ,
42
- switchMap ( ( [ navigationEnd , activationEnd ] ) => {
43
- const url = navigationEnd . url ;
44
- const screen_name = activationEnd . snapshot . routeConfig && activationEnd . snapshot . routeConfig . path || url ;
45
- const outlet = activationEnd . snapshot . outlet ;
46
- const component = activationEnd . snapshot . component ;
47
- const ret = new Array < Promise < void > > ( ) ;
48
- if ( automaticallyLogScreenViews !== false ) {
49
- if ( component ) {
50
- const screen_class = component . hasOwnProperty ( 'name' ) && ( component as any ) . name || component . toString ( ) ;
51
- ret . push ( analytics . logEvent ( "screen_view" , { app_name, screen_class, app_version, screen_name, outlet, url } ) ) ;
52
- } else if ( activationEnd . snapshot . routeConfig && activationEnd . snapshot . routeConfig . loadChildren ) {
53
- ret . push ( ( activationEnd . snapshot . routeConfig . loadChildren as any ) ( ) . then ( ( child :any ) => {
54
- const screen_class = child . name ;
55
- console . log ( "logEvent" , "screen_view" , { app_name, screen_class, app_version, screen_name, outlet, url } ) ;
56
- return analytics . logEvent ( "screen_view" , { app_name, screen_class, app_version, screen_name, outlet, url } ) ;
57
- } ) ) ;
58
- } else {
59
- ret . push ( analytics . logEvent ( "screen_view" , { app_name, app_version, screen_name, outlet, url } ) ) ;
60
- }
61
- }
62
- if ( automaticallySetCurrentScreen !== false ) {
63
- ret . push ( analytics . setCurrentScreen ( screen_name || url , { global : outlet == "primary" } ) ) ;
64
- }
65
- return Promise . all ( ret ) ;
66
- } ) ,
67
- runOutsideAngular ( zone )
68
- ) . subscribe ( ) ;
69
- }
39
+ if ( ! router ) { return this }
40
+ const app_name = providedAppName || DEFAULT_APP_NAME ;
41
+ const app_version = providedAppVersion || DEFAULT_APP_VERSION ;
42
+ const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
43
+ const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
44
+ this . disposable = navigationEndEvents . pipe (
45
+ withLatestFrom ( activationEndEvents ) ,
46
+ 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
54
+ } ;
55
+ const component = activationEnd . snapshot . component ;
56
+ const routeConfig = activationEnd . snapshot . routeConfig ;
57
+ const loadedConfig = routeConfig && ( routeConfig as any ) . _loadedConfig ;
58
+ const loadChildren = routeConfig && routeConfig . loadChildren ;
59
+ if ( component ) {
60
+ return of ( { ...params , firebase_screen_class : nameOrToString ( component ) } ) ;
61
+ } else if ( loadedConfig && loadedConfig . module && loadedConfig . module . _moduleType ) {
62
+ return of ( { ...params , firebase_screen_class : nameOrToString ( loadedConfig . module . _moduleType ) } ) ;
63
+ } else if ( typeof loadChildren === "string" ) {
64
+ // TODO is this an older lazy loading style parse
65
+ return of ( { ...params , firebase_screen_class : loadChildren } ) ;
66
+ } else if ( loadChildren ) {
67
+ // TODO look into the other return types here
68
+ return from ( loadChildren ( ) as Promise < any > ) . pipe ( map ( child => ( { ...params , firebase_screen_class : nameOrToString ( child ) } ) ) ) ;
69
+ } else {
70
+ // TODO figure out what forms of router events I might be missing
71
+ return of ( params ) ;
72
+ }
73
+ } ) ,
74
+ tap ( params => {
75
+ // 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 } )
81
+ }
82
+ } ) ,
83
+ map ( params => ( { firebase_screen_id : nextScreenId ( params ) , ...params } ) ) ,
84
+ groupBy ( params => params . outlet ) ,
85
+ mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
86
+ 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 ,
90
+ ...current !
91
+ } : current ! ) ,
92
+ tap ( params => analytics . logEvent ( 'screen_view' , params ) ) ,
93
+ runOutsideAngular ( zone )
94
+ ) . subscribe ( ) ;
70
95
}
71
96
72
97
ngOnDestroy ( ) {
@@ -99,4 +124,22 @@ export class UserTrackingService implements OnDestroy {
99
124
ngOnDestroy ( ) {
100
125
if ( this . disposable ) { this . disposable . unsubscribe ( ) ; }
101
126
}
102
- }
127
+ }
128
+
129
+ // firebase_screen_id is an INT64 but use INT32 cause javascript
130
+ const randomInt32 = ( ) => Math . floor ( Math . random ( ) * ( 2 ** 32 - 1 ) ) - 2 ** 31 ;
131
+
132
+ const currentScreenIds : { [ key :string ] : number } = { } ;
133
+
134
+ const nextScreenId = ( params :AngularFireAnalyticsEventParams ) => {
135
+ const scope = params . outlet ;
136
+ if ( currentScreenIds . hasOwnProperty ( scope ) ) {
137
+ return ++ currentScreenIds [ scope ] ;
138
+ } else {
139
+ const ret = randomInt32 ( ) ;
140
+ currentScreenIds [ scope ] = ret ;
141
+ return ret ;
142
+ }
143
+ }
144
+
145
+ const nameOrToString = ( it :any ) : string => it . name || it . toString ( ) ;
0 commit comments