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,48 +32,62 @@ 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 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 ( ) ;
6991 }
7092
7193 ngOnDestroy ( ) {
@@ -98,4 +120,22 @@ export class UserTrackingService implements OnDestroy {
98120 ngOnDestroy ( ) {
99121 if ( this . disposable ) { this . disposable . unsubscribe ( ) ; }
100122 }
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