Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
e5be927
Firebase v7, analytics and remote-config
jamesdaniels Oct 2, 2019
1cf961b
Cleaning up the DI tokens
jamesdaniels Oct 2, 2019
f5f67ad
Cleaning things up
jamesdaniels Oct 4, 2019
73413e8
DI and jazz
jamesdaniels Oct 4, 2019
dd0efb1
Bumping the tests
jamesdaniels Nov 8, 2019
dffca53
Adding to the root-spec
jamesdaniels Nov 9, 2019
9731e3c
Spelling is good.
jamesdaniels Nov 9, 2019
7308f9c
Merge branch 'master' into firebase-v7
jamesdaniels Nov 9, 2019
dcdf8bc
Have to include UMDs in the karma.conf
jamesdaniels Nov 9, 2019
6f71d46
Just importing performance is destablizing the app
jamesdaniels Nov 11, 2019
6638b9d
Merge branch 'master' into firebase-v7
jamesdaniels Nov 12, 2019
d7d52c8
Adding the zone arg to the app factory
jamesdaniels Nov 12, 2019
eb4bc00
First pass on the new RC API and the start of the AngularFireLazy effort
jamesdaniels Nov 13, 2019
89344cc
Update src/remote-config/tsconfig-test.json
jamesdaniels Nov 14, 2019
075afe6
Reworking things a bit
jamesdaniels Nov 20, 2019
b8b351a
Router as optional, drop this/private from screen tracking
jamesdaniels Nov 20, 2019
1e39052
Minor changes to RC
jamesdaniels Nov 20, 2019
768c21b
It's firebase_screen_class
jamesdaniels Nov 21, 2019
60c0cad
Reworking analytics
jamesdaniels Nov 22, 2019
9b2e920
current!
jamesdaniels Nov 22, 2019
916e069
Use _loadedConfig if available and scope screen_id on outlet
jamesdaniels Nov 22, 2019
5955925
Fixing the types to handle older Firebase SDKs
jamesdaniels Nov 22, 2019
659165e
SEMVER notes on the DI tokens
jamesdaniels Nov 22, 2019
62d90b9
Starting on the docs
jamesdaniels Nov 22, 2019
1a43ad1
Merge branch 'firebase-v7' of github.com:angular/angularfire2 into fi…
jamesdaniels Nov 22, 2019
67e1b55
Monitoring...
jamesdaniels Dec 9, 2019
91778ff
More work on analytics
jamesdaniels Dec 11, 2019
460170c
more expirimentation
jamesdaniels Dec 12, 2019
3c1ad1f
Flushing out RC more and fixing SSR issues
jamesdaniels Dec 14, 2019
e2d83c8
New RC API
jamesdaniels Dec 16, 2019
4601932
Mapping to objects and templates, budget pipe
jamesdaniels Dec 17, 2019
7fe92ed
More strict types
jamesdaniels Dec 17, 2019
d47dc3f
Fix proxy in Node, get component name for analytics in both JIT and AOT
jamesdaniels Dec 21, 2019
05c840b
Cleaning things up, beyond docs ready for release
jamesdaniels Jan 7, 2020
b46b382
Docs and cleanup
jamesdaniels Jan 7, 2020
ff65db8
Fixing analytics spec
jamesdaniels Jan 7, 2020
baaeccf
Adding more API to the docs
jamesdaniels Jan 7, 2020
04a3bb1
Further simplifications to the DI tokens
jamesdaniels Jan 7, 2020
c37fcb7
Add Analytics and RemoteConfig to install-and-setup
jamesdaniels Jan 7, 2020
6753a67
Merge branch 'master' into firebase-v7
jamesdaniels Jan 7, 2020
6f114c6
Our RC Value implements a Partial, so minors dont break
jamesdaniels Jan 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
More work on analytics
  • Loading branch information
jamesdaniels committed Dec 11, 2019
commit 91778ff414a82cc0a527b52fdfe90c0fba75c87b
5 changes: 3 additions & 2 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { NgModule, Optional } from '@angular/core';
import { UserTrackingService, ScreenTrackingService } from './analytics.service';
import { AngularFireAnalytics } from './analytics';

