Skip to content

Commit 91778ff

Browse files
committed
More work on analytics
1 parent 67e1b55 commit 91778ff

File tree

5 files changed

+103
-70
lines changed

5 files changed

+103
-70
lines changed

src/analytics/analytics.module.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { NgModule, Optional } from '@angular/core';
22
import { UserTrackingService, ScreenTrackingService } from './analytics.service';
33
import { AngularFireAnalytics } from './analytics';
44

5-
@NgModule()
5+
@NgModule({
6+
providers: [ AngularFireAnalytics ]
7+
})
68
export class AngularFireAnalyticsModule {
79
constructor(
8-
analytics: AngularFireAnalytics,
910
@Optional() screenTracking: ScreenTrackingService,
1011
@Optional() userTracking: UserTrackingService
1112
) { }

src/analytics/analytics.service.ts

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,102 @@
1-
import { Injectable, Inject, Optional, NgZone, OnDestroy, InjectionToken } from '@angular/core';
1+
import { Injectable, Optional, NgZone, OnDestroy } 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';
5-
import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire';
5+
import { runOutsideAngular } from '@angular/fire';
66
import { AngularFireAnalytics } from './analytics';
77
import { User } from 'firebase/app';
8+
import { Title } from '@angular/platform-browser';
89

9-
export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
10-
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
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';
19+
const SCREEN_CLASS_KEY = 'firebase_screen_class';
20+
const OUTLET_KEY = 'outlet';
21+
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';
1126

12-
const DEFAULT_APP_VERSION = '?';
13-
const DEFAULT_APP_NAME = 'Angular App';
27+
const SCREEN_VIEW_EVENT = 'screen_view';
28+
const EVENT_ORIGIN_AUTO = 'auto';
29+
const DEFAULT_SCREEN_CLASS = '???';
30+
const NG_PRIMARY_OUTLET = 'primary';
31+
const SCREEN_INSTANCE_DELIMITER = '#';
1432

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-
25-
@Injectable({
26-
providedIn: 'root'
27-
})
33+
@Injectable()
2834
export class ScreenTrackingService implements OnDestroy {
2935

3036
private disposable: Subscription|undefined;
3137

3238
constructor(
3339
analytics: AngularFireAnalytics,
3440
@Optional() router:Router,
35-
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
36-
@Optional() @Inject(APP_NAME) providedAppName:string|null,
41+
@Optional() title:Title,
3742
zone: NgZone
3843
) {
3944
if (!router) { return this }
40-
const app_name = providedAppName || DEFAULT_APP_NAME;
41-
const app_version = providedAppVersion || DEFAULT_APP_VERSION;
4245
const activationEndEvents = router.events.pipe(filter<ActivationEnd>(e => e instanceof ActivationEnd));
4346
const navigationEndEvents = router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd));
4447
this.disposable = navigationEndEvents.pipe(
4548
withLatestFrom(activationEndEvents),
4649
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
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+
[EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO,
57+
[FIREBASE_SCREEN_NAME_KEY]: screen_name,
58+
[OUTLET_KEY]: activationEnd.snapshot.outlet
5459
};
60+
if (title) { params[PAGE_TITLE_KEY] = title.getTitle() }
5561
const component = activationEnd.snapshot.component;
5662
const routeConfig = activationEnd.snapshot.routeConfig;
63+
// TODO maybe not lean on _loadedConfig...
5764
const loadedConfig = routeConfig && (routeConfig as any)._loadedConfig;
5865
const loadChildren = routeConfig && routeConfig.loadChildren;
5966
if (component) {
60-
return of({...params, firebase_screen_class: nameOrToString(component) });
67+
return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(component) });
6168
} else if (loadedConfig && loadedConfig.module && loadedConfig.module._moduleType) {
62-
return of({...params, firebase_screen_class: nameOrToString(loadedConfig.module._moduleType)});
69+
return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(loadedConfig.module._moduleType)});
6370
} else if (typeof loadChildren === "string") {
64-
// TODO is this an older lazy loading style parse
65-
return of({...params, firebase_screen_class: loadChildren });
71+
// TODO is the an older lazy loading style? parse, if so
72+
return of({...params, [SCREEN_CLASS_KEY]: loadChildren });
6673
} else if (loadChildren) {
6774
// TODO look into the other return types here
68-
return from(loadChildren() as Promise<any>).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) })));
75+
return from(loadChildren() as Promise<any>).pipe(map(child => ({...params, [SCREEN_CLASS_KEY]: nameOrToString(child) })));
6976
} else {
7077
// TODO figure out what forms of router events I might be missing
71-
return of(params);
78+
return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS});
7279
}
7380
}),
7481
tap(params => {
7582
// TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
76-
if (params.outlet == "primary") {
77-
// TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id?
78-
// also shouldn't these be computed in the setCurrentScreen function? prior too?
79-
// do we want to be logging screen name or class?
80-
analytics.setCurrentScreen(params.screen_name, { global: true })
83+
if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) {
84+
// TODO do we want to track the firebase_ attributes?
85+
analytics.setCurrentScreen(params.screen_name);
86+
analytics.updateConfig({ [PAGE_PATH_KEY]: params[PAGE_PATH_KEY] });
87+
if (title) { analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) }
8188
}
8289
}),
83-
map(params => ({ firebase_screen_id: nextScreenId(params), ...params})),
84-
groupBy(params => params.outlet),
90+
map(params => ({ [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), ...params })),
91+
groupBy(params => params[OUTLET_KEY]),
8592
mergeMap(group => group.pipe(startWith(undefined), pairwise())),
8693
map(([prior, current]) => prior ? {
87-
firebase_previous_class: prior.firebase_screen_class,
88-
firebase_previous_screen: prior.firebase_screen,
89-
firebase_previous_id: prior.firebase_screen_id,
94+
[PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY],
95+
[PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY],
96+
[PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[SCREEN_INSTANCE_ID_KEY],
9097
...current!
9198
} : current!),
92-
tap(params => analytics.logEvent('screen_view', params)),
99+
tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params)),
93100
runOutsideAngular(zone)
94101
).subscribe();
95102
}
@@ -100,9 +107,7 @@ export class ScreenTrackingService implements OnDestroy {
100107

101108
}
102109

