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 }); }