/crosslytics

Universal, isomorphic analytics API with pluggable trackers

Primary LanguageTypeScriptApache License 2.0Apache-2.0

Crosslytics

Build Status Coverage Status License

Tired of writing repetitive code to send the same analytics data to all your different tracking services? Or formatting the same data slightly differently for each service, like using eventAction for Google Analytics but event_name for Intercom?

Use Crosslytics for a unified event definition and analytics reporting API: Define events once and report them to all your analytics services with a single call. We've also abstracted the services themselves into a pluggable Tracker architecture so you can use only the services you need and quickly add support for new ones.

Why not use Segment? We didn't want to add another hosted service dependency, so we came up with a light-weight library to send telemetry directly. The meat of Crosslytics lies with its unified datamodels. The models are in fact based on the Segment spec, so usage semantics should actually be very similar.

Usage

Define events

Define events by implementing the TrackedEvent interface. Since the Crosslytics library is isomorphic, you only have to do this once. You can then share the same event definitions between your client and server code:

// events.ts
type DashboardPanelEventArgs = {
  'Panel ID': string,
  'Panel Type'?: number,
  'Panel Name'?: string
};

export class DashboardPanelCreated implements TrackedEvent<DashboardPanelEventArgs> {
  readonly name = 'DashboardPanel Created';
  readonly category = 'Dashboard';
  readonly argPriority: (keyof DashboardPanelEventArgs)[] = [
    'Panel ID',
    'Panel Type',
    'Panel Name'
  ];
  constructor(public args: DashboardPanelEventArgs) {}
}

Note that you don't necessarily have to use classes. You could, for instance, define factories that construct your TrackedEvents:

type DashboardPanelCreated = TrackedEvent<DashboardPanelEventArgs>;
const makeDashboardPanelCreated = (args: DashboardPanelEventArgs): DashboardPanelCreated => {
  return {
    name: 'DashboardPanel Created',
    category: 'Dashboard',
    argPriority: [
        'Panel ID',
        'Panel Type',
        'Panel Name'
    ],
    args
  };
}

Server-side

Node.js with Express

Do a one-time setup of trackers in your application code using Express middleware:

// server.ts
import * as express from 'express';
import { Crosslytics, Identity } from 'crosslytics';
import { GoogleAnalyticsTracker } from 'crosslytics-node-google-analytics-tracker';
// Other trackers as desired, e.g.
// import { IntercomTracker } from 'crosslytics-node-intercom-tracker';

const app = express();
app.use((req: Request, res: Response, next: NextFunction) => {
  // Store a reference to use later
  req.telemetry = new Crosslytics();

  const gaTracker = new GoogleAnalyticsTracker('UA-12345678-9');
  req.telemetry.trackers.set(gaTracker.id, gaTracker);

  // Add more trackers as desired
  // const intercomTracker = new IntercomTracker('token');
  // req.telemetry.trackers.set('token', intercomTracker);

  // Set current user if you know who it is
  req.telemetry.identify({userId: ''});
  next();
});

Now you can report events to all trackers using a single call:

// dashboard.controller.ts
import { DashboardPanelCreated } from './events';
app.route('panel').post((req: Request, res: Response, next: NextFunction) => {
  ... // App stuff
  
  const ev = new DashboardPanelCreated({
    'Panel ID': '123456',
    'Panel Name': 'Test Panel'
  });
  req.telemetry.track(ev); // Wait if you want
});

Client-side

React with Redux

We recommend you track client-side analytics using Redux Actions themselves by adding analytics metadata to your actions. For example, you could include an action.analytics property when you want an action to be tracked. This way, you can setup Redux middleware to automatically report analytics, greatly reducing the amount of tracking code you need to write.

First, create a middleware to perform event tracking based on action metadata:

// middleware.ts
import { Action, Middleware } from 'redux';
import { Crosslytics, TrackedEvent } from 'crosslytics';

// Create a middleware to use with your Redux store
export function analyticsMiddleware(crosslytics: Crosslytics): Middleware {
  return () => next => <T, A extends TrackableAction<T>>(action: A) => {
    if (action.analytics) {
      crosslytics.track(action.analytics);
    }
    return next(action);
  };
}

// Woohoo TypeScript type-safety
interface TrackableAction<T> extends Action { analytics?: TrackedEvent<T> }

Then setup your trackers and add the middleware to your Redux store (also one-time):

// store.ts
import { createStore, applyMiddleware } from 'redux';
import { analyticsMiddleware } from './middleware.ts';
import { Crosslytics } from 'crosslytics';
import { GoogleAnalyticsTracker } from 'crosslytics-browser-google-analytics-tracker';
// Other trackers as desired, e.g.
// import { IntercomTracker } from 'crosslytics-browser-intercom-tracker';

export const crosslytics = new Crosslytics();

const gaTracker = new GoogleAnalyticsTracker('UA-12345678-9');
crosslytics.trackers.set(gaTracker.id, gaTracker);

// Add more trackers as desired
// const intercomTracker = new IntercomTracker('token');
// crosslytics.trackers.set('token', intercomTracker);

let store = createStore(
  reducers, // defined elsewhere
  applyMiddleware(
    analyticsMiddleware(crosslytics)
    // other middleware
  )
);

// If you're using a router, you can also track page views
const history = createHistory(); // your router of choice, e.g. 'react-router-redux'
history.listen(location => crosslytics.page({ url: location.pathname }));

Finally, you can now just add an action.analytics property to your actions and the events will be tracked automatically by the middleware. An example async action creator:

// dashboards.actions.ts

// See server example: this is the same event defintion across client and server
import { DashboardPanelCreated } from './events';

// Action for successful panel creation
export const panelCreated = (panel) => {
  const ev = new DashboardPanelCreated({
    'Panel ID': panel.id,
    'Panel Name': panel.name
  });
  return {
    type: 'PANEL_CREATED', // Could even reuse ev.name here
    payload: panel,
    analytics: ev
  };
};

If you further want to decouple event construction from your action creators, we recommend implementing a factory function to create the events and dependency injecting the factory into your action creators. For example, if you're using redux-thunk, you could inject the factory via the withExtraArgument() method.

Trackers

Existing trackers

Google Analytics

Intercom:

Pendo

Create your own

Use our boilerplate templates: