1- import { Injectable , Optional , NgZone , OnDestroy } from '@angular/core' ;
1+ import { Injectable , Optional , NgZone , OnDestroy , ComponentFactoryResolver , Inject , PLATFORM_ID } from '@angular/core' ;
22import { Subscription , from , Observable , empty , of } from 'rxjs' ;
33import { filter , withLatestFrom , switchMap , map , tap , pairwise , startWith , groupBy , mergeMap } from 'rxjs/operators' ;
44import { Router , NavigationEnd , ActivationEnd } from '@angular/router' ;
55import { runOutsideAngular } from '@angular/fire' ;
66import { AngularFireAnalytics } from './analytics' ;
77import { User } from 'firebase/app' ;
88import { Title } from '@angular/platform-browser' ;
9+ import { isPlatformBrowser } from '@angular/common' ;
910
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' ;
11+ const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin' ;
12+ const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class' ;
13+ const FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id' ;
14+ const FIREBASE_PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen' ;
1915const FIREBASE_SCREEN_CLASS_KEY = 'firebase_screen_class' ;
16+ const FIREBASE_SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id' ;
17+ const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen' ;
2018const OUTLET_KEY = 'outlet' ;
19+ const PAGE_PATH_KEY = 'page_path' ;
2120const 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' ;
26-
27- // Do I need these?
2821const SCREEN_CLASS_KEY = 'screen_class' ;
29- const GA_SCREEN_CLASS_KEY = 'ga_screen_class' ;
30- const GA_SCREEN_NAME_KEY = 'ga_screen' ;
22+ const SCREEN_NAME_KEY = 'screen_name' ;
3123
3224const SCREEN_VIEW_EVENT = 'screen_view' ;
3325const EVENT_ORIGIN_AUTO = 'auto' ;
@@ -44,77 +36,89 @@ export class ScreenTrackingService implements OnDestroy {
4436 analytics : AngularFireAnalytics ,
4537 @Optional ( ) router :Router ,
4638 @Optional ( ) title :Title ,
39+ componentFactoryResolver : ComponentFactoryResolver ,
40+ @Inject ( PLATFORM_ID ) platformId :Object ,
4741 zone : NgZone
4842 ) {
49- if ( ! router ) { return this }
50- const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
51- const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
52- this . disposable = navigationEndEvents . pipe (
53- withLatestFrom ( activationEndEvents ) ,
54- switchMap ( ( [ navigationEnd , activationEnd ] ) => {
55- // SEMVER: start using optional chains and nullish coalescing once we support newer typescript
56- const page_path = navigationEnd . url ;
57- const screen_name = activationEnd . snapshot . routeConfig && activationEnd . snapshot . routeConfig . path || page_path ;
58- const params = {
59- [ SCREEN_NAME_KEY ] : screen_name ,
60- [ PAGE_PATH_KEY ] : page_path ,
61- [ EVENT_ORIGIN_KEY ] : EVENT_ORIGIN_AUTO ,
62- // TODO remove unneeded, just testing here
63- [ FIREBASE_SCREEN_NAME_KEY ] : `${ screen_name } (firebase)` ,
64- [ GA_SCREEN_NAME_KEY ] : `${ screen_name } (ga)` ,
65- [ OUTLET_KEY ] : activationEnd . snapshot . outlet
66- } ;
67- if ( title ) { params [ PAGE_TITLE_KEY ] = title . getTitle ( ) }
68- const component = activationEnd . snapshot . component ;
69- const routeConfig = activationEnd . snapshot . routeConfig ;
70- // TODO maybe not lean on _loadedConfig...
71- const loadedConfig = routeConfig && ( routeConfig as any ) . _loadedConfig ;
72- const loadChildren = routeConfig && routeConfig . loadChildren ;
73- if ( component ) {
74- return of ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( component ) } ) ;
75- } else if ( loadedConfig && loadedConfig . module && loadedConfig . module . _moduleType ) {
76- return of ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( loadedConfig . module . _moduleType ) } ) ;
77- } else if ( typeof loadChildren === "string" ) {
78- // TODO is the an older lazy loading style? parse, if so
79- return of ( { ...params , [ SCREEN_CLASS_KEY ] : loadChildren } ) ;
80- } else if ( loadChildren ) {
81- // TODO look into the other return types here
82- return from ( loadChildren ( ) as Promise < any > ) . pipe ( map ( child => ( { ...params , [ SCREEN_CLASS_KEY ] : nameOrToString ( child ) } ) ) ) ;
83- } else {
84- // TODO figure out what forms of router events I might be missing
85- return of ( { ...params , [ SCREEN_CLASS_KEY ] : DEFAULT_SCREEN_CLASS } ) ;
86- }
87- } ) ,
88- map ( params => ( {
89- // TODO remove unneeded, just testing here
90- [ GA_SCREEN_CLASS_KEY ] : `${ params [ SCREEN_CLASS_KEY ] } (ga)` ,
91- [ FIREBASE_SCREEN_CLASS_KEY ] : `${ params [ SCREEN_CLASS_KEY ] } (firebase)` ,
92- [ SCREEN_INSTANCE_ID_KEY ] : getScreenInstanceID ( params ) ,
93- ...params
94- } ) ) ,
95- tap ( params => {
96- // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
97- if ( params [ OUTLET_KEY ] == NG_PRIMARY_OUTLET ) {
98- // TODO do we want to track the firebase_ attributes?
99- analytics . setCurrentScreen ( params [ SCREEN_NAME_KEY ] ) ;
100- analytics . updateConfig ( {
101- [ PAGE_PATH_KEY ] : params [ PAGE_PATH_KEY ] ,
102- [ SCREEN_CLASS_KEY ] : params [ SCREEN_CLASS_KEY ]
103- } ) ;
104- if ( title ) { analytics . updateConfig ( { [ PAGE_TITLE_KEY ] : params [ PAGE_TITLE_KEY ] } ) }
105- }
106- } ) ,
107- groupBy ( params => params [ OUTLET_KEY ] ) ,
108- mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
109- map ( ( [ prior , current ] ) => prior ? {
110- [ PREVIOUS_SCREEN_CLASS_KEY ] : prior [ SCREEN_CLASS_KEY ] ,
111- [ PREVIOUS_SCREEN_NAME_KEY ] : prior [ SCREEN_NAME_KEY ] ,
112- [ PREVIOUS_SCREEN_INSTANCE_ID_KEY ] : prior [ SCREEN_INSTANCE_ID_KEY ] ,
113- ...current !
114- } : current ! ) ,
115- tap ( params => analytics . logEvent ( SCREEN_VIEW_EVENT , params ) ) ,
116- runOutsideAngular ( zone )
117- ) . subscribe ( ) ;
43+ if ( ! router || ! isPlatformBrowser ( platformId ) ) { return this }
44+ zone . runOutsideAngular ( ( ) => {
45+ const activationEndEvents = router . events . pipe ( filter < ActivationEnd > ( e => e instanceof ActivationEnd ) ) ;
46+ const navigationEndEvents = router . events . pipe ( filter < NavigationEnd > ( e => e instanceof NavigationEnd ) ) ;
47+ this . disposable = navigationEndEvents . pipe (
48+ withLatestFrom ( activationEndEvents ) ,
49+ switchMap ( ( [ navigationEnd , activationEnd ] ) => {
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+ [ FIREBASE_EVENT_ORIGIN_KEY ] : EVENT_ORIGIN_AUTO ,
57+ [ FIREBASE_SCREEN_NAME_KEY ] : screen_name ,
58+ [ OUTLET_KEY ] : activationEnd . snapshot . outlet
59+ } ;
60+ if ( title ) {
61+ params [ PAGE_TITLE_KEY ] = title . getTitle ( )
62+ }
63+ const component = activationEnd . snapshot . component ;
64+ const routeConfig = activationEnd . snapshot . routeConfig ;
65+ const loadChildren = routeConfig && routeConfig . loadChildren ;
66+ // TODO figure out how to handle minification
67+ if ( typeof loadChildren === "string" ) {
68+ // SEMVER: this is the older lazy load style "./path#ClassName", drop this when we drop old ng
69+ // TODO is it worth seeing if I can look up the component factory selector from the module name?
70+ // it's lazy so it's not registered with componentFactoryResolver yet... seems a pain for a depreciated style
71+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : loadChildren . split ( '#' ) [ 1 ] } ) ;
72+ } else if ( typeof component === 'string' ) {
73+ // TODO figure out when this would this be a string
74+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : component } ) ;
75+ } else if ( component ) {
76+ const componentFactory = componentFactoryResolver . resolveComponentFactory ( component ) ;
77+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : componentFactory . selector } ) ;
78+ } else if ( loadChildren ) {
79+ const loadedChildren = loadChildren ( ) ;
80+ var loadedChildren$ : Observable < any > ;
81+ // TODO clean up this handling...
82+ // can componentFactorymoduleType take an ngmodulefactory or should i pass moduletype?
83+ try { loadedChildren$ = from ( zone . runOutsideAngular ( ( ) => loadedChildren as any ) ) } catch ( _ ) { loadedChildren$ = of ( loadedChildren as any ) }
84+ return loadedChildren$ . pipe ( map ( child => {
85+ const componentFactory = componentFactoryResolver . resolveComponentFactory ( child ) ;
86+ return { ...params , [ SCREEN_CLASS_KEY ] : componentFactory . selector } ;
87+ } ) ) ;
88+ } else {
89+ // TODO figure out what forms of router events I might be missing
90+ return of ( { ...params , [ SCREEN_CLASS_KEY ] : DEFAULT_SCREEN_CLASS } ) ;
91+ }
92+ } ) ,
93+ map ( params => ( {
94+ [ FIREBASE_SCREEN_CLASS_KEY ] : params [ SCREEN_CLASS_KEY ] ,
95+ [ FIREBASE_SCREEN_INSTANCE_ID_KEY ] : getScreenInstanceID ( params ) ,
96+ ...params
97+ } ) ) ,
98+ tap ( params => {
99+ // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
100+ if ( params [ OUTLET_KEY ] == NG_PRIMARY_OUTLET ) {
101+ analytics . setCurrentScreen ( params [ SCREEN_NAME_KEY ] ) ;
102+ analytics . updateConfig ( {
103+ [ PAGE_PATH_KEY ] : params [ PAGE_PATH_KEY ] ,
104+ [ SCREEN_CLASS_KEY ] : params [ SCREEN_CLASS_KEY ]
105+ } ) ;
106+ if ( title ) {
107+ analytics . updateConfig ( { [ PAGE_TITLE_KEY ] : params [ PAGE_TITLE_KEY ] } )
108+ }
109+ }
110+ } ) ,
111+ groupBy ( params => params [ OUTLET_KEY ] ) ,
112+ mergeMap ( group => group . pipe ( startWith ( undefined ) , pairwise ( ) ) ) ,
113+ map ( ( [ prior , current ] ) => prior ? {
114+ [ FIREBASE_PREVIOUS_SCREEN_CLASS_KEY ] : prior [ SCREEN_CLASS_KEY ] ,
115+ [ FIREBASE_PREVIOUS_SCREEN_NAME_KEY ] : prior [ SCREEN_NAME_KEY ] ,
116+ [ FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY ] : prior [ FIREBASE_SCREEN_INSTANCE_ID_KEY ] ,
117+ ...current !
118+ } : current ! ) ,
119+ tap ( params => zone . runOutsideAngular ( ( ) => analytics . logEvent ( SCREEN_VIEW_EVENT , params ) ) )
120+ ) . subscribe ( ) ;
121+ } ) ;
118122 }
119123
120124 ngOnDestroy ( ) {
@@ -165,6 +169,4 @@ const getScreenInstanceID = (params:{[key:string]: any}) => {
165169 knownScreenInstanceIDs [ screenInstanceKey ] = ret ;
166170 return ret ;
167171 }
168- }
169-
170- const nameOrToString = ( it :any ) : string => it . name || it . toString ( ) ;
172+ }
0 commit comments