1
- import { Injectable , Optional , NgZone , OnDestroy } from '@angular/core' ;
1
+ import { Injectable , Optional , NgZone , OnDestroy , ComponentFactoryResolver , Inject , PLATFORM_ID } from '@angular/core' ;
2
2
import { Subscription , from , Observable , empty , of } from 'rxjs' ;
3
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 } from '@angular/fire' ;
6
6
import { AngularFireAnalytics } from './analytics' ;
7
7
import { User } from 'firebase/app' ;
8
8
import { Title } from '@angular/platform-browser' ;
9
+ import { isPlatformBrowser } from '@angular/common' ;
9
10
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' ;
19
15
const 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' ;
20
18
const OUTLET_KEY = 'outlet' ;
19
+ const PAGE_PATH_KEY = 'page_path' ;
21
20
const 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?
28
21
const 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' ;
31
23
32
24
const SCREEN_VIEW_EVENT = 'screen_view' ;
33
25
const EVENT_ORIGIN_AUTO = 'auto' ;
@@ -44,77 +36,89 @@ export class ScreenTrackingService implements OnDestroy {
44
36
analytics : AngularFireAnalytics ,
45
37
@Optional ( ) router :Router ,
46
38
@Optional ( ) title :Title ,
39
+ componentFactoryResolver : ComponentFactoryResolver ,
40
+ @Inject ( PLATFORM_ID ) platformId :Object ,
47
41
zone : NgZone
48
42
) {
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
+ } ) ;
118
122
}
119
123
120
124
ngOnDestroy ( ) {
@@ -165,6 +169,4 @@ const getScreenInstanceID = (params:{[key:string]: any}) => {
165
169
knownScreenInstanceIDs [ screenInstanceKey ] = ret ;
166
170
return ret ;
167
171
}
168
- }
169
-
170
- const nameOrToString = ( it :any ) : string => it . name || it . toString ( ) ;
172
+ }
0 commit comments