11import { 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' ;
44import { Router , NavigationEnd , ActivationEnd } from '@angular/router' ;
55import { runOutsideAngular , _lazySDKProxy , _firebaseAppFactory } from '@angular/fire' ;
66import { AngularFireAnalytics } from './analytics' ;
77import { User } from 'firebase/app' ;
88
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' ) ;
119export const APP_VERSION = new InjectionToken < string > ( 'angularfire2.analytics.appVersion' ) ;
1210export const APP_NAME = new InjectionToken < string > ( 'angularfire2.analytics.appName' ) ;
1311
1412const DEFAULT_APP_VERSION = '?' ;
1513const DEFAULT_APP_NAME = 'Angular App' ;
1614
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+
1725@Injectable ( {
1826 providedIn : 'root'
1927} )
@@ -24,49 +32,66 @@ export class ScreenTrackingService implements OnDestroy {
2432 constructor (
2533 analytics : AngularFireAnalytics ,
2634 @Optional ( ) router :Router ,
27- @Optional ( ) @Inject ( AUTOMATICALLY_SET_CURRENT_SCREEN ) automaticallySetCurrentScreen :boolean | null ,
28- @Optional ( ) @Inject ( AUTOMATICALLY_LOG_SCREEN_VIEWS ) automaticallyLogScreenViews :boolean | null ,
2935 @Optional ( ) @Inject ( APP_VERSION ) providedAppVersion :string | null ,
3036 @Optional ( ) @Inject ( APP_NAME ) providedAppName :string | null ,
3137 zone : NgZone
3238 ) {
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 ( ) ;
7095 }
7196
7297 ngOnDestroy ( ) {
@@ -99,4 +124,22 @@ export class UserTrackingService implements OnDestroy {
99124 ngOnDestroy ( ) {
100125 if ( this . disposable ) { this . disposable . unsubscribe ( ) ; }
101126 }
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