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,48 +32,62 @@ 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 firebase_screen_class = component . hasOwnProperty ( 'name' ) && ( component as any ) . name || component . toString ( ) ;
51
- ret . push ( analytics . logEvent ( "screen_view" , { app_name, firebase_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 firebase_screen_class = child . name ;
55
- return analytics . logEvent ( "screen_view" , { app_name, firebase_screen_class, app_version, screen_name, outlet, url } ) ;
56
- } ) ) ;
57
- } else {
58
- ret . push ( analytics . logEvent ( "screen_view" , { app_name, app_version, screen_name, outlet, url } ) ) ;
59
- }
60
- }
61
- if ( automaticallySetCurrentScreen !== false ) {
62
- ret . push ( analytics . setCurrentScreen ( screen_name || url , { global : outlet == "primary" } ) ) ;
63
- }
64
- return Promise . all ( ret ) ;
65
- } ) ,
66
- runOutsideAngular ( zone )
67
- ) . subscribe ( ) ;
68
- }
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 loadChildren = routeConfig && routeConfig . loadChildren ;
58
+ if ( component ) {
59
+ return of ( { ...params , firebase_screen_class : nameOrToString ( component ) } ) ;
60
+ } else if ( typeof loadChildren === "string" ) {
61
+ // TODO is this an older lazy loading style parse
62
+ return of ( { ...params , firebase_screen_class : loadChildren } ) ;
63
+ } else if ( loadChildren ) {
64
+ // TODO look into the return types here
65
+ return from ( loadChildren ) . pipe ( map ( child => ( { ...params , firebase_screen_class : nameOrToString ( child ) } ) ) ) ;
66
+ } else {
67
+ // TODO figure out what forms of router events I might be missing
68
+ return of ( params ) ;
69
+ }
70
+ } ) ,
71
+ tap ( params => {
72
+ // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
73
+ if ( params . outlet == "primary" ) {
74
+ // TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id?
75
+ // also shouldn't these be computed in the setCurrentScreen function? prior too?
76
+ analytics . setCurrentScreen ( params . screen_name , { global : true } )
77
+ }
78
+ } ) ,
79
+ map ( params => ( { firebase_screen_id : getScreenId ( params ) , ...params } ) ) ,
80
+ groupBy ( params => params . outlet ) ,
81
+ mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
82
+ map ( ( [ prior , current ] ) => prior ? {
83
+ firebase_previous_class : prior . firebase_screen_class ,
84
+ firebase_previous_screen : prior . firebase_screen ,
85
+ firebase_previous_id : prior . firebase_screen_id ,
86
+ ...current
87
+ } : current ) ,
88
+ switchMap ( params => analytics . logEvent ( 'screen_view' , params ) ) ,
89
+ runOutsideAngular ( zone )
90
+ ) . subscribe ( ) ;
69
91
}
70
92
71
93
ngOnDestroy ( ) {
@@ -98,4 +120,22 @@ export class UserTrackingService implements OnDestroy {
98
120
ngOnDestroy ( ) {
99
121
if ( this . disposable ) { this . disposable . unsubscribe ( ) ; }
100
122
}
101
- }
123
+ }
124
+
125
+ let nextScreenId = Math . floor ( Math . random ( ) * 2 ** 64 ) - 2 ** 63 ;
126
+
127
+ const screenIds : { [ key :string ] : number } = { } ;
128
+
129
+ const getScreenId = ( params :AngularFireAnalyticsEventParams ) => {
130
+ const name = params . screen_name ;
131
+ const existingScreenId = screenIds [ name ] ;
132
+ if ( existingScreenId ) {
133
+ return existingScreenId ;
134
+ } else {
135
+ const screenId = nextScreenId ++ ;
136
+ screenIds [ name ] = screenId ;
137
+ return screenId ;
138
+ }
139
+ }
140
+
141
+ const nameOrToString = ( it :any ) : string => it . name || it . toString ( ) ;
0 commit comments