@NgModule()
@NgModule({
providers: [ AngularFireAnalytics ]
})
export class AngularFireAnalyticsModule {
constructor(
analytics: AngularFireAnalytics,
@Optional() screenTracking: ScreenTrackingService,
@Optional() userTracking: UserTrackingService
) { }
Expand Down
129 changes: 69 additions & 60 deletions src/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -1,95 +1,102 @@
import { Injectable, Inject, Optional, NgZone, OnDestroy, InjectionToken } from '@angular/core';
import { Injectable, Optional, NgZone, OnDestroy } from '@angular/core';
import { Subscription, from, Observable, empty, of } from 'rxjs';
import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators';
import { Router, NavigationEnd, ActivationEnd } from '@angular/router';
import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire';
import { runOutsideAngular } from '@angular/fire';
import { AngularFireAnalytics } from './analytics';
import { User } from 'firebase/app';
import { Title } from '@angular/platform-browser';

export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
// Gold seems to take page_title and screen_path but the v2 protocol doesn't seem
// to allow any class name, obviously v2 was designed for the basic web. I'm still
// sending firebase_screen_class (largely for BQ compatability) but the Firebase Console
// doesn't appear to be consuming the event properties.
// FWIW I'm seeing notes that firebase_* is depreciated in favor of ga_* in GMS... so IDK
const SCREEN_NAME_KEY = 'screen_name';
const PAGE_PATH_KEY = 'page_path';
const EVENT_ORIGIN_KEY = 'event_origin';
const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen';
const SCREEN_CLASS_KEY = 'firebase_screen_class';
const OUTLET_KEY = 'outlet';
const PAGE_TITLE_KEY = 'page_title';
const PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class';
const PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id';
const PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen';
const SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id';

const DEFAULT_APP_VERSION = '?';
const DEFAULT_APP_NAME = 'Angular App';
const SCREEN_VIEW_EVENT = 'screen_view';
const EVENT_ORIGIN_AUTO = 'auto';
const DEFAULT_SCREEN_CLASS = '???';
const NG_PRIMARY_OUTLET = 'primary';
const SCREEN_INSTANCE_DELIMITER = '#';

type AngularFireAnalyticsEventParams = {
app_name: string;
firebase_screen_class: string | undefined;
firebase_screen: string;
app_version: string;
screen_name: string;
outlet: string;
url: string;
};

@Injectable({
providedIn: 'root'
})
@Injectable()
export class ScreenTrackingService implements OnDestroy {

private disposable: Subscription|undefined;

constructor(
analytics: AngularFireAnalytics,
@Optional() router:Router,
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
@Optional() @Inject(APP_NAME) providedAppName:string|null,
@Optional() title:Title,
zone: NgZone
) {
if (!router) { return this }
const app_name = providedAppName || DEFAULT_APP_NAME;
const app_version = providedAppVersion || DEFAULT_APP_VERSION;
const activationEndEvents = router.events.pipe(filter<ActivationEnd>(e => e instanceof ActivationEnd));
const navigationEndEvents = router.events.pipe(filter<NavigationEnd>(e => e instanceof NavigationEnd));
this.disposable = navigationEndEvents.pipe(
withLatestFrom(activationEndEvents),
switchMap(([navigationEnd, activationEnd]) => {
const url = navigationEnd.url;
const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url;
const params: AngularFireAnalyticsEventParams = {
app_name, app_version, screen_name, url,
firebase_screen_class: undefined,
firebase_screen: screen_name,
outlet: activationEnd.snapshot.outlet
// SEMVER: start using optional chains and nullish coalescing once we support newer typescript
const page_path = navigationEnd.url;
const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || page_path;
const params = {
[SCREEN_NAME_KEY]: screen_name,
[PAGE_PATH_KEY]: page_path,
[EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO,
[FIREBASE_SCREEN_NAME_KEY]: screen_name,
[OUTLET_KEY]: activationEnd.snapshot.outlet
};
if (title) { params[PAGE_TITLE_KEY] = title.getTitle() }
const component = activationEnd.snapshot.component;
const routeConfig = activationEnd.snapshot.routeConfig;
// TODO maybe not lean on _loadedConfig...
const loadedConfig = routeConfig && (routeConfig as any)._loadedConfig;
const loadChildren = routeConfig && routeConfig.loadChildren;
if (component) {
return of({...params, firebase_screen_class: nameOrToString(component) });
return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(component) });
} else if (loadedConfig && loadedConfig.module && loadedConfig.module._moduleType) {
return of({...params, firebase_screen_class: nameOrToString(loadedConfig.module._moduleType)});
return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(loadedConfig.module._moduleType)});
} else if (typeof loadChildren === "string") {
// TODO is this an older lazy loading style parse
return of({...params, firebase_screen_class: loadChildren });
// TODO is the an older lazy loading style? parse, if so
return of({...params, [SCREEN_CLASS_KEY]: loadChildren });
} else if (loadChildren) {
// TODO look into the other return types here
return from(loadChildren() as Promise<any>).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) })));
return from(loadChildren() as Promise<any>).pipe(map(child => ({...params, [SCREEN_CLASS_KEY]: nameOrToString(child) })));
} else {
// TODO figure out what forms of router events I might be missing
return of(params);
return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS});
}
}),
tap(params => {
// TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
if (params.outlet == "primary") {
// TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id?
// also shouldn't these be computed in the setCurrentScreen function? prior too?
// do we want to be logging screen name or class?
analytics.setCurrentScreen(params.screen_name, { global: true })
if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) {
// TODO do we want to track the firebase_ attributes?
analytics.setCurrentScreen(params.screen_name);
analytics.updateConfig({ [PAGE_PATH_KEY]: params[PAGE_PATH_KEY] });
if (title) { analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) }
}
}),
map(params => ({ firebase_screen_id: nextScreenId(params), ...params})),
groupBy(params => params.outlet),
map(params => ({ [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), ...params })),
groupBy(params => params[OUTLET_KEY]),
mergeMap(group => group.pipe(startWith(undefined), pairwise())),
map(([prior, current]) => prior ? {
firebase_previous_class: prior.firebase_screen_class,
firebase_previous_screen: prior.firebase_screen,
firebase_previous_id: prior.firebase_screen_id,
[PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY],
[PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY],
[PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[SCREEN_INSTANCE_ID_KEY],
...current!
} : current!),
tap(params => analytics.logEvent('screen_view', params)),
tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params)),
runOutsideAngular(zone)
).subscribe();
}
Expand All @@ -100,9 +107,7 @@ export class ScreenTrackingService implements OnDestroy {

}