103-
@Injectable({
104-
providedIn: 'root'
105-
})
110+
@Injectable()
106111
export class UserTrackingService implements OnDestroy {
107112

108113
private disposable: Subscription|undefined;
@@ -116,7 +121,7 @@ export class UserTrackingService implements OnDestroy {
116121
// TODO can I hook into auth being loaded...
117122
map(app => app.auth()),
118123
switchMap(auth => auth ? new Observable<User>(auth.onAuthStateChanged.bind(auth)) : empty()),
119-
switchMap(user => analytics.setUserId(user ? user.uid : null!, { global: true })),
124+
switchMap(user => analytics.setUserId(user ? user.uid : null!)),
120125
runOutsideAngular(zone)
121126
).subscribe();
122127
}
@@ -126,18 +131,22 @@ export class UserTrackingService implements OnDestroy {
126131
}
127132
}
128133

129-
// firebase_screen_id is an INT64 but use INT32 cause javascript
130-
const randomInt32 = () => Math.floor(Math.random() * (2**32 - 1)) - 2**31;
134+
// this is an INT64 in iOS/Android but use INT32 cause javascript
135+
let nextScreenInstanceID = Math.floor(Math.random() * (2**32 - 1)) - 2**31;
131136

132-
const currentScreenIds: {[key:string]: number} = {};
137+
const knownScreenInstanceIDs: {[key:string]: number} = {};
133138

