UNPKG

@angular/fire

Version:

The official library for Firebase and Angular

570 lines (563 loc) 23.6 kB
import { __awaiter } from 'tslib'; import { InjectionToken, Injectable, Inject, Optional, PLATFORM_ID, NgZone, ɵɵdefineInjectable, ɵɵinject, NgModuleFactory, ComponentFactoryResolver, Injector, INJECTOR, NgModule } from '@angular/core'; import { of, EMPTY, Observable, from } from 'rxjs'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; import { observeOn, switchMap, map, tap, shareReplay, filter, withLatestFrom, groupBy, mergeMap, startWith, pairwise } from 'rxjs/operators'; import { ɵAngularFireSchedulers, ɵfirebaseAppFactory, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { ActivationEnd, NavigationEnd, ROUTES, Router } from '@angular/router'; import { Title } from '@angular/platform-browser'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @record */ function Config() { } /** @type {?} */ const COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); /** @type {?} */ const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); /** @type {?} */ const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); /** @type {?} */ const DEBUG_MODE = new InjectionToken('angularfire2.analytics.debugMode'); /** @type {?} */ const CONFIG = new InjectionToken('angularfire2.analytics.config'); /** @type {?} */ const APP_NAME_KEY = 'app_name'; /** @type {?} */ const APP_VERSION_KEY = 'app_version'; /** @type {?} */ const DEBUG_MODE_KEY = 'debug_mode'; /** @type {?} */ const ANALYTICS_ID_FIELD = 'measurementId'; /** @type {?} */ const GTAG_CONFIG_COMMAND = 'config'; /** @type {?} */ const GTAG_FUNCTION_NAME = 'gtag'; /** @type {?} */ const DATA_LAYER_NAME = 'dataLayer'; // WARNING: interface has both a type and a value, skipping emit /** @type {?} */ let gtag; /** @type {?} */ let analyticsInitialized; /** @type {?} */ const analyticsInstanceCache = {}; class AngularFireAnalytics { /** * @param {?} options * @param {?} nameOrConfig * @param {?} analyticsCollectionEnabled * @param {?} providedAppVersion * @param {?} providedAppName * @param {?} debugModeEnabled * @param {?} providedConfig * @param {?} platformId * @param {?} zone */ constructor(options, nameOrConfig, analyticsCollectionEnabled, providedAppVersion, providedAppName, debugModeEnabled, providedConfig, // tslint:disable-next-line:ban-types platformId, zone) { this.options = options; if (!analyticsInitialized) { if (isPlatformBrowser(platformId)) { gtag = window[GTAG_FUNCTION_NAME] || ((/** * @param {...?} args * @return {?} */ (...args) => { window[DATA_LAYER_NAME].push(args); })); window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; analyticsInitialized = zone.runOutsideAngular((/** * @return {?} */ () => new Promise((/** * @param {?} resolve * @return {?} */ resolve => { window[GTAG_FUNCTION_NAME] = (/** * @param {...?} args * @return {?} */ (...args) => { if (args[0] === 'js') { resolve(); } gtag(...args); }); })))); } else { gtag = (/** * @return {?} */ () => { }); analyticsInitialized = Promise.resolve(); } } /** @type {?} */ let analytics = analyticsInstanceCache[options[ANALYTICS_ID_FIELD]]; if (!analytics) { analytics = of(undefined).pipe(observeOn(new ɵAngularFireSchedulers(zone).outsideAngular), switchMap((/** * @return {?} */ () => isPlatformBrowser(platformId) ? import('firebase/analytics') : EMPTY)), map((/** * @return {?} */ () => ɵfirebaseAppFactory(options, zone, nameOrConfig))), map((/** * @param {?} app * @return {?} */ app => app.analytics())), tap((/** * @param {?} analytics * @return {?} */ analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false); } })), shareReplay({ bufferSize: 1, refCount: false })); analyticsInstanceCache[options[ANALYTICS_ID_FIELD]] = analytics; } 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 }); } return ɵlazySDKProxy(this, analytics, zone); } /** * @param {?} config * @return {?} */ updateConfig(config) { return __awaiter(this, void 0, void 0, function* () { yield analyticsInitialized; gtag(GTAG_CONFIG_COMMAND, this.options[ANALYTICS_ID_FIELD], Object.assign(Object.assign({}, config), { update: true })); }); } } AngularFireAnalytics.decorators = [ { type: Injectable, args: [{ providedIn: 'any' },] } ]; /** @nocollapse */ AngularFireAnalytics.ctorParameters = () => [ { type: undefined, decorators: [{ type: Inject, args: [FIREBASE_OPTIONS,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [FIREBASE_APP_NAME,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [COLLECTION_ENABLED,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [APP_VERSION,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [APP_NAME,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DEBUG_MODE,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [CONFIG,] }] }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }, { type: NgZone } ]; /** @nocollapse */ AngularFireAnalytics.ɵprov = ɵɵdefineInjectable({ factory: function AngularFireAnalytics_Factory() { return new AngularFireAnalytics(ɵɵinject(FIREBASE_OPTIONS), ɵɵinject(FIREBASE_APP_NAME, 8), ɵɵinject(COLLECTION_ENABLED, 8), ɵɵinject(APP_VERSION, 8), ɵɵinject(APP_NAME, 8), ɵɵinject(DEBUG_MODE, 8), ɵɵinject(CONFIG, 8), ɵɵinject(PLATFORM_ID), ɵɵinject(NgZone)); }, token: AngularFireAnalytics, providedIn: "any" }); if (false) { /** * @type {?} * @private */ AngularFireAnalytics.prototype.options; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @type {?} */ const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin'; /** @type {?} */ const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; /** @type {?} */ const FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id'; /** @type {?} */ const FIREBASE_PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen'; /** @type {?} */ const FIREBASE_SCREEN_CLASS_KEY = 'firebase_screen_class'; /** @type {?} */ const FIREBASE_SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id'; /** @type {?} */ const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen'; /** @type {?} */ const OUTLET_KEY = 'outlet'; /** @type {?} */ const PAGE_PATH_KEY = 'page_path'; /** @type {?} */ const PAGE_TITLE_KEY = 'page_title'; /** @type {?} */ const SCREEN_CLASS_KEY = 'screen_class'; /** @type {?} */ const SCREEN_NAME_KEY = 'screen_name'; /** @type {?} */ const SCREEN_VIEW_EVENT = 'screen_view'; /** @type {?} */ const EVENT_ORIGIN_AUTO = 'auto'; /** @type {?} */ const DEFAULT_SCREEN_CLASS = '???'; /** @type {?} */ const NG_PRIMARY_OUTLET = 'primary'; /** @type {?} */ const SCREEN_INSTANCE_DELIMITER = '#'; /** @type {?} */ const ANNOTATIONS = '__annotations__'; // this is an INT64 in iOS/Android but use INT32 cause javascript /** @type {?} */ let nextScreenInstanceID = Math.floor(Math.random() * (Math.pow(2, 32) - 1)) - Math.pow(2, 31); /** @type {?} */ const knownScreenInstanceIDs = {}; /** @type {?} */ const getScreenInstanceID = (/** * @param {?} params * @return {?} */ (params) => { // unique the screen class against the outlet name /** @type {?} */ const screenInstanceKey = [ params[SCREEN_CLASS_KEY], params[OUTLET_KEY] ].join(SCREEN_INSTANCE_DELIMITER); if (knownScreenInstanceIDs.hasOwnProperty(screenInstanceKey)) { return knownScreenInstanceIDs[screenInstanceKey]; } else { /** @type {?} */ const ret = nextScreenInstanceID++; knownScreenInstanceIDs[screenInstanceKey] = ret; return ret; } }); const ɵ0 = getScreenInstanceID; class ScreenTrackingService { /** * @param {?} analytics * @param {?} router * @param {?} title * @param {?} componentFactoryResolver * @param {?} platformId * @param {?} debugModeEnabled * @param {?} zone * @param {?} injector */ constructor(analytics, router, title, componentFactoryResolver, // tslint:disable-next-line:ban-types platformId, debugModeEnabled, zone, injector) { if (!router || !isPlatformBrowser(platformId)) { return this; } zone.runOutsideAngular((/** * @return {?} */ () => { /** @type {?} */ const activationEndEvents = router.events.pipe(filter((/** * @param {?} e * @return {?} */ e => e instanceof ActivationEnd))); /** @type {?} */ const navigationEndEvents = router.events.pipe(filter((/** * @param {?} e * @return {?} */ e => e instanceof NavigationEnd))); this.disposable = navigationEndEvents.pipe(withLatestFrom(activationEndEvents), switchMap((/** * @param {?} __0 * @return {?} */ ([navigationEnd, activationEnd]) => { // SEMVER: start using optional chains and nullish coalescing once we support newer typescript /** @type {?} */ const pagePath = navigationEnd.url; /** @type {?} */ const screenName = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || pagePath; /** @type {?} */ const params = { [SCREEN_NAME_KEY]: screenName, [PAGE_PATH_KEY]: pagePath, [FIREBASE_EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO, [FIREBASE_SCREEN_NAME_KEY]: screenName, [OUTLET_KEY]: activationEnd.snapshot.outlet }; if (title) { params[PAGE_TITLE_KEY] = title.getTitle(); } /** @type {?} */ const component = activationEnd.snapshot.component; /** @type {?} */ const routeConfig = activationEnd.snapshot.routeConfig; /** @type {?} */ 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(Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: loadChildren.split('#')[1] })); } else if (typeof component === 'string') { return of(Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: component })); } else if (component) { /** @type {?} */ const componentFactory = componentFactoryResolver.resolveComponentFactory(component); return of(Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: componentFactory.selector })); } else if (loadChildren) { /** @type {?} */ const loadedChildren = loadChildren(); /** @type {?} */ const loadedChildren$ = (loadedChildren instanceof Observable) ? loadedChildren : from(Promise.resolve(loadedChildren)); return loadedChildren$.pipe(map((/** * @param {?} lazyModule * @return {?} */ lazyModule => { if (lazyModule instanceof NgModuleFactory) { // AOT create an injector /** @type {?} */ const moduleRef = lazyModule.create(injector); // INVESTIGATE is this the right way to get at the matching route? /** @type {?} */ const routes = moduleRef.injector.get(ROUTES); /** @type {?} */ const component = routes[0][0].component; try { /** @type {?} */ const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(component); return Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: componentFactory.selector }); } catch (_) { return Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS }); } } else { // JIT look at the annotations // INVESTIGATE are there public APIs for this stuff? /** @type {?} */ const declarations = [].concat.apply([], (lazyModule[ANNOTATIONS] || []).map((/** * @param {?} f * @return {?} */ (f) => f.declarations))); /** @type {?} */ const selectors = [].concat.apply([], declarations.map((/** * @param {?} c * @return {?} */ (c) => (c[ANNOTATIONS] || []).map((/** * @param {?} f * @return {?} */ (f) => 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 Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: selectors[0] || DEFAULT_SCREEN_CLASS }); } }))); } else { return of(Object.assign(Object.assign({}, params), { [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS })); } })), map((/** * @param {?} params * @return {?} */ params => (Object.assign({ [FIREBASE_SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY], [FIREBASE_SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params) }, params)))), tap((/** * @param {?} params * @return {?} */ 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((/** * @param {?} params * @return {?} */ params => params[OUTLET_KEY])), // tslint:disable-next-line mergeMap((/** * @param {?} group * @return {?} */ group => group.pipe(startWith(undefined), pairwise()))), map((/** * @param {?} __0 * @return {?} */ ([prior, current]) => prior ? Object.assign({ [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((/** * @param {?} params * @return {?} */ params => debugModeEnabled && console.info(SCREEN_VIEW_EVENT, params))), tap((/** * @param {?} params * @return {?} */ params => zone.runOutsideAngular((/** * @return {?} */ () => analytics.logEvent(SCREEN_VIEW_EVENT, params)))))).subscribe(); })); } /** * @return {?} */ ngOnDestroy() { if (this.disposable) { this.disposable.unsubscribe(); } } } ScreenTrackingService.decorators = [ { type: Injectable, args: [{ providedIn: 'any' },] } ]; /** @nocollapse */ ScreenTrackingService.ctorParameters = () => [ { type: AngularFireAnalytics }, { type: Router, decorators: [{ type: Optional }] }, { type: Title, decorators: [{ type: Optional }] }, { type: ComponentFactoryResolver }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: [DEBUG_MODE,] }] }, { type: NgZone }, { type: Injector } ]; /** @nocollapse */ ScreenTrackingService.ɵprov = ɵɵdefineInjectable({ factory: function ScreenTrackingService_Factory() { return new ScreenTrackingService(ɵɵinject(AngularFireAnalytics), ɵɵinject(Router, 8), ɵɵinject(Title, 8), ɵɵinject(ComponentFactoryResolver), ɵɵinject(PLATFORM_ID), ɵɵinject(DEBUG_MODE, 8), ɵɵinject(NgZone), ɵɵinject(INJECTOR)); }, token: ScreenTrackingService, providedIn: "any" }); if (false) { /** * @type {?} * @private */ ScreenTrackingService.prototype.disposable; } class UserTrackingService { // TODO a user properties injector /** * @param {?} analytics * @param {?} zone * @param {?} platformId */ constructor(analytics, zone, // tslint:disable-next-line:ban-types platformId) { /** @type {?} */ const schedulers = new ɵAngularFireSchedulers(zone); if (!isPlatformServer(platformId)) { zone.runOutsideAngular((/** * @return {?} */ () => { // @ts-ignore zap the import in the UMD this.disposable = from(import('firebase/auth')).pipe(observeOn(schedulers.outsideAngular), switchMap((/** * @return {?} */ () => analytics.app)), map((/** * @param {?} app * @return {?} */ app => app.auth())), switchMap((/** * @param {?} auth * @return {?} */ auth => new Observable(auth.onAuthStateChanged.bind(auth)))), switchMap((/** * @param {?} user * @return {?} */ user => analytics.setUserId(user ? user.uid : null)))).subscribe(); })); } } /** * @return {?} */ ngOnDestroy() { if (this.disposable) { this.disposable.unsubscribe(); } } } UserTrackingService.decorators = [ { type: Injectable, args: [{ providedIn: 'any' },] } ]; /** @nocollapse */ UserTrackingService.ctorParameters = () => [ { type: AngularFireAnalytics }, { type: NgZone }, { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] } ]; /** @nocollapse */ UserTrackingService.ɵprov = ɵɵdefineInjectable({ factory: function UserTrackingService_Factory() { return new UserTrackingService(ɵɵinject(AngularFireAnalytics), ɵɵinject(NgZone), ɵɵinject(PLATFORM_ID)); }, token: UserTrackingService, providedIn: "any" }); if (false) { /** * @type {?} * @private */ UserTrackingService.prototype.disposable; } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class AngularFireAnalyticsModule { /** * @param {?} analytics * @param {?} screenTracking * @param {?} userTracking */ constructor(analytics, screenTracking, userTracking) { // calling anything on analytics will eagerly load the SDK // tslint:disable-next-line:no-unused-expression analytics.app; } } AngularFireAnalyticsModule.decorators = [ { type: NgModule, args: [{ providers: [AngularFireAnalytics] },] } ]; /** @nocollapse */ AngularFireAnalyticsModule.ctorParameters = () => [ { type: AngularFireAnalytics }, { type: ScreenTrackingService, decorators: [{ type: Optional }] }, { type: UserTrackingService, decorators: [{ type: Optional }] } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ export { APP_NAME, APP_VERSION, AngularFireAnalytics, AngularFireAnalyticsModule, COLLECTION_ENABLED, CONFIG, DEBUG_MODE, ScreenTrackingService, UserTrackingService }; //# sourceMappingURL=angular-fire-analytics.js.map