Skip to content

Commit 60c0cad

Browse files
committed
Reworking analytics
1 parent 768c21b commit 60c0cad

File tree

1 file changed

+83
-43
lines changed

1 file changed

+83
-43
lines changed

src/analytics/analytics.service.ts

+83-43
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,27 @@
11
import { 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';
44
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
55
import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire';
66
import { AngularFireAnalytics } from './analytics';
77
import { 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');
119
export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
1210
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
1311

1412
const DEFAULT_APP_VERSION = '?';
1513
const 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

Comments
 (0)