134-
const nextScreenId = (params:AngularFireAnalyticsEventParams) => {
135-
const scope = params.outlet;
136-
if (currentScreenIds.hasOwnProperty(scope)) {
137-
return ++currentScreenIds[scope];
139+
const getScreenInstanceID = (params:{[key:string]: any}) => {
140+
// unique the screen class against the outlet name
141+
const screenInstanceKey = [
142+
params[SCREEN_CLASS_KEY],
143+
params[OUTLET_KEY]
144+
].join(SCREEN_INSTANCE_DELIMITER);
145+
if (knownScreenInstanceIDs.hasOwnProperty(screenInstanceKey)) {
146+
return knownScreenInstanceIDs[screenInstanceKey];
138147
} else {
139-
const ret = randomInt32();
140-
currentScreenIds[scope] = ret;
148+
const ret = nextScreenInstanceID++;
149+
knownScreenInstanceIDs[screenInstanceKey] = ret;
141150
return ret;
142151
}
143152
}

src/analytics/analytics.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,24 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
22
import { of } from 'rxjs';
33
import { map, tap, shareReplay, switchMap } from 'rxjs/operators';
4-
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
4+
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, ɵlazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
55
import { analytics, app } from 'firebase';
66

77
export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken<boolean>('angularfire2.analytics.analyticsCollectionEnabled');
88

9+
export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
10+
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
11+
export const DEBUG_MODE = new InjectionToken<boolean>('angularfire2.analytics.debugMode');
12+
13+
const APP_NAME_KEY = 'app_name';
14+
const APP_VERSION_KEY = 'app_version';
15+
const DEBUG_MODE_KEY = 'debug_mode';
16+
const ANALYTICS_ID_FIELD = 'measurementId';
17+
const GTAG_CONFIG_COMMAND = 'config';
18+
19+
// TODO can we get this from js sdk?
20+
const GTAG_FUNCTION = 'gtag';
21+
922
// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<analytics.Analytics>`
1023
type AnalyticsProxy = {
1124
// TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType<T[K]> is infering
@@ -20,15 +33,18 @@ type AnalyticsProxy = {
2033

2134
export interface AngularFireAnalytics extends AnalyticsProxy {};
2235

23-
@Injectable({
24-
providedIn: "root"
25-
})
36+
@Injectable()
2637
export class AngularFireAnalytics {
2738

39+
public updateConfig: (options: {[key:string]: any}) => void;
40+
2841
constructor(
2942
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
3043
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined,
3144
@Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null,
45+
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
46+
@Optional() @Inject(APP_NAME) providedAppName:string|null,
47+
@Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null,
3248
zone: NgZone
3349
) {
3450
const analytics = of(undefined).pipe(
@@ -39,12 +55,19 @@ export class AngularFireAnalytics {
3955
map(app => <analytics.Analytics>app.analytics()),
4056
tap(analytics => {
4157
if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) }
58+
if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) }
59+
if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) }
60+
if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) }
4261
}),
4362
runOutsideAngular(zone),
4463
shareReplay(1)
4564
);
4665

47-
return _lazySDKProxy(this, analytics, zone);
66+
this.updateConfig = (config: {[key:string]: any}) => analytics.toPromise().then(() =>
67+
window[GTAG_FUNCTION](GTAG_CONFIG_COMMAND, options[ANALYTICS_ID_FIELD], { ...config, update: true })
68+
);
69+
70+
return ɵlazySDKProxy(this, analytics, zone);
4871
}
4972

5073
}

src/core/angularfire2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export const runInZone = (zone: NgZone) => <T>(obs$: Observable<T>): Observable<
8080
{ [K in PromiseReturningFunctionPropertyNames<T> ]: (...args: Parameters<T[K]>) => ReturnType<T[K]> };
8181
*/
8282

83-
export const _lazySDKProxy = (klass: any, observable: Observable<any>, zone: NgZone) => new Proxy(klass, {
83+
export const ɵlazySDKProxy = (klass: any, observable: Observable<any>, zone: NgZone) => new Proxy(klass, {
8484
get: (_, name) => zone.runOutsideAngular(() =>
8585
klass[name] || new Proxy(() =>
8686
observable.toPromise().then(mod => {

src/remote-config/remote-config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
22
import { Observable, concat, of, empty } from 'rxjs';
33
import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators';
4-
import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
4+
import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
55
import { remoteConfig } from 'firebase/app';
66

77
export interface DefaultConfig {[key:string]: string|number|boolean};
@@ -100,7 +100,7 @@ export class AngularFireRemoteConfig {
100100
}, new Array<KeyedValue>(keys.length));
101101
}
102102

103-
const proxy: AngularFireRemoteConfig = _lazySDKProxy(this, remoteConfig, zone);
103+
const proxy: AngularFireRemoteConfig = ɵlazySDKProxy(this, remoteConfig, zone);
104104

105105
this.default$ = of(defaultToStartWith);
106106

0 commit comments

Comments
 (0)