@Injectable({
providedIn: 'root'
})
@Injectable()
export class UserTrackingService implements OnDestroy {

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

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

const currentScreenIds: {[key:string]: number} = {};
const knownScreenInstanceIDs: {[key:string]: number} = {};

const nextScreenId = (params:AngularFireAnalyticsEventParams) => {
const scope = params.outlet;
if (currentScreenIds.hasOwnProperty(scope)) {
return ++currentScreenIds[scope];
const getScreenInstanceID = (params:{[key:string]: any}) => {
// unique the screen class against the outlet name
const screenInstanceKey = [
params[SCREEN_CLASS_KEY],
params[OUTLET_KEY]
].join(SCREEN_INSTANCE_DELIMITER);
if (knownScreenInstanceIDs.hasOwnProperty(screenInstanceKey)) {
return knownScreenInstanceIDs[screenInstanceKey];
} else {
const ret = randomInt32();
currentScreenIds[scope] = ret;
const ret = nextScreenInstanceID++;
knownScreenInstanceIDs[screenInstanceKey] = ret;
return ret;
}
}
Expand Down
33 changes: 28 additions & 5 deletions src/analytics/analytics.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
import { of } from 'rxjs';
import { map, tap, shareReplay, switchMap } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, ɵlazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire';
import { analytics, app } from 'firebase';

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

export const APP_VERSION = new InjectionToken<string>('angularfire2.analytics.appVersion');
export const APP_NAME = new InjectionToken<string>('angularfire2.analytics.appName');
export const DEBUG_MODE = new InjectionToken<boolean>('angularfire2.analytics.debugMode');

const APP_NAME_KEY = 'app_name';
const APP_VERSION_KEY = 'app_version';
const DEBUG_MODE_KEY = 'debug_mode';
const ANALYTICS_ID_FIELD = 'measurementId';
const GTAG_CONFIG_COMMAND = 'config';

// TODO can we get this from js sdk?
const GTAG_FUNCTION = 'gtag';

// SEMVER: once we move to Typescript 3.6 use `PromiseProxy<analytics.Analytics>`
type AnalyticsProxy = {
// TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType<T[K]> is infering
Expand All @@ -20,15 +33,18 @@ type AnalyticsProxy = {

export interface AngularFireAnalytics extends AnalyticsProxy {};

@Injectable({
providedIn: "root"
})
@Injectable()
export class AngularFireAnalytics {

public updateConfig: (options: {[key:string]: any}) => void;

constructor(
@Inject(FIREBASE_OPTIONS) options:FirebaseOptions,
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined,
@Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null,
@Optional() @Inject(APP_VERSION) providedAppVersion:string|null,
@Optional() @Inject(APP_NAME) providedAppName:string|null,
@Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null,
zone: NgZone
) {
const analytics = of(undefined).pipe(
Expand All @@ -39,12 +55,19 @@ export class AngularFireAnalytics {
map(app => <analytics.Analytics>app.analytics()),
tap(analytics => {
if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) }
if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) }
if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) }
if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) }
}),
runOutsideAngular(zone),
shareReplay(1)
);

return _lazySDKProxy(this, analytics, zone);
this.updateConfig = (config: {[key:string]: any}) => analytics.toPromise().then(() =>
window[GTAG_FUNCTION](GTAG_CONFIG_COMMAND, options[ANALYTICS_ID_FIELD], { ...config, update: true })
);

return ɵlazySDKProxy(this, analytics, zone);
}

}
2 changes: 1 addition & 1 deletion src/core/angularfire2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export const runInZone = (zone: NgZone) => <T>(obs$: Observable<T>): Observable<
{ [K in PromiseReturningFunctionPropertyNames<T> ]: (...args: Parameters<T[K]>) => ReturnType<T[K]> };
*/

export const _lazySDKProxy = (klass: any, observable: Observable<any>, zone: NgZone) => new Proxy(klass, {
export const ɵlazySDKProxy = (klass: any, observable: Observable<any>, zone: NgZone) => new Proxy(klass, {
get: (_, name) => zone.runOutsideAngular(() =>
klass[name] || new Proxy(() =>
observable.toPromise().then(mod => {
Expand Down
4 changes: 2 additions & 2 deletions src/remote-config/remote-config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core';
import { Observable, concat, of, empty } from 'rxjs';
import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators';
import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire';
import { remoteConfig } from 'firebase/app';

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

const proxy: AngularFireRemoteConfig = _lazySDKProxy(this, remoteConfig, zone);
const proxy: AngularFireRemoteConfig = ɵlazySDKProxy(this, remoteConfig, zone);

this.default$ = of(defaultToStartWith);

Expand Down