Skip to content
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

Adding AngularFireAnalytics, AngularFireRemoteConfig, and refactoring DI Tokens #2187

Merged
merged 41 commits into from
Jan 7, 2020

Conversation

jamesdaniels
Copy link
Member

@jamesdaniels jamesdaniels commented Oct 2, 2019

  • Adding support for Firebase v7 added in 5.2.3
  • Adding AngularFireAnalytics and AngularFireRemoteConfig modules
  • AddingScreenTrackingService and UserTrackingService which operate in conjunction with AngularFireAnalytics to track screen views and user identifiers respectively
  • Cleaning up the injection token naming patterns, for semver they are aliases to the old ones

TODO:

  • address the Zone.js issues introduced
  • rethink the Remote Config API, not in love
  • test coverage
  • docs
  • add startWith support to AngularFireRemoteConfig
  • export filterRemote(), filterFresh(), etc. pipes in @angular/fire/remote-config
  • add distinctUntilChanged with a custom compare function to AngularFireRemoteConfig
  • export Value as implements Partial<remoteConfig.Value> in the .d.ts, so we don't break on minors
  • generally clean up the types in RC
  • fix the types if you are running older Firebase (this will make travis happy again)
  • implement more of the router types in analytics (perhaps stop using _loadedConfig...)
  • continue to test the robustness of analytics
  • expand the test coverage
  • fix the proxy in the SSR environment

AngularFire "Lazy"

Both AngularFireAnalytics and AngularFireRemoteConfig are of a design @davideast and I have been thinking about for a while; that we've been calling "AngularFire Lazy".

These modules not only provide convenience observables and integrations with Angular, they also lazily load their respective Firebase modules and proxy the Firebase SDK (accounting for the aforementioned lazy-loading) and patch Zone.js.

This allows you to use all of the methods available on the Firebase SDK while knowing that AngularFire has addressed Angular-compatibility. The only major difference is that anything in the vanilla Firebase SDK that isn't a promise now is.

The use of a proxy means that we should not need to add support for new additions to the Firebase SDK for you to take advantage.

Your feedback is wanted! We'll be bringing similar capabilities to the other modules in future releases 😄

@angular/fire/analytics

AngularFireAnalyticsModule

Provides AngularFireAnalytics and initializes ScreenTrackingService and UserTrackingService, if they were loaded.

AngularFireAnalytics

Lazy loads firebase/analytics and proxies firebase.analytics(). APP_VERSION and APP_NAME will be loaded into the Google Analytics, if they are provided.

API

updateConfig(options: {[key:string]: any}): Promise<void>;

// from firebase.analytics() proxy:
logEvent(eventName: string, eventParams?: {[key: string]: any}, options?: analytics.AnalyticsCallOptions): Promise<void>;
setCurrentScreen(screenName: string, options?: analytics.AnalyticsCallOptions): Promise<void>;
setUserId(id: string, options?: analytics.AnalyticsCallOptions): Promise<void>;
setUserProperties(properties: analytics.CustomParams, options?: analytics.AnalyticsCallOptions): Promise<void>;
setAnalyticsCollectionEnabled(enabled: boolean): Promise<void>;
app: Promise<app.App>;

DI

ANALYTICS_COLLECTION_ENABLED: boolean
Globally disable Google Analytics collection by setting this to false. (default: true)

APP_VERSION: string
The application version to pass to Google Analytics.

APP_NAME: string
The application name to pass to Google Analytics.

DEBUG_MODE: boolean
Start Google Analytics in debug mode, so you can test your events in DebugView in the Firebase Console. (default: false)

Also takes FIREBASE_OPTIONS and FIREBASE_APP_NAME like all other Modules.

Usage

@NgModule({
  imports: [
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireAnalyticsModule
  ],
  providers: [
    ScreenTrackingService,
    UserTrackingService,
    { provide: DEBUG_MODE, useFactory: isDevMode },
    { provide: APP_NAME, useValue: 'my awesome web app' },
    { provide: APP_VERSION, useValue: '0.0.0 (707e803)'}
  ]
})
export class AppModule { }

...

constructor(analytics: AngularFireAnalytics) {
    analytics.logEvent('custom_event', { ... }).then(() => {
        ...
    });
}

ScreenTrackingService

Logs screen views and tracks the current screen on Router NavigationEnd events.

UserTrackingService

Tracks the user's uid, if firebase/auth is loaded.

@angular/fire/remote-config

AngularFireRemoteConfigModule

Provides AngularFireRemoteConfig

AngularFireRemoteConfig

Lazy loads firebase/remote-config, proxies firebase.remoteConfig(), and provides convenience observables & pipes for working with Remote Config.

API

interface ConfigTemplate {[key:string]: string|number|boolean}

type Parameter extends remoteConfig.Value {
  key: string,
  fetchTimeMillis: number
}

class AngularFireRemoteConfig {
  changes:    Observable<Parameter>;
  parameters: Observable<Parameter[]>;
  numbers:    Observable<{[key:string]: number|undefined}>  & {[key:string]: Observable<number>};
  booleans:   Observable<{[key:string]: boolean|undefined}> & {[key:string]: Observable<boolean>};
  strings:    Observable<{[key:string]: string|undefined}>  & {[key:string]: Observable<string|undefined>};
  
  // from firebase.remoteConfig() proxy:
  activate: () => Promise<boolean>;
  ensureInitialized: () => Promise<void>;
  fetch: () => Promise<void>;
  fetchAndActivate: () => Promise<boolean>;
  getAll: () => Promise<{[key:string]: remoteConfig.Value}>;
  getBoolean: (key:string) => Promise<boolean>;
  getNumber: (key:string) => Promise<number>;
  getString: (key:string) => Promise<string>;
  getValue: (key:string) => Promise<remoteConfig.Value>;
  setLogLevel: (logLevel: remoteConfig.LogLevel) => Promise<void>;
  settings: Promise<remoteConfig.Settings>;
  defaultConfig: Promise<{[key: string]: string | number | boolean}>;
  fetchTimeMillis: Promise<number>;
  lastFetchStatus: Promise<remoteConfig.FetchStatus>;
}

// Pipes for working with .changes and .parameters

filterRemote: () => MonoTypeOperatorFunction<Parameter | Parameter[]>
filterFresh: (interval: number) => MonoTypeOperatorFunction<Parameter | Parameter[]>
budget: <T>(interval: number) => MonoTypeOperatorFunction<T>

// map and scanToObject have several overloads
// scanToObject is for use with .changes
scanToObject: () => OperatorFunction<Parameter, {[key: string]: string|undefined}>
// mapToObject is the same behavior are scanToObject but for use with .parameters,
mapToObject: () => OperatorFunction<Parameter[], {[key: string]: string|undefined}>

DI

DEFAULT_CONFIG: {[key:string]: string|number|boolean}
Provide default values for Remote Config

REMOTE_CONFIG_SETTINGS: remoteConfig.Settings
Configure your remote config instance

Also takes FIREBASE_OPTIONS and FIREBASE_APP_NAME like all other Modules.

Usage

@NgModule({
  imports: [
    AngularFireModule.initializeApp(environment.firebase),
    AngularFireRemoteConfigModule
  ],
  providers: [
    { provide: DEFAULT_CONFIG, useValue: { enableAwesome: true } },
    {
      provide: REMOTE_CONFIG_SETTINGS,
      useFactory: () => isDevMode() ? { minimumFetchIntervalMillis: 1 } : {}
    }
  ]
})
export class AppModule { }

...

constructor(remoteConfig: AngularFireRemoteConfig) {
    remoteConfig.changes.pipe(
      filterFresh(172_800_000), // ensure we have values from at least 48 hours ago
      first(),
      // scanToObject when used this way is similar to defaults
      // but most importantly smart-casts remote config values and adds type safety
      scanToObject({
        enableAwesome: true,
        titleBackgroundColor: 'blue',
        titleFontSize: 12
      })
    ).subscribe();

    // all remote config values cast as strings
    remoteConfig.strings.subscribe(...)
    remoteConfig.booleans.subscribe(...); // as booleans
    remoteConfig.numbers.subscribe(...); // as numbers

    // convenience for observing a single string
    remoteConfig.strings.titleBackgroundColor.subscribe(...);
    remoteConfig.booleans.enableAwesome.subscribe(...); // boolean
    remoteConfig.numbers.titleBackgroundColor.subscribe(...); // number

    // however those may emit more than once as the remote config cache fires and gets fresh values from the server
    // you can filter it out of .changes for more control:
    remoteConfig.changes.pipe(
      filter(param => param.key === 'titleBackgroundColor'),
      map(param => param.asString())
      // budget at most 800ms and return the freshest value possible in that time
      // our budget pipe is similar to timeout but won't error or abort the pending server fetch (it won't emit it, if the deadline is exceeded, but it will have been fetched so can use the freshest values on next subscription)
      budget(800),
      last()
    ).subscribe(...)

    // just like .changes, but scanned as into an array
    remoteConfig.parameters.subscribe(all => ...);

    // or make promisified firebase().remoteConfig() calls direct off AngularFireRemoteConfig
    // using our proxy
    remoteConfig.getAll().then(all => ...);
    remoteConfig.lastFetchStatus.then(status => ...);
}

@jamesdaniels jamesdaniels changed the title WIP Firebase v7, analytics and remote-config WIP Firebase v7, analytics, remote-config, DI Token refactor Oct 4, 2019
@jamesdaniels
Copy link
Member Author

@jhuleatt @davideast just a WIP, need to get the tests together. But feel free to start reviewing implementation and API.

@jamesdaniels jamesdaniels added this to the 5.3.0 milestone Oct 4, 2019
@johanchouquet
Copy link
Contributor

Hi @jamesdaniels , i find the APIs for Analytics and Remote Config quite nice. So cool to have these new features ready soon.

@Mintenker

This comment has been minimized.

@Newbie012

This comment has been minimized.

@jamesdaniels

This comment has been minimized.

@jimmykane

This comment has been minimized.

@jamesdaniels

This comment has been minimized.

* Add all, numbers, strings, and booleans Observables to AngularFireRemoteConfig
* Proxy all of firebase.remoteConfig() in AngularFireRemoteConfig dealing with lazy loading of the SDK
* Same effort with AngularFireAnalytics
@jamesdaniels jamesdaniels changed the title WIP Firebase v7, analytics, remote-config, DI Token refactor Adding AngularFireAnalytics, AngularFireRemoteConfig, and refactoring DI Tokens Nov 13, 2019
@jamesdaniels
Copy link
Member Author

Updated this branch and the comment, big changes, looking for feedback. Thanks!

@jamesdaniels

This comment has been minimized.

@jamesdaniels

This comment has been minimized.

@jamesdaniels
Copy link
Member Author

New RC API has been published, 5.3.0-rc.3 on NPM under the @next tag. Unless something major comes up, going to call this the final RC for this PR and release later this week.

Rocking out on docs is the plan from here and making sure the proxy doesn't explode when you try to make a call in a non-browser environment.

Thanks for all the feedback!

Gotta ship this and clear my plate so I can immediately jump on Angular 9 and cut AngularFire v6 after the holidays!

@savilaf

This comment has been minimized.

@jimmykane
Copy link

jimmykane commented Dec 20, 2019

I think I have an issue or this is due to AF RC + Angular 9
I get:

Access to fetch at 'https://firebaseinstallations.googleapis.com/v1/projects/quantified-self-io/installations/cfw8xt-wFML3zmZ-Yp-hbi/authTokens:generate' from origin 'https://beta.quantified-self.io' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.```

Perhaps related to https://github.com/firebase/firebase-js-sdk/issues/2364

@jamesdaniels
Copy link
Member Author

Cutting 5.3.0-rc.4. Turns out screen_class wasn't tracking to my satisfaction in lazy loaded routes. I'm out of the well trodden path of public APIs (that I know about) and I haven't tried to test this with deeply nested / complex routes or pushed on trickier edge cases in the Angular router, so would appreciate eyeballs on the ScreenTrackingService.

@jamesdaniels
Copy link
Member Author

@savilaf sounds like the RC endpoint is just throttling you, this exception should affect anything other than your possibly getting more fresh values from the server. You can fix this error by either increasing your minimumFetchIntervalMillis in REMOTE_CONFIG_SETTINGS or using the filterFresh pipe I provide in addition to a first.

See how I addressed in one of my apps here:

export class AppComponent implements OnInit, OnDestroy {

  public configuration$: Observable<typeof environment.remoteConfigDefaults & Record<string, string|undefined>>;

  constructor(
    private remoteConfig: AngularFireRemoteConfig,
    @Inject(PLATFORM_ID) private platformId: Object
  ) { }

  ngOnInit() {
    const template = environment.remoteConfigDefaults;
    this.configuration$ = this.remoteConfig.parameters.pipe(
      untilComponentDestroyed(this),
      // rate-limit to 48-hours, unless we're not in the browser & only have defaults
      isPlatformBrowser(this.platformId) ? filterFresh(172_800_000) : tap(),
      first(),
      mapToObject(template)
    );
  }

  ngOnDestroy() { }

}

@mattgek
Copy link

mattgek commented Dec 22, 2019

Hi,

Will there be an option to disable analytics (GDPR Europe)? It must be possible for users to opt-out from analytics.

@jamesdaniels
Copy link
Member Author

@mattgek tottally. With this API you can make it opt-in via setting ANALYTICS_COLLECTION_ENABLED to false by default, then opt-in with setAnalyticsCollectionEnabled(true).

Opt-out you can simply setAnalyticsCollectionEnabled(false).

Either way you can then useFactory for ANALYTICS_COLLECTION_ENABLED DI token and look at the user's cookies or local store; so their choice persists.

@jimmykane
Copy link

On Angular 9 I get when I enable the UserTrackingService :

ERROR TypeError: Cannot read property 'subscribe' of undefined
    at Observable._subscribe (subscribeToObservable.js:4)
    at Observable._trySubscribe (Observable.js:42)
    at Observable.subscribe (Observable.js:28)
    at MapOperator.call (map.js:16)
    at Observable.subscribe (Observable.js:23)
    at SwitchMapOperator.call (switchMap.js:17)
    at Observable.subscribe (Observable.js:23)
    at SwitchMapOperator.call (switchMap.js:17)
    at Observable.subscribe (Observable.js:23)
    at Observable._subscribe (angularfire2.js:59)
    at Observable._trySubscribe (Observable.js:42)
    at Observable.subscribe (Observable.js:28)
    at angularfire2.js:53
    at ZoneDelegate.invoke (zone-evergreen.js:365)
    at Zone.run (zone-evergreen.js:124)
    at NgZone.runOutsideAngular (core.js:41316)
    at Observable._subscribe (angularfire2.js:52)
    at Observable._trySubscribe (Observable.js:42)
    at Observable.subscribe (Observable.js:28)
    at new UserTrackingService (analytics.service.js:138)
    at Object.UserTrackingService_Factory [as factory] (analytics.service.js:146)
    at R3Injector.hydrate (core.js:18772)
    at R3Injector.get (core.js:18534)
    at injectInjectorOnly (core.js:903)
    at Module.ɵɵinject (core.js:913)
    at Object.AngularFireAnalyticsModule_Factory [as factory] (analytics.module.js:21)
    at R3Injector.hydrate (core.js:18772)
    at R3Injector.get (core.js:18534)
    at core.js:18464
    at Set.forEach (<anonymous>)
    at new R3Injector (core.js:18460)
    at createInjector (core.js:18411)
    at new NgModuleRef$1 (core.js:36393)
    at NgModuleFactory$1.create (core.js:36496)
    at core.js:42390
    at ZoneDelegate.invoke (zone-evergreen.js:365)
    at Object.onInvoke (core.js:41480)
    at ZoneDelegate.invoke (zone-evergreen.js:364)
    at Zone.run (zone-evergreen.js:124)
    at NgZone.run (core.js:41255)
    at PlatformRef.bootstrapModuleFactory (core.js:42383)
    at core.js:42459
    at ZoneDelegate.invoke (zone-evergreen.js:365)
    at Zone.run (zone-evergreen.js:124)
    at zone-evergreen.js:851
    at ZoneDelegate.invokeTask (zone-evergreen.js:400)
    at Zone.runTask (zone-evergreen.js:168)
    at drainMic

src/storage/storage.ts Outdated Show resolved Hide resolved
@jamesdaniels jamesdaniels merged commit 0c5a157 into master Jan 7, 2020
@jamesdaniels jamesdaniels deleted the firebase-v7 branch January 7, 2020 22:58
@jamesdaniels
Copy link
Member Author

jamesdaniels commented Jan 7, 2020

Released 5.3 with some minor changes from the last RC. Thanks for your feedback everyone, if you’re still having issues please file new issues.

The priority from here will be getting ng9 / ivy support all buttoned up; and the backlog of PRs addressed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.