Send data to multiple analytics services without re-implementing a new API
This addon adds a simple metrics service to your app that makes it easy to
send data to multiple analytics services without having to implement a new API
each time. Track events, page views, and more through one API, and add or
remove analytics services just by changing how you activate their adapters.
Writing your own adapter for an unsupported service is easy too — see Writing your own adapters.
Upgrading from the classic (string-name) API? Adapters are now activated by passing adapter classes to
metrics.activateAdapters(...)rather than by string name viaconfig/environment. This makes the addon compatible withember-strict-application-resolverand Embroider's static builds. See Configuration & activation.
- Ember.js v5.8 or above
- Embroider or ember-auto-import v2
ember install ember-metricsEach service ships as an adapter class you import from
ember-metrics/metrics-adapters/<name> (dasherized), e.g.
ember-metrics/metrics-adapters/google-analytics-four. The options below are
the values you put in that adapter's config.
-
GoogleAnalytics—ember-metrics/metrics-adapters/google-analyticsid: Property ID, e.g.UA-XXXX-Y
-
GoogleAnalyticsFour—ember-metrics/metrics-adapters/google-analytics-fourid: Measurement Id, e.g.G-XXXXoptions: optional An object passed directly to the configuration tag, e.g.:
options = { anonymize_ip: true, debug_mode: environment === 'development', };
By default GA4 automatically tracks page views when the history location changes. This means
this.metrics.trackPage()is ignored by default. If you want to track page views manually, setsend_page_view: falseinside the options. To avoid double counting, make sure enhanced measurement is configured correctly — typically disabling Page changes based on browser history events under the advanced settings of the page views section. -
Mixpanel—ember-metrics/metrics-adapters/mixpaneltoken: Mixpanel token- Optionally other config options to override
-
GoogleTagManager—ember-metrics/metrics-adapters/google-tag-managerid: Container ID, e.g.GTM-XXXXdataLayer: An array containing a single POJO of information, e.g.:
dataLayer = [ { pageCategory: 'signup', visitorType: 'high-value', }, ];
envParams: A string with custom arguments for configuring GTM environments (Live, Dev, etc), e.g.:
envParams: 'gtm_auth=xxxxx>m_preview=env-xx>m_cookies_win=x';
-
Segment—ember-metrics/metrics-adapters/segmentkey: Segment keyproxyDomain: optional Custom domain proxy
-
Piwik—ember-metrics/metrics-adapters/piwikpiwikUrl: Tracker URLsiteId: Site Id
-
Intercom—ember-metrics/metrics-adapters/intercomappId: App ID
-
FacebookPixel—ember-metrics/metrics-adapters/facebook-pixelid: IDdataProcessingOptions: optional An object defining the method, country and state for data processing options
dataProcessingOptions: { method: ['LDU'], country: 1, state: 1000 }
-
Amplitude—ember-metrics/metrics-adapters/amplitudeapiKey: API Key
-
AzureAppInsights—ember-metrics/metrics-adapters/azure-app-insightsinstrumentationKey: Instrumentation Key
-
Pendo—ember-metrics/metrics-adapters/pendoapiKey: API Key
-
MatomoTagManager—ember-metrics/metrics-adapters/matomo-tag-managermatomoUrl: Matomo URLcontainerId: Container ID, e.g.acbd1234
-
Hotjar—ember-metrics/metrics-adapters/hotjarsiteId: SiteID
-
Adobe Dynamic Tag Management -
Simple Analytics
The metrics service doesn't activate anything on its own — you activate
adapters explicitly by passing adapter classes (not string names) to
metrics.activateAdapters(...). This keeps the addon compatible with
ember-strict-application-resolver and Embroider's static builds: there's no
container lookup by convention, nothing depends on config:environment being
registered on the owner, and only the adapters you actually import end up in
your build.
A good place to do this once is your application route — it runs on boot, before anything renders:
// app/routes/application.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import ENV from 'my-app/config/environment';
import GoogleAnalytics from 'ember-metrics/metrics-adapters/google-analytics';
import Mixpanel from 'ember-metrics/metrics-adapters/mixpanel';
export default class ApplicationRoute extends Route {
@service metrics;
constructor() {
super(...arguments);
// Needed only if you gate adapters per environment (see `environments` below).
this.metrics.appEnvironment = ENV.environment;
this.metrics.activateAdapters([
{
name: 'GoogleAnalytics',
adapter: GoogleAnalytics,
environments: ['development', 'production'],
config: {
id: 'UA-XXXX-Y',
// Use `analytics_debug.js` in development
debug: ENV.environment === 'development',
// Use verbose tracing of GA events
trace: ENV.environment === 'development',
// Ensure development env hits aren't sent to GA
sendHitTask: ENV.environment !== 'development',
// Specify Google Analytics plugins
require: ['ecommerce'],
},
},
{
name: 'Mixpanel',
adapter: Mixpanel,
environments: ['production'],
config: {
token: '0f76c037-4d76-4fce-8a0f-a9a8f89d1453',
},
},
]);
}
}Anywhere that runs once early works — activateAdapters is idempotent. The
application route is a good default because it needs no legacy initializer API
and has clean access to the service.
Each entry in the array has the following shape:
/**
* @param {String} name A label you choose, used to target this
* adapter with `invoke` and the single-adapter
* form of the convenience methods.
* @param {Function} adapter The adapter class itself.
* @param {String[]} environments Environments to activate the adapter in.
* Defaults to `['all']`.
* @param {Object} config Configuration passed to the adapter's
* constructor (see the options for each
* service above).
*/
{
name: 'GoogleAnalytics',
adapter: GoogleAnalytics,
environments: ['all'],
config: {},
}To only activate an adapter in specific environments, list them under
environments and set metrics.appEnvironment (e.g. to ENV.environment)
before calling activateAdapters. Valid values are whatever your app uses for
its environment (commonly development, test, production), plus:
all— the default; the adapter is activated in every environment.
activateAdapters is idempotent: adapters already active under the same name
are not reinstantiated, so it's safe to call it multiple times as more adapters
or config become available (see Activating adapters later).
If you're using ember-cli-content-security-policy,
you'll need to modify the content security policy to allow loading of any remote
scripts. In config/environment.js, add this to the ENV hash (modify as
necessary):
// example for loading Google Analytics
contentSecurityPolicy: {
'default-src': "'none'",
'script-src': "'self' www.google-analytics.com",
'font-src': "'self'",
'connect-src': "'self' www.google-analytics.com",
'img-src': "'self'",
'style-src': "'self'",
'media-src': "'self'"
}Inject the service into any object registered in the container that you wish to
track from. For example, you can call trackPage across all your analytics
services whenever you transition into a route:
// app/routes/application.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service metrics;
@service router;
constructor() {
super(...arguments);
this.router.on('routeDidChange', () => {
const page = this.router.currentURL;
const title = this.router.currentRouteName || 'unknown';
this.metrics.trackPage({ page, title });
});
}
}To only call a single service, pass its name (the label you registered it
with) as the first argument:
// only invokes `trackPage` on the adapter registered as `GoogleAnalytics`
metrics.trackPage('GoogleAnalytics', {
title: 'My Awesome App',
});Often you may want to include information like the current user's name with
every event or page view that's tracked. Any properties set on
metrics.context are merged into the options for every service call.
this.metrics.context.userName = 'Jimbo';
this.metrics.trackPage({ page: 'page/1' }); // { userName: 'Jimbo', page: 'page/1' }There are four main methods, all with the same argument signature. The
optional first argument targets a single adapter by its registered name;
omit it to call every activated adapter.
-
trackPage([name], options)Commonly used to track page views. Because of how single-page apps route, you'll usually call this yourself (e.g. on
routeDidChange) to track page views. -
trackEvent([name], options)A general-purpose method for tracking a named event in your application.
-
identify([name], options)For analytics services that support identifying a user.
-
alias([name], options)For services that implement it, notifies the service that an anonymous user now has a unique identifier.
If an adapter implements a method beyond the four above, call it with invoke.
For example, the Amplitude adapter exposes optOut/optIn:
-
invoke(method, [name], options)// Target a single adapter by passing its name *and* an options object — with // only two arguments the second is treated as options, not a name. metrics.invoke('optOut', 'Amplitude', {});
Adapters that don't implement the method are skipped. The [name] argument may
also be an array to target several adapters at once, e.g.
metrics.invoke('trackEvent', ['GoogleAnalytics', 'Mixpanel'], { ... }).
Because activateAdapters is idempotent, you can call it again at any time —
for example once a dynamic API key becomes available from a model:
// app/routes/application.js
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import GoogleAnalytics from 'ember-metrics/metrics-adapters/google-analytics';
export default class ApplicationRoute extends Route {
@service metrics;
afterModel(model) {
this.metrics.activateAdapters([
{
name: 'GoogleAnalytics',
adapter: GoogleAnalytics,
environments: ['all'],
config: { id: model.googleAnalyticsKey },
},
]);
}
}Adapters already active under the same name won't be reinstantiated, so it's
safe to call this from multiple places as more adapters or config become
available.
An adapter is a class that extends BaseAdapter and implements, at minimum,
install and uninstall:
// app/metrics-adapters/my-adapter.js
import BaseAdapter from 'ember-metrics/metrics-adapters/base';
export default class MyAdapter extends BaseAdapter {
// Human-readable name used in assertions.
toStringExtension() {
return 'MyAdapter';
}
// Called when the adapter is activated. Responsible for injecting the
// provider's script tag and initializing it. Read config off `this.config`.
install() {
const { apiKey } = this.config;
// ...load and initialize the service using `apiKey`
}
// Called when the adapter/service is torn down. Remove the script tag and
// any globals the service created (usually on `window`).
uninstall() {
// ...clean up
}
// Optional — implement the ones your service supports.
identify(options) {}
trackEvent(options) {}
trackPage(options) {}
alias(options) {}
}Then register your class like any bundled adapter:
import MyAdapter from 'my-app/metrics-adapters/my-adapter';
metrics.activateAdapters([
{
name: 'MyAdapter',
adapter: MyAdapter,
environments: ['all'],
config: { apiKey: '29fJs90qnfEa' },
},
]);ember-metrics ships TypeScript types. BaseAdapter, AdapterConfig, and
AdapterOptions are exported from ember-metrics/metrics-adapters/base, so you
can author adapters in TypeScript with full type-checking. The shape of a
activateAdapters entry is exported as AdapterRegistration from
ember-metrics/services/metrics.
If you build an adapter for a service others might use, consider publishing it as a community adapter and opening a PR to list it here.
We're grateful to these wonderful people who've contributed to ember-metrics:
See the Contributing guide for details.
This project is licensed under the MIT License.