Skip to content

Flushing out the new API #2963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 6, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
Fix modular user/screen tracking
  • Loading branch information
jamesdaniels committed Oct 6, 2021
commit a5f63d32cde83f6b0cd3748549021b9703f56d23
6 changes: 5 additions & 1 deletion samples/advanced/src/app/app.module.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 13 additions & 5 deletions src/analytics/analytics.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import { registerVersion } from 'firebase/app';
import { ScreenTrackingService } from './screen-tracking.service';
import { UserTrackingService } from './user-tracking.service';

const PROVIDED_ANALYTICS_INSTANCES = new InjectionToken<Analytics[]>('angularfire2.analytics-instances');
export const PROVIDED_ANALYTICS_INSTANCE_FACTORIES = new InjectionToken<Array<(injector: Injector) => Analytics>>('angularfire2.analytics-instances.factory');
export const PROVIDED_ANALYTICS_INSTANCES = new InjectionToken<Analytics[]>('angularfire2.analytics-instances');
const IS_SUPPORTED = new InjectionToken<boolean>('angularfire2.analytics.isSupported');

const isSupportedSymbol = Symbol('angularfire2.analytics.isSupported');
const isSupportedValueSymbol = Symbol('angularfire2.analytics.isSupported.value');
export const isSupportedPromiseSymbol = Symbol('angularfire2.analytics.isSupported');

globalThis[isSupportedPromiseSymbol] ||= isSupported().then(it => globalThis[isSupportedValueSymbol] = it);

export function defaultAnalyticsInstanceFactory(isSupported: boolean, provided: FirebaseAnalytics[]|undefined, defaultApp: FirebaseApp) {
if (!isSupported) { return null; }
Expand Down Expand Up @@ -49,9 +53,9 @@ const DEFAULT_ANALYTICS_INSTANCE_PROVIDER = {
ANALYTICS_INSTANCES_PROVIDER,
{
provide: APP_INITIALIZER,
useValue: () => isSupported().then(it => globalThis[isSupportedSymbol] = it),
useValue: () => globalThis[isSupportedPromiseSymbol],
multi: true,
},
}
]
})
export class AnalyticsModule {
Expand All @@ -68,7 +72,11 @@ export function provideAnalytics(fn: (injector: Injector) => FirebaseAnalytics,
ngModule: AnalyticsModule,
providers: [{
provide: IS_SUPPORTED,
useFactory: () => globalThis[isSupportedSymbol],
useFactory: () => globalThis[isSupportedValueSymbol],
}, {
provide: PROVIDED_ANALYTICS_INSTANCE_FACTORIES,
useValue: fn,
multi: true,
}, {
provide: PROVIDED_ANALYTICS_INSTANCES,
useFactory: analyticsInstanceFactory(fn),
Expand Down
17 changes: 14 additions & 3 deletions src/analytics/screen-tracking.service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { ComponentFactoryResolver, Injectable, NgZone, OnDestroy, Optional } from '@angular/core';
import { Inject, ComponentFactoryResolver, Injectable, NgZone, OnDestroy, Optional, Injector } from '@angular/core';
import { of, Subscription, Observable } from 'rxjs';
import { distinctUntilChanged, filter, groupBy, map, mergeMap, pairwise, startWith, switchMap } from 'rxjs/operators';
import { ActivationEnd, Router, ɵEmptyOutletComponent } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { VERSION } from '@angular/fire';
import { FirebaseApp } from '@angular/fire/app';
import { registerVersion } from 'firebase/app';

import { Analytics } from './analytics';
import { logEvent } from './firebase';
import { UserTrackingService } from './user-tracking.service';
import { analyticsInstanceFactory, defaultAnalyticsInstanceFactory, isSupportedPromiseSymbol, PROVIDED_ANALYTICS_INSTANCE_FACTORIES } from './analytics.module';

const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin';
const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class';
Expand Down Expand Up @@ -146,21 +148,30 @@ export class ScreenTrackingService implements OnDestroy {
private disposable: Subscription | undefined;

constructor(
analytics: Analytics,
@Optional() router: Router,
@Optional() title: Title,
componentFactoryResolver: ComponentFactoryResolver,
zone: NgZone,
@Optional() userTrackingService: UserTrackingService,
firebaseApp: FirebaseApp,
@Inject(PROVIDED_ANALYTICS_INSTANCE_FACTORIES) analyticsInstanceFactories: Array<(injector: Injector) => Analytics>,
injector: Injector,
) {
registerVersion('angularfire', VERSION.full, 'screen-tracking');
if (!analytics || !router) { return this; }
if (!router) { return this; }
// Analytics is not ready to be injected yet, as the APP_INITIALIZER hasn't evulated yet, do this the hard way
const analyticsInstance: Promise<Analytics|null> = globalThis[isSupportedPromiseSymbol].then((isSupported: boolean) => {
const analyticsInstances = analyticsInstanceFactories.map(fn => analyticsInstanceFactory(fn)(zone, isSupported, injector));
return defaultAnalyticsInstanceFactory(isSupported, analyticsInstances, firebaseApp);
});
zone.runOutsideAngular(() => {
this.disposable = ɵscreenViewEvent(router, title, componentFactoryResolver).pipe(
switchMap(async params => {
if (userTrackingService) {
await userTrackingService.initialized;
}
const analytics = await analyticsInstance;
if (!analytics) { return; }
return logEvent(analytics, SCREEN_VIEW_EVENT, params);
})
).subscribe();
Expand Down
35 changes: 20 additions & 15 deletions src/analytics/user-tracking.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Injectable, NgZone, OnDestroy } from '@angular/core';
import { Inject, Injectable, Injector, NgZone, OnDestroy } from '@angular/core';
import { Analytics } from './analytics';
import { Subscription } from 'rxjs';
import { VERSION } from '@angular/fire';
import { Auth, authState } from '@angular/fire/auth';
import { registerVersion } from 'firebase/app';
import { setUserId } from './firebase';
import { analyticsInstanceFactory, defaultAnalyticsInstanceFactory, isSupportedPromiseSymbol, PROVIDED_ANALYTICS_INSTANCE_FACTORIES } from './analytics.module';
import { FirebaseApp } from '@angular/fire/app';

@Injectable()
export class UserTrackingService implements OnDestroy {
Expand All @@ -13,24 +15,27 @@ export class UserTrackingService implements OnDestroy {
private readonly disposables: Array<Subscription> = [];

constructor(
analytics: Analytics,
auth: Auth,
zone: NgZone,
@Inject(PROVIDED_ANALYTICS_INSTANCE_FACTORIES) analyticsInstanceFactories: Array<(injector: Injector) => Analytics>,
injector: Injector,
firebaseApp: FirebaseApp,
) {
registerVersion('angularfire', VERSION.full, 'user-tracking');
if (analytics) {
let resolveInitialized: () => void;
this.initialized = zone.runOutsideAngular(() => new Promise(resolve => { resolveInitialized = resolve; }));
this.disposables = [
// TODO add credential tracking back in
authState(auth).subscribe(user => {
resolveInitialized();
setUserId(analytics, user?.uid);
}),
];
} else {
this.initialized = Promise.reject();
}
// Analytics is not ready to be injected yet, as the APP_INITIALIZER hasn't evulated yet, do this the hard way
const analyticsInstance: Promise<Analytics|null> = globalThis[isSupportedPromiseSymbol].then((isSupported: boolean) => {
const analyticsInstances = analyticsInstanceFactories.map(fn => analyticsInstanceFactory(fn)(zone, isSupported, injector));
return defaultAnalyticsInstanceFactory(isSupported, analyticsInstances, firebaseApp);
});
let resolveInitialized: () => void;
this.initialized = zone.runOutsideAngular(() => new Promise(resolve => { resolveInitialized = resolve; }));
this.disposables = [
// TODO add credential tracking back in
authState(auth).subscribe(user => {
analyticsInstance.then(analytics => analytics && setUserId(analytics, user?.uid));
resolveInitialized();
}),
];
}

ngOnDestroy() {
Expand Down
7 changes: 4 additions & 3 deletions src/schematics/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { FirebaseRc, Workspace, WorkspaceProject, FirebaseApp, DeployOptions, FE
import { join } from 'path';
import { SchematicsException, Tree } from '@angular-devkit/schematics';
import ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript';
import { findNode, addImportToModule, insertImport } from '@schematics/angular/utility/ast-utils';
import { findNode, addImportToModule, addProviderToModule, insertImport } from '@schematics/angular/utility/ast-utils';
import { InsertChange, ReplaceChange, applyToUpdateRecorder, Change } from '@schematics/angular/utility/change';
import { buildRelativePath } from '@schematics/angular/utility/find-module';
import { overwriteIfExists } from './common';
Expand Down Expand Up @@ -217,10 +217,11 @@ export function addToNgModule(host: Tree, options: { sourcePath: string, feature
options.features.includes(FEATURES.Analytics) &&
!findNode(source, ts.SyntaxKind.Identifier, 'provideAnalytics')
) {
// TODO add user and screen tracking service
changes.push(
insertImport(source, modulePath, ['provideAnalytics', 'getAnalytics'] as any, '@angular/fire/analytics'),
insertImport(source, modulePath, ['provideAnalytics', 'getAnalytics', 'ScreenTrackingService', 'UserTrackingService'] as any, '@angular/fire/analytics'),
...addImportToModule(source, modulePath, `provideAnalytics(() => getAnalytics())`, null as any),
...addProviderToModule(source, modulePath, `ScreenTrackingService`, null as any),
...addProviderToModule(source, modulePath, `UserTrackingService`, null as any),
);
}

Expand Down