Skip to content

Commit 075afe6

Browse files
committed
Reworking things a bit
1 parent 89344cc commit 075afe6

File tree

7 files changed

+188
-109
lines changed

7 files changed

+188
-109
lines changed

src/analytics/analytics.module.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { NgModule } from '@angular/core';
1+
import { NgModule, Optional } from '@angular/core';
2+
import { UserTrackingService, ScreenTrackingService } from './analytics.service';
23
import { AngularFireAnalytics } from './analytics';
34

4-
@NgModule({
5-
providers: [ AngularFireAnalytics ]
6-
})
5+
@NgModule()
76
export class AngularFireAnalyticsModule {
8-
constructor(_: AngularFireAnalytics) {
9-
// DI inject Analytics here for the automatic data collection
10-
}
7+
constructor(
8+
analytics: AngularFireAnalytics,
9+
@Optional() screenTracking: ScreenTrackingService,
10+
@Optional() userTracking: UserTrackingService
11+
) { }
1112
}

src/analytics/analytics.service.ts

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
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';
4+
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
5+
import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire';
6+
import { AngularFireAnalytics } from './analytics';
7+
import { User } from 'firebase/app';
8+
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');
11+
export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
12+
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
13+
14+
const DEFAULT_APP_VERSION = '?';
15+
const DEFAULT_APP_NAME = 'Angular App';
16+
17+
@Injectable({
18+
providedIn: 'root'
19+
})
20+
export class ScreenTrackingService implements OnDestroy {
21+
22+
private disposable: Subscription|undefined;
23+
24+
constructor(
25+
private analytics: AngularFireAnalytics,
26+
private router:Router,
27+
@Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) private automaticallySetCurrentScreen:boolean|null,
28+
@Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) private automaticallyLogScreenViews:boolean|null,
29+
@Optional() @Inject(APP_VERSION) private providedAppVersion:string|null,
30+
@Optional() @Inject(APP_NAME) private providedAppName:string|null,
31+
private zone: NgZone
32+
) {
33+
if (this.automaticallySetCurrentScreen !== false || this.automaticallyLogScreenViews !== false) {
34+
const app_name = this.providedAppName || DEFAULT_APP_NAME;
35+
const app_version = this.providedAppVersion || DEFAULT_APP_VERSION;
36+
const activationEndEvents = this.router.events.pipe(filter<ActivationEnd>(e => e instanceof ActivationEnd));
37+
const navigationEndEvents = this.router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd));
38+
this.disposable = navigationEndEvents.pipe(
39+
withLatestFrom(activationEndEvents),
40+
switchMap(([navigationEnd, activationEnd]) => {
41+
const url = navigationEnd.url;
42+
const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url;
43+
const outlet = activationEnd.snapshot.outlet;
44+
const component = activationEnd.snapshot.component;
45+
const ret = new Array<Promise<void>>();
46+
if (this.automaticallyLogScreenViews !== false) {
47+
if (component) {
48+
const screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString();
49+
ret.push(this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }));
50+
} else if (activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.loadChildren) {
51+
ret.push((activationEnd.snapshot.routeConfig.loadChildren as any)().then((child:any) => {
52+
const screen_class = child.name;
53+
console.log("logEvent", "screen_view", { app_name, screen_class, app_version, screen_name, outlet, url });
54+
return this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url });
55+
}));
56+
} else {
57+
ret.push(this.analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url }));
58+
}
59+
}
60+
if (this.automaticallySetCurrentScreen !== false) {
61+
ret.push(this.analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }));
62+
}
63+
return Promise.all(ret);
64+
}),
65+
runOutsideAngular(this.zone)
66+
).subscribe();
67+
}
68+
}
69+
70+
ngOnDestroy() {
71+
if (this.disposable) { this.disposable.unsubscribe(); }
72+
}
73+
74+
}
75+
76+
@Injectable({
77+
providedIn: 'root'
78+
})
79+
export class UserTrackingService implements OnDestroy {
80+
81+
private disposable: Subscription|undefined;
82+
83+
// TODO a user properties injector
84+
constructor(
85+
analytics: AngularFireAnalytics,
86+
zone: NgZone
87+
) {
88+
this.disposable = from(analytics.app).pipe(
89+
// TODO can I hook into auth being loaded...
90+
map(app => app.auth()),
91+
switchMap(auth => auth ? new Observable<User>(auth.onAuthStateChanged.bind(auth)) : empty()),
92+
switchMap(user => analytics.setUserId(user ? user.uid : null!, { global: true })),
93+
runOutsideAngular(zone)
94+
).subscribe();
95+
}
96+
97+
ngOnDestroy() {
98+
if (this.disposable) { this.disposable.unsubscribe(); }
99+
}
100+
}

src/analytics/analytics.ts

Lines changed: 12 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,10 @@
11
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
2-
import { Observable, from } from 'rxjs';
3-
import { map, tap, filter, withLatestFrom, shareReplay } from 'rxjs/operators';
4-
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
2+
import { of } from 'rxjs';
3+
import { map, tap, shareReplay, switchMap } from 'rxjs/operators';
54
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
65
import { analytics, app } from 'firebase';
76

8-
export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken<boolean>('angularfire2.analytics.setCurrentScreen');
9-
export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken<boolean>('angularfire2.analytics.logScreenViews');
107
export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken<boolean>('angularfire2.analytics.analyticsCollectionEnabled');
11-
export const AUTOMATICALLY_TRACK_USER_IDENTIFIER = new InjectionToken<boolean>('angularfire2.analytics.trackUserIdentifier');
12-
export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
13-
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
14-
15-
export const DEFAULT_APP_VERSION = '?';
16-
export const DEFAULT_APP_NAME = 'Angular App';
178

189
// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<analytics.Analytics>`
1910
type AnalyticsProxy = {
@@ -29,74 +20,30 @@ type AnalyticsProxy = {
2920

3021
export interface AngularFireAnalytics extends AnalyticsProxy {};
3122

32-
@Injectable()
23+
@Injectable({
24+
providedIn: "root"
25+
})
3326
export class AngularFireAnalytics {
3427

35-
/**
36-
* Firebase Analytics instance
37-
*/
38-
private readonly analytics$: Observable<FirebaseAnalytics>;
39-
private get analytics() { return this.analytics$.toPromise(); }
40-
4128
constructor(
4229
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
4330
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined,
44-
@Optional() router:Router,
45-
@Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null,
46-
@Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) automaticallyLogScreenViews:boolean|null,
4731
@Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null,
48-
@Optional() @Inject(AUTOMATICALLY_TRACK_USER_IDENTIFIER) automaticallyTrackUserIdentifier:boolean|null,
49-
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
50-
@Optional() @Inject(APP_NAME) providedAppName:string|null,
5132
zone: NgZone
5233
) {
53-
// @ts-ignore zapping in the UMD in the build script
54-
const requireAnalytics = from(import('firebase/analytics'));
55-
const app = _firebaseAppFactory(options, zone, nameOrConfig);
56-
57-
this.analytics$ = requireAnalytics.pipe(
58-
map(() => app.analytics()),
34+
const analytics = of(undefined).pipe(
35+
// @ts-ignore zapping in the UMD in the build script
36+
switchMap(() => zone.runOutsideAngular(() => import('firebase/analytics'))),
37+
map(() => _firebaseAppFactory(options, zone, nameOrConfig)),
38+
map(app => app.analytics()),
5939
tap(analytics => {
6040
if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) }
6141
}),
6242
runOutsideAngular(zone),
6343
shareReplay(1)
6444
);
6545

66-
if (router && (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false)) {
67-
const app_name = providedAppName || DEFAULT_APP_NAME;
68-
const app_version = providedAppVersion || DEFAULT_APP_VERSION;
69-
const activationEndEvents = router.events.pipe(filter<ActivationEnd>(e => e instanceof ActivationEnd));
70-
const navigationEndEvents = router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd));
71-
navigationEndEvents.pipe(
72-
withLatestFrom(activationEndEvents, this.analytics$),
73-
tap(([navigationEnd, activationEnd, analytics]) => {
74-
const url = navigationEnd.url;
75-
const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined;
76-
const outlet = activationEnd.snapshot.outlet;
77-
if (automaticallyLogScreenViews !== false) {
78-
analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url });
79-
}
80-
if (automaticallySetCurrentScreen !== false) {
81-
// TODO when is screen_name undefined?
82-
analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" });
83-
}
84-
}),
85-
runOutsideAngular(zone)
86-
).subscribe();
87-
}
88-
89-
// TODO do something other than just check auth presence, what if it's lazy loaded?
90-
if (app.auth && automaticallyTrackUserIdentifier !== false) {
91-
new Observable<firebase.User|null>(app.auth().onAuthStateChanged.bind(app.auth())).pipe(
92-
withLatestFrom(this.analytics$),
93-
tap(([user, analytics]) => analytics.setUserId(user ? user.uid : null!, { global: true })),
94-
runOutsideAngular(zone)
95-
).subscribe()
96-
}
97-
98-
return _lazySDKProxy(this, this.analytics, zone);
99-
46+
return _lazySDKProxy(this, analytics, zone);
10047
}
10148

102-
}
49+
}

src/analytics/public_api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from './analytics';
22
export * from './analytics.module';
3+
export * from './analytics.service';

src/core/angularfire2.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,10 +79,10 @@ export const runInZone = (zone: NgZone) => <T>(obs$: Observable<T>): Observable<
7979
{ [K in PromiseReturningFunctionPropertyNames<T> ]: (...args: Parameters<T[K]>) => ReturnType<T[K]> };
8080
*/
8181

82-
export const _lazySDKProxy = (klass: any, promise: Promise<any>, zone: NgZone) => new Proxy(klass, {
82+
export const _lazySDKProxy = (klass: any, observable: Observable<any>, zone: NgZone) => new Proxy(klass, {
8383
get: (_, name) => zone.runOutsideAngular(() =>
8484
klass[name] || new Proxy(() =>
85-
promise.then(mod => {
85+
observable.toPromise().then(mod => {
8686
const ret = mod[name];
8787
// TODO move to proper type guards
8888
if (typeof ret == 'function') {
Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
import { NgModule } from '@angular/core';
2-
import { AngularFireRemoteConfig } from './remote-config';
32

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

0 commit comments

Comments
 (0)