diff --git a/CHANGELOG.md b/CHANGELOG.md
index 103fda323..53e219c1b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,10 @@
+
+# [6.1.0-rc.3](https://github.com/angular/angularfire/compare/6.1.0-rc.2...6.1.0-rc.3) (2020-11-14)
+
+### Bug Fixes
+
+* **analytics:** Bunch of analytics & screen tracking improvements ([#2654](https://github.com/angular/angularfire/pull/2654)) ([5bc159a](https://github.com/angular/angularfire/commit/5bc159a))
+
# [6.1.0-rc.2](https://github.com/angular/angularfire/compare/6.1.0-rc.1...6.1.0-rc.2) (2020-11-13)
diff --git a/sample/src/app/app-routing.module.ts b/sample/src/app/app-routing.module.ts
index 7f0c21fba..e3eeded12 100644
--- a/sample/src/app/app-routing.module.ts
+++ b/sample/src/app/app-routing.module.ts
@@ -1,10 +1,24 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
-
+import { ProtectedComponent } from './protected/protected.component';
+import { AngularFireAuthGuard } from '@angular/fire/auth-guard';
+import { SecondaryComponent } from './secondary/secondary.component';
const routes: Routes = [
- { path: '', component: HomeComponent, pathMatch: 'full' }
+ { path: '', component: HomeComponent, outlet: 'primary' },
+ { path: '', component: SecondaryComponent, outlet: 'secondary' },
+ { path: '', component: SecondaryComponent, outlet: 'tertiary' },
+ { path: 'protected', component: ProtectedComponent, canActivate: [AngularFireAuthGuard] },
+ { path: 'protected-lazy',
+ loadChildren: () => import('./protected-lazy/protected-lazy.module').then(m => m.ProtectedLazyModule),
+ canActivate: [AngularFireAuthGuard] },
+ { path: 'protected', component: ProtectedComponent, canActivate: [AngularFireAuthGuard], outlet: 'secondary' },
+ { path: 'protected', component: ProtectedComponent, canActivate: [AngularFireAuthGuard], outlet: 'tertiary' },
+ { path: 'protected-lazy',
+ loadChildren: () => import('./protected-lazy/protected-lazy.module').then(m => m.ProtectedLazyModule),
+ canActivate: [AngularFireAuthGuard],
+ outlet: 'secondary' },
];
@NgModule({
diff --git a/sample/src/app/app.component.ts b/sample/src/app/app.component.ts
index e0cb7c544..dbe6cbc6c 100644
--- a/sample/src/app/app.component.ts
+++ b/sample/src/app/app.component.ts
@@ -4,7 +4,12 @@ import { FirebaseApp } from '@angular/fire';
@Component({
selector: 'app-root',
template: `
+ Home | Protected | Protected Lazy | Protected Lazy Deep | Protected Lazy Deep
+ Home | Protected | Protected Lazy
+
+ Home | Protected
+
`,
styles: [``]
})
diff --git a/sample/src/app/app.module.ts b/sample/src/app/app.module.ts
index 598e3ee1a..7dd0c4eb3 100644
--- a/sample/src/app/app.module.ts
+++ b/sample/src/app/app.module.ts
@@ -10,9 +10,12 @@ import { AngularFireModule } from '@angular/fire';
import {
AngularFireAnalyticsModule,
+ APP_NAME,
+ APP_VERSION,
DEBUG_MODE as ANALYTICS_DEBUG_MODE,
ScreenTrackingService,
- UserTrackingService
+ UserTrackingService,
+ COLLECTION_ENABLED
} from '@angular/fire/analytics';
import { FirestoreComponent } from './firestore/firestore.component';
@@ -55,24 +58,20 @@ import { FunctionsComponent } from './functions/functions.component';
AngularFireDatabaseModule,
AngularFirestoreModule.enablePersistence({ synchronizeTabs: true }),
AngularFireAuthModule,
+ AngularFireAuthGuardModule,
AngularFireRemoteConfigModule,
AngularFireMessagingModule,
- // AngularFireAnalyticsModule, // TODO having trouble getting this to work in IE
+ AngularFireAnalyticsModule,
AngularFireFunctionsModule,
- // AngularFirePerformanceModule, // TODO having trouble getting this to work in IE
+ AngularFirePerformanceModule,
AngularFireAuthGuardModule
],
providers: [
- /*
- TODO Analytics and Performance monitoring aren't working in IE, sort this out
- UserTrackingService,
- ScreenTrackingService,
- PerformanceMonitoringService,
- {
- provide: ANALYTICS_DEBUG_MODE,
- useFactory: () => isDevMode()
- },
- */
+ UserTrackingService,
+ ScreenTrackingService,
+ PerformanceMonitoringService,
+ { provide: ANALYTICS_DEBUG_MODE, useValue: false },
+ { provide: COLLECTION_ENABLED, useValue: true },
{ provide: USE_AUTH_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9099] : undefined },
{ provide: USE_DATABASE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9000] : undefined },
{ provide: USE_FIRESTORE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 8080] : undefined },
@@ -84,6 +83,8 @@ import { FunctionsComponent } from './functions/functions.component';
{ provide: USE_DEVICE_LANGUAGE, useValue: true },
{ provide: VAPID_KEY, useValue: environment.vapidKey },
{ provide: SERVICE_WORKER, useFactory: () => navigator?.serviceWorker?.getRegistration() ?? undefined },
+ { provide: APP_VERSION, useValue: '0.0.0' },
+ { provide: APP_NAME, useValue: 'Angular' }
],
bootstrap: [AppComponent]
})
diff --git a/sample/src/app/protected-lazy/protected-lazy-routing.module.ts b/sample/src/app/protected-lazy/protected-lazy-routing.module.ts
new file mode 100644
index 000000000..3cdad047c
--- /dev/null
+++ b/sample/src/app/protected-lazy/protected-lazy-routing.module.ts
@@ -0,0 +1,16 @@
+import { NgModule } from '@angular/core';
+import { Routes, RouterModule } from '@angular/router';
+
+import { ProtectedLazyComponent } from './protected-lazy.component';
+
+const routes: Routes = [
+ { path: '', component: ProtectedLazyComponent },
+ { path: 'asdf', component: ProtectedLazyComponent },
+ { path: ':id/bob', component: ProtectedLazyComponent }
+];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+export class ProtectedLazyRoutingModule { }
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.css b/sample/src/app/protected-lazy/protected-lazy.component.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.html b/sample/src/app/protected-lazy/protected-lazy.component.html
new file mode 100644
index 000000000..ca6bdab0f
--- /dev/null
+++ b/sample/src/app/protected-lazy/protected-lazy.component.html
@@ -0,0 +1 @@
+
protected-lazy works!
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.spec.ts b/sample/src/app/protected-lazy/protected-lazy.component.spec.ts
new file mode 100644
index 000000000..9268ccded
--- /dev/null
+++ b/sample/src/app/protected-lazy/protected-lazy.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProtectedLazyComponent } from './protected-lazy.component';
+
+describe('ProtectedLazyComponent', () => {
+ let component: ProtectedLazyComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ProtectedLazyComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ProtectedLazyComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/sample/src/app/protected-lazy/protected-lazy.component.ts b/sample/src/app/protected-lazy/protected-lazy.component.ts
new file mode 100644
index 000000000..45525dafa
--- /dev/null
+++ b/sample/src/app/protected-lazy/protected-lazy.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-protected-lazy',
+ templateUrl: './protected-lazy.component.html',
+ styleUrls: ['./protected-lazy.component.css']
+})
+export class ProtectedLazyComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/sample/src/app/protected-lazy/protected-lazy.module.ts b/sample/src/app/protected-lazy/protected-lazy.module.ts
new file mode 100644
index 000000000..c0aa18188
--- /dev/null
+++ b/sample/src/app/protected-lazy/protected-lazy.module.ts
@@ -0,0 +1,15 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { ProtectedLazyRoutingModule } from './protected-lazy-routing.module';
+import { ProtectedLazyComponent } from './protected-lazy.component';
+
+
+@NgModule({
+ declarations: [ProtectedLazyComponent],
+ imports: [
+ CommonModule,
+ ProtectedLazyRoutingModule
+ ]
+})
+export class ProtectedLazyModule { }
diff --git a/sample/src/app/protected/protected.component.css b/sample/src/app/protected/protected.component.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/sample/src/app/protected/protected.component.html b/sample/src/app/protected/protected.component.html
new file mode 100644
index 000000000..842979e71
--- /dev/null
+++ b/sample/src/app/protected/protected.component.html
@@ -0,0 +1 @@
+protected works!
diff --git a/sample/src/app/protected/protected.component.spec.ts b/sample/src/app/protected/protected.component.spec.ts
new file mode 100644
index 000000000..81b13b842
--- /dev/null
+++ b/sample/src/app/protected/protected.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ProtectedComponent } from './protected.component';
+
+describe('ProtectedComponent', () => {
+ let component: ProtectedComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ ProtectedComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ProtectedComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/sample/src/app/protected/protected.component.ts b/sample/src/app/protected/protected.component.ts
new file mode 100644
index 000000000..3cfe145fe
--- /dev/null
+++ b/sample/src/app/protected/protected.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-protected',
+ templateUrl: './protected.component.html',
+ styleUrls: ['./protected.component.css']
+})
+export class ProtectedComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/sample/src/app/secondary/secondary.component.css b/sample/src/app/secondary/secondary.component.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/sample/src/app/secondary/secondary.component.html b/sample/src/app/secondary/secondary.component.html
new file mode 100644
index 000000000..fa9404328
--- /dev/null
+++ b/sample/src/app/secondary/secondary.component.html
@@ -0,0 +1 @@
+secondary works!
diff --git a/sample/src/app/secondary/secondary.component.spec.ts b/sample/src/app/secondary/secondary.component.spec.ts
new file mode 100644
index 000000000..41cf50be0
--- /dev/null
+++ b/sample/src/app/secondary/secondary.component.spec.ts
@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { SecondaryComponent } from './secondary.component';
+
+describe('SecondaryComponent', () => {
+ let component: SecondaryComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ SecondaryComponent ]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(SecondaryComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/sample/src/app/secondary/secondary.component.ts b/sample/src/app/secondary/secondary.component.ts
new file mode 100644
index 000000000..52c4b29d7
--- /dev/null
+++ b/sample/src/app/secondary/secondary.component.ts
@@ -0,0 +1,15 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+ selector: 'app-secondary',
+ templateUrl: './secondary.component.html',
+ styleUrls: ['./secondary.component.css']
+})
+export class SecondaryComponent implements OnInit {
+
+ constructor() { }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts
index 0ec58aaf7..9223d4d00 100644
--- a/src/analytics/analytics.service.ts
+++ b/src/analytics/analytics.service.ts
@@ -3,17 +3,16 @@ import {
Inject,
Injectable,
Injector,
- NgModuleFactory,
NgZone,
OnDestroy,
Optional,
PLATFORM_ID
} from '@angular/core';
import { from, Observable, of, Subscription } from 'rxjs';
-import { filter, groupBy, map, mergeMap, observeOn, pairwise, startWith, switchMap, tap, withLatestFrom } from 'rxjs/operators';
-import { ActivationEnd, NavigationEnd, Router, ROUTES } from '@angular/router';
+import { distinctUntilChanged, filter, groupBy, map, mergeMap, observeOn, pairwise, startWith, switchMap, tap } from 'rxjs/operators';
+import { ActivationEnd, Router, ɵEmptyOutletComponent } from '@angular/router';
import { ɵAngularFireSchedulers } from '@angular/fire';
-import { AngularFireAnalytics, DEBUG_MODE } from './analytics';
+import { AngularFireAnalytics } from './analytics';
import firebase from 'firebase/app';
import { Title } from '@angular/platform-browser';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@@ -30,16 +29,11 @@ const PAGE_PATH_KEY = 'page_path';
const PAGE_TITLE_KEY = 'page_title';
const SCREEN_CLASS_KEY = 'screen_class';
const SCREEN_NAME_KEY = 'screen_name';
-
const SCREEN_VIEW_EVENT = 'screen_view';
const EVENT_ORIGIN_AUTO = 'auto';
const DEFAULT_SCREEN_CLASS = '???';
-const NG_PRIMARY_OUTLET = 'primary';
const SCREEN_INSTANCE_DELIMITER = '#';
-const ANNOTATIONS = '__annotations__';
-
-
// this is an INT64 in iOS/Android but use INT32 cause javascript
let nextScreenInstanceID = Math.floor(Math.random() * (2 ** 32 - 1)) - 2 ** 31;
@@ -72,25 +66,30 @@ export class ScreenTrackingService implements OnDestroy {
componentFactoryResolver: ComponentFactoryResolver,
// tslint:disable-next-line:ban-types
@Inject(PLATFORM_ID) platformId: Object,
- @Optional() @Inject(DEBUG_MODE) debugModeEnabled: boolean | null,
zone: NgZone,
- injector: Injector
+ injector: Injector,
) {
if (!router || !isPlatformBrowser(platformId)) {
return this;
}
zone.runOutsideAngular(() => {
const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd));
- const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd));
- this.disposable = navigationEndEvents.pipe(
- withLatestFrom(activationEndEvents),
- switchMap(([navigationEnd, activationEnd]) => {
- // SEMVER: start using optional chains and nullish coalescing once we support newer typescript
- const pagePath = navigationEnd.url;
- const screenName = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || pagePath;
+ this.disposable = activationEndEvents.pipe(
+ switchMap(activationEnd => {
+ // router parseUrl is having trouble with outlets when they're empty
+ // e.g, /asdf/1(bob://sally:asdf), so put another slash in when empty
+ const urlTree = router.parseUrl(router.url.replace(/(?:\().+(?:\))/g, a => a.replace('://', ':///')));
+ const pagePath = urlTree.root.children[activationEnd.snapshot.outlet]?.toString() || '';
+ const actualSnapshot = router.routerState.root.children.map(it => it).find(it => it.outlet === activationEnd.snapshot.outlet);
+ let actualDeep = actualSnapshot;
+ while (actualDeep.firstChild) {
+ actualDeep = actualDeep.firstChild;
+ }
+ const screenName = actualDeep.pathFromRoot.map(s => s.routeConfig?.path).filter(it => it).join('/') || '/';
+
const params = {
[SCREEN_NAME_KEY]: screenName,
- [PAGE_PATH_KEY]: pagePath,
+ [PAGE_PATH_KEY]: `/${pagePath}`,
[FIREBASE_EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO,
[FIREBASE_SCREEN_NAME_KEY]: screenName,
[OUTLET_KEY]: activationEnd.snapshot.outlet
@@ -98,85 +97,52 @@ export class ScreenTrackingService implements OnDestroy {
if (title) {
params[PAGE_TITLE_KEY] = title.getTitle();
}
- const component = activationEnd.snapshot.component;
- const routeConfig = activationEnd.snapshot.routeConfig;
- const loadChildren = routeConfig && routeConfig.loadChildren;
- // TODO figure out how to handle minification
- if (typeof loadChildren === 'string') {
- // SEMVER: this is the older lazy load style "./path#ClassName", drop this when we drop old ng
- // TODO is it worth seeing if I can look up the component factory selector from the module name?
- // it's lazy so it's not registered with componentFactoryResolver yet... seems a pain for a depreciated style
- return of({ ...params, [SCREEN_CLASS_KEY]: loadChildren.split('#')[1] });
- } else if (typeof component === 'string') {
+
+ let component = actualSnapshot.component;
+ if (component) {
+ if (component === ɵEmptyOutletComponent) {
+ let deepSnapshot = activationEnd.snapshot;
+ // TODO when might there be mutple children, different outlets? explore
+ while (deepSnapshot.firstChild) {
+ deepSnapshot = deepSnapshot.firstChild;
+ }
+ component = deepSnapshot.component;
+ }
+ } else {
+ component = activationEnd.snapshot.component;
+ }
+
+ if (typeof component === 'string') {
return of({ ...params, [SCREEN_CLASS_KEY]: component });
} else if (component) {
const componentFactory = componentFactoryResolver.resolveComponentFactory(component);
return of({ ...params, [SCREEN_CLASS_KEY]: componentFactory.selector });
- } else if (loadChildren) {
- const loadedChildren = loadChildren();
- const loadedChildren$: Observable = (loadedChildren instanceof Observable) ?
- loadedChildren :
- from(Promise.resolve(loadedChildren));
- return loadedChildren$.pipe(
- map(lazyModule => {
- if (lazyModule instanceof NgModuleFactory) {
- // AOT create an injector
- const moduleRef = lazyModule.create(injector);
- // INVESTIGATE is this the right way to get at the matching route?
- const routes = moduleRef.injector.get(ROUTES);
- const component = routes[0][0].component; // should i just be grabbing 0-0 here?
- try {
- const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(component);
- return { ...params, [SCREEN_CLASS_KEY]: componentFactory.selector };
- } catch (_) {
- return { ...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS };
- }
- } else {
- // JIT look at the annotations
- // INVESTIGATE are there public APIs for this stuff?
- const declarations = [].concat.apply([], (lazyModule[ANNOTATIONS] || []).map((f: any) => f.declarations));
- const selectors = [].concat.apply([], declarations.map((c: any) => (c[ANNOTATIONS] || []).map((f: any) => f.selector)));
- // should I just be grabbing the selector like this or should i match against the route component?
- // const routerModule = lazyModule.ngInjectorDef.imports.find(i => i.ngModule && ....);
- // const route = routerModule.providers[0].find(p => p.provide == ROUTES).useValue[0];
- return { ...params, [SCREEN_CLASS_KEY]: selectors[0] || DEFAULT_SCREEN_CLASS };
- }
- })
- );
} else {
- return of({ ...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS });
+ // lazy loads cause extra activations, ignore
+ return of(null);
}
}),
+ filter(it => it),
map(params => ({
[FIREBASE_SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY],
[FIREBASE_SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params),
...params
})),
- tap(params => {
- // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet?
- if (params[OUTLET_KEY] === NG_PRIMARY_OUTLET) {
- analytics.setCurrentScreen(params[SCREEN_NAME_KEY]);
- analytics.updateConfig({
- [PAGE_PATH_KEY]: params[PAGE_PATH_KEY],
- [SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY]
- });
- if (title) {
- analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] });
- }
- }
- }),
- groupBy(params => params[OUTLET_KEY]),
- // tslint:disable-next-line
- mergeMap(group => group.pipe(startWith(undefined), pairwise())),
- map(([prior, current]) => prior ? {
- [FIREBASE_PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY],
- [FIREBASE_PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY],
- [FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[FIREBASE_SCREEN_INSTANCE_ID_KEY],
- ...current
- } : current),
- // tslint:disable-next-line:no-console
- tap(params => debugModeEnabled && console.info(SCREEN_VIEW_EVENT, params)),
- tap(params => zone.runOutsideAngular(() => analytics.logEvent(SCREEN_VIEW_EVENT, params)))
+ groupBy(it => it[OUTLET_KEY]),
+ mergeMap(it => it.pipe(
+ distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
+ startWith(undefined),
+ pairwise(),
+ map(([prior, current]) =>
+ prior ? {
+ [FIREBASE_PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY],
+ [FIREBASE_PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY],
+ [FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[FIREBASE_SCREEN_INSTANCE_ID_KEY],
+ ...current
+ } : current
+ ),
+ tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params))
+ ))
).subscribe();
});
}
diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts
index eb8764ef7..0116c610a 100644
--- a/src/analytics/analytics.ts
+++ b/src/analytics/analytics.ts
@@ -1,5 +1,5 @@
import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core';
-import { EMPTY, Observable, of } from 'rxjs';
+import { EMPTY, of } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
import { map, tap, shareReplay, switchMap, observeOn } from 'rxjs/operators';
import {
@@ -29,26 +29,28 @@ export const CONFIG = new InjectionToken('angularfire2.analytics.config'
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';
-const GTAG_FUNCTION_NAME = 'gtag';
+const GTAG_FUNCTION_NAME = 'gtag'; // TODO rename these
const DATA_LAYER_NAME = 'dataLayer';
export interface AngularFireAnalytics extends ɵPromiseProxy {
}
let gtag: (...args: any[]) => void;
-let analyticsInitialized: Promise;
-const analyticsInstanceCache: { [key: string]: Observable } = {};
+
+// tslint:disable-next-line
+var __analyticsInitialized: Promise;
@Injectable({
providedIn: 'any'
})
export class AngularFireAnalytics {
+ private measurementId: string;
+
async updateConfig(config: Config) {
- await analyticsInitialized;
- gtag(GTAG_CONFIG_COMMAND, this.options[ANALYTICS_ID_FIELD], { ...config, update: true });
+ await __analyticsInitialized;
+ window[GTAG_FUNCTION_NAME](GTAG_CONFIG_COMMAND, this.measurementId, { ...config, update: true });
}
constructor(
@@ -64,62 +66,68 @@ export class AngularFireAnalytics {
zone: NgZone
) {
- if (!analyticsInitialized) {
+ if (!__analyticsInitialized) {
if (isPlatformBrowser(platformId)) {
window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || [];
- /**
- * According to the gtag documentation, this function that defines a custom data layer cannot be
- * an arrow function because 'arguments' is not an array. It is actually an object that behaves
- * like an array and contains more information then just indexes. Transforming this into arrow function
- * caused issue #2505 where analytics no longer sent any data.
- */
- // tslint:disable-next-line: only-arrow-functions
- gtag = (window[GTAG_FUNCTION_NAME] as any) || (function(..._args: any[]) {
- (window[DATA_LAYER_NAME] as any).push(arguments);
- });
- analyticsInitialized = zone.runOutsideAngular(() =>
- new Promise(resolve => {
- window[GTAG_FUNCTION_NAME] = (...args: any[]) => {
- if (args[0] === 'js') {
- resolve();
+
+ __analyticsInitialized = new Promise(resolve => {
+ window[GTAG_FUNCTION_NAME] = (...args: any[]) => {
+ // wait to initialize until we know the measurementId as the one in config is unstable
+ if (args[0] === 'config' && args[2].origin === 'firebase') {
+ this.measurementId = args[1];
+ resolve();
+ }
+ if (args[0] === 'event') {
+ if (providedAppName) {
+ args[2][APP_NAME_KEY] = providedAppName;
+ }
+ if (providedAppVersion) {
+ args[2][APP_VERSION_KEY] = providedAppVersion;
}
- gtag(...args);
- };
- })
- );
+ }
+ if (debugModeEnabled && typeof console !== 'undefined') {
+ // tslint:disable-next-line:no-console
+ console.info(...args);
+ }
+ /**
+ * According to the gtag documentation, this function that defines a custom data layer cannot be
+ * an arrow function because 'arguments' is not an array. It is actually an object that behaves
+ * like an array and contains more information then just indexes. Transforming this into arrow function
+ * caused issue #2505 where analytics no longer sent any data.
+ */
+ // tslint:disable-next-line: only-arrow-functions
+ (function(..._args: any[]) {
+ window[DATA_LAYER_NAME].push(arguments);
+ })(...args);
+ };
+ });
} else {
- gtag = () => {
- };
- analyticsInitialized = Promise.resolve();
+ gtag = () => {};
+ __analyticsInitialized = Promise.resolve();
}
}
- let analytics = analyticsInstanceCache[options[ANALYTICS_ID_FIELD]];
- if (!analytics) {
- analytics = of(undefined).pipe(
- observeOn(new ɵAngularFireSchedulers(zone).outsideAngular),
- switchMap(() => isPlatformBrowser(platformId) ? import('firebase/analytics') : EMPTY),
- map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
- map(app => app.analytics()),
- tap(analytics => {
- if (analyticsCollectionEnabled === false) {
- analytics.setAnalyticsCollectionEnabled(false);
- }
- }),
- shareReplay({ bufferSize: 1, refCount: false })
- );
- analyticsInstanceCache[options[ANALYTICS_ID_FIELD]] = analytics;
- }
+ const analytics = of(undefined).pipe(
+ observeOn(new ɵAngularFireSchedulers(zone).outsideAngular),
+ switchMap(() => isPlatformBrowser(platformId) ? import('firebase/analytics') : EMPTY),
+ map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
+ // app.analytics doesn't expose settings, which is odd... bug?
+ /* tap((app: any) => app.analytics.settings({
+ dataLayerName: DATA_LAYER_NAME,
+ gtagName: GTAG_FUNCTION_NAME,
+ })), */
+ map(app => app.analytics()),
+ tap(analytics => {
+ if (analyticsCollectionEnabled === false) {
+ analytics.setAnalyticsCollectionEnabled(false);
+ }
+ }),
+ shareReplay({ bufferSize: 1, refCount: false })
+ );
if (providedConfig) {
this.updateConfig(providedConfig);
}
- if (providedAppName) {
- this.updateConfig({ [APP_NAME_KEY]: providedAppName });
- }
- if (providedAppVersion) {
- this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion });
- }
if (debugModeEnabled) {
this.updateConfig({ [DEBUG_MODE_KEY]: 1 });
}