Skip to content

Commit 3c1ad1f

Browse files
committed
Flushing out RC more and fixing SSR issues
1 parent 460170c commit 3c1ad1f

File tree

6 files changed

+218
-168
lines changed

6 files changed

+218
-168
lines changed

src/analytics/analytics.service.ts

Lines changed: 92 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,25 @@
1-
import { Injectable, Optional, NgZone, OnDestroy } from '@angular/core';
1+
import { Injectable, Optional, NgZone, OnDestroy, ComponentFactoryResolver, Inject, PLATFORM_ID } from '@angular/core';
22
import { Subscription, from, Observable, empty, of } from 'rxjs';
33
import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators';
44
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
55
import { runOutsideAngular } from '@angular/fire';
66
import { AngularFireAnalytics } from './analytics';
77
import { User } from 'firebase/app';
88
import { 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';
1915
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';
2018
const OUTLET_KEY = 'outlet';
19+
const PAGE_PATH_KEY = 'page_path';
2120
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?
2821
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';
3123

3224
const SCREEN_VIEW_EVENT = 'screen_view';
3325
const 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+
}

src/analytics/analytics.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface AngularFireAnalytics extends AnalyticsProxy {};
3636
@Injectable()
3737
export class AngularFireAnalytics {
3838

39-
private gtag: (...args: any[]) => {};
39+
private gtag: (...args: any[]) => void;
4040
private analyticsInitialized: Promise<void>;
4141

4242
async updateConfig(config: {[key:string]: any}) {
@@ -51,22 +51,28 @@ export class AngularFireAnalytics {
5151
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
5252
@Optional() @Inject(APP_NAME) providedAppName:string|null,
5353
@Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null,
54-
@Inject(PLATFORM_ID) platformId,
54+
@Inject(PLATFORM_ID) platformId:Object,
5555
zone: NgZone
5656
) {
5757

58-
if (isPlatformBrowser(platformId)) {
59-
window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || [];
60-
this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) }
61-
this.analyticsInitialized = new Promise(resolve => {
58+
if (!isPlatformBrowser(platformId)) {
59+
// TODO flush out non-browser support
60+
this.analyticsInitialized = Promise.resolve();
61+
this.gtag = () => {}
62+
// TODO fix the proxy for the server
63+
return this;
64+
}
65+
66+
window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || [];
67+
this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) }
68+
this.analyticsInitialized = zone.runOutsideAngular(() =>
69+
new Promise(resolve => {
6270
window[GTAG_FUNCTION_NAME] = (...args: any[]) => {
6371
if (args[0] == 'js') { resolve() }
6472
this.gtag(...args);
6573
}
66-
});
67-
} else {
68-
this.analyticsInitialized = Promise.reject();
69-
}
74+
})
75+
);
7076

7177
if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) }
7278
if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) }
@@ -86,6 +92,7 @@ export class AngularFireAnalytics {
8692
);
8793

8894
return ɵlazySDKProxy(this, analytics, zone);
95+
8996
}
9097

9198
}

src/core/angularfire2.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,20 @@ export const ɵlazySDKProxy = (klass: any, observable: Observable<any>, zone: Ng
8484
get: (_, name) => zone.runOutsideAngular(() =>
8585
klass[name] || new Proxy(() =>
8686
observable.toPromise().then(mod => {
87-
const ret = mod[name];
88-
// TODO move to proper type guards
89-
if (typeof ret == 'function') {
90-
return ret.bind(mod);
91-
} else if (ret && ret.then) {
92-
return ret.then((res:any) => zone.run(() => res));
87+
if (mod) {
88+
const ret = mod[name];
89+
// TODO move to proper type guards
90+
if (typeof ret == 'function') {
91+
return ret.bind(mod);
92+
} else if (ret && ret.then) {
93+
return ret.then((res:any) => zone.run(() => res));
94+
} else {
95+
return zone.run(() => ret);
96+
}
9397
} else {
94-
return zone.run(() => ret);
98+
// the module is not available, SSR maybe?
99+
// TODO dig into this deeper, maybe return a never resolving promise?
100+
return () => {};
95101
}
96102
}), {
97103
get: (self, name) => self()[name],
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { NgModule } from '@angular/core';
2+
import { AngularFireRemoteConfig } from './remote-config';
23

3-
@NgModule()
4+
@NgModule({
5+
providers: [AngularFireRemoteConfig]
6+
})
47
export class AngularFireRemoteConfigModule { }

0 commit comments

Comments
 (0)