UNPKG

ember-metrics

Version:

Send data to multiple analytics integrations without re-implementing new API

220 lines (190 loc) 5.89 kB
import Service from '@ember/service'; import { assert } from '@ember/debug'; import { dasherize } from '@ember/string'; import { getOwner } from '@ember/application'; export default class Metrics extends Service { /** * Cached adapters to reduce multiple expensive lookups. * * @property _adapters * @private * @type Object * @default null */ _adapters = {}; /** * Contextual information attached to each call to an adapter. Often you'll * want to include things like `currentUser.name` with every event or page * view that's tracked. Any properties that you bind to `metrics.context` * will be merged into the options for every service call. * * @property context * @type Object * @default null */ context = {}; /** * Indicates whether calls to the service will be forwarded to the adapters. * This is determined by investigating the user's doNotTrack settings. * * Note that the doNotTrack specification is deprecated, and could stop * working at any minute. As such should this feature not be detected we * presume tracking is permitted. * * @property enabled * @type Boolean */ enabled = typeof navigator !== 'undefined' && navigator.doNotTrack !== '1'; /** * Environment the host application is running in (e.g. development or production). */ appEnvironment = null; /** * When the Service is created, activate adapters that were specified in the * configuration. This config is injected into the Service as `options`. */ constructor() { super(...arguments); const owner = getOwner(this); owner.registerOptionsForType('ember-metrics@metrics-adapter', { instantiate: false, }); owner.registerOptionsForType('metrics-adapter', { instantiate: false }); const config = owner.factoryFor('config:environment').class; const { metricsAdapters = [] } = config; const { environment = 'development' } = config; this._options = { metricsAdapters, environment }; this.appEnvironment = environment; this.activateAdapters(metricsAdapters); } /** * Instantiates adapters from passed adapter options and caches them for future retrieval. * * @method activateAdapters * @param {Array} adapterOptions * @return {Object} instantiated adapters */ activateAdapters(adapterOptions = []) { if (!this.enabled) { return; } const adaptersForEnv = this._adaptersForEnv(adapterOptions); const activeAdapters = {}; for (let { name, config } of adaptersForEnv) { let adapterClass = this._lookupAdapter(name); if (typeof FastBoot === 'undefined' || adapterClass.supportsFastBoot) { activeAdapters[name] = this._adapters[name] || this._activateAdapter({ adapterClass, config }); } } this._adapters = activeAdapters; return this._adapters; } /** * Returns all adapterOptions that should be activated in the current application environment. * Defaults to all environments if the option is `all` or undefined. * * @method adaptersForEnv * @param {Array} adapterOptions * @private * @return {Array} - adapter options in the current environment */ _adaptersForEnv(adapterOptions = []) { return adapterOptions.filter(({ environments = ['all'] }) => { return ( environments.includes('all') || environments.includes(this.appEnvironment) ); }); } /** * Looks up the adapter from the container. Prioritizes the consuming app's * adapters over the addon's adapters. * * @method _lookupAdapter * @param {String} adapterName * @private * @return {Adapter} a local adapter or an adapter from the addon */ _lookupAdapter(adapterName) { assert( '[ember-metrics] Could not find metrics adapter without a name.', adapterName ); const availableAdapter = getOwner(this).lookup( `ember-metrics@metrics-adapter:${dasherize(adapterName)}` ); const localAdapter = getOwner(this).lookup( `metrics-adapter:${dasherize(adapterName)}` ); const adapter = localAdapter || availableAdapter; assert( `[ember-metrics] Could not find metrics adapter ${adapterName}.`, adapter ); return adapter; } /** * Instantiates an adapter. * * @method _activateAdapter * @param {Object} * @private * @return {Adapter} */ _activateAdapter({ adapterClass, config }) { const adapter = new adapterClass(config); adapter.install(); return adapter; } identify() { this.invoke('identify', ...arguments); } alias() { this.invoke('alias', ...arguments); } trackEvent() { this.invoke('trackEvent', ...arguments); } trackPage() { this.invoke('trackPage', ...arguments); } /** * Invokes a method on the passed adapter, or across all activated adapters if not passed. * * @method invoke * @param {String} methodName * @param {Rest} args * @return {Void} */ invoke(methodName, ...args) { if (!this.enabled) { return; } let selectedAdapterNames, options; if (args.length > 1) { selectedAdapterNames = makeArray(args[0]); options = args[1]; } else { selectedAdapterNames = Object.keys(this._adapters); options = args[0]; } for (let adapterName of selectedAdapterNames) { let adapter = this._adapters[adapterName]; adapter && adapter[methodName]({ ...this.context, ...options }); } } /** * On teardown, destroy cached adapters together with the Service. * * @method willDestroy * @return {void} */ willDestroy() { Object.values(this._adapters).forEach((adapter) => adapter.uninstall()); } } function makeArray(maybeArray) { return Array.isArray(maybeArray) ? Array.from(maybeArray) : Array(maybeArray); }