Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

754 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ember-metrics

Send data to multiple analytics services without re-implementing a new API

Download count all time npm version CI Ember Observer Score

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 via config/environment. This makes the addon compatible with ember-strict-application-resolver and Embroider's static builds. See Configuration & activation.

Compatibility

  • Ember.js v5.8 or above
  • Embroider or ember-auto-import v2

Installation

ember install ember-metrics

Supported services and options

Each 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.

  1. GoogleAnalyticsember-metrics/metrics-adapters/google-analytics

  2. GoogleAnalyticsFourember-metrics/metrics-adapters/google-analytics-four

    • id: Measurement Id, e.g. G-XXXX
    • options: 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, set send_page_view: false inside 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.

  3. Mixpanelember-metrics/metrics-adapters/mixpanel

  4. GoogleTagManagerember-metrics/metrics-adapters/google-tag-manager

    • id: Container ID, e.g. GTM-XXXX
    • dataLayer: 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&gtm_preview=env-xx&gtm_cookies_win=x';
  5. Segmentember-metrics/metrics-adapters/segment

  6. Piwikember-metrics/metrics-adapters/piwik

  7. Intercomember-metrics/metrics-adapters/intercom

  8. FacebookPixelember-metrics/metrics-adapters/facebook-pixel

    dataProcessingOptions: {
      method: ['LDU'],
      country: 1,
      state: 1000
    }
  9. Amplitudeember-metrics/metrics-adapters/amplitude

  10. AzureAppInsightsember-metrics/metrics-adapters/azure-app-insights

  11. Pendoember-metrics/metrics-adapters/pendo

  12. MatomoTagManagerember-metrics/metrics-adapters/matomo-tag-manager

  13. Hotjarember-metrics/metrics-adapters/hotjar

Community adapters

  1. Adobe Dynamic Tag Management

  2. Simple Analytics

Configuration & activation

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).

Content Security Policy

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'"
}

Usage

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',
});

Context

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' }

Service API

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'], { ... }).

Activating adapters later

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.

Writing your own adapters

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.

Contributors

We're grateful to these wonderful people who've contributed to ember-metrics:

Contributing

See the Contributing guide for details.

License

This project is licensed under the MIT License.

About

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

Topics

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages