azukiapp/azk

View on GitHub
src/utils/tracker.js

Summary

Maintainability
A
0 mins
Test Coverage
/* @flow */

import Azk from 'azk';
import { _, config, log, t, lazy_require } from 'azk';
import { meta as azkMeta } from 'azk';
import { promisify, TimeoutError, promiseResolve } from 'azk/utils/promises';
import { deviceInfo } from 'azk/utils';

var lazy = lazy_require({
  uuid         : 'node-uuid',
  InsightKeenIo: 'insight-keen-io',
  InsightKeenIoWithMeta: () => {
    class InsightKeenIoWithMeta extends lazy.InsightKeenIo {
      constructor(opts) {
        super(opts);
      }
    }
    return InsightKeenIoWithMeta;
  }
});

export class TrackerEvent {
  constructor(collection, tracker) {
    this.collection = collection;
    this.tracker    = tracker;
    this._data      = {
      "keen": {
        "addons" : [{
          "name" : "keen:ip_to_geo",
          "input": {
            "ip" : "meta.ip_address"
          },
          "output": "meta.ip_geo_info"
        }],

        // Two time-related properties are included in your
        //  event automatically. The properties “keen.timestamp”
        //  and “keen.created_at” are set at the time your event
        //  is recorded. You have the ability to overwrite the
        //  keen.timestamp property. This could be useful, for example,
        //  if you are backfilling historical data.
        //  Be sure to use ISO-8601 Format.
        //
        //  - keen.io/docs/event-data-modeling/event-data-intro/#id9
        //
        // > (new Date(2011, 10, 11, 9, 11, 11, 111)).toISOString()
        //    '2011-11-11T11:11:11.111Z'
        "timestamp": (new Date()).toISOString(),
      },
      "meta": {}
    };
  }

  final_data() {
    return _.merge({}, this._data, {
      "meta": _.clone(this.tracker.meta)
    });
  }

  addData(data) {
    this._data = _.merge({}, this._data, data);
  }

  send(extra_func = null) {
    if (!this.tracker.loadTrackerPermission()) { return promiseResolve(false); }

    if (_.isFunction(extra_func)) {
      extra_func(this);
    }

    let event_id   = this.tracker.generateRandomId('event_id');
    var final_data = this.final_data();
    this.tracker.logAnalyticsData(event_id, this.collection, final_data);

    // track data with insight
    return this._track(this.collection, final_data)
      .timeout(10000)
      .then((tracking_result) => {
        if (tracking_result !== 0) {
          throw new Error(tracking_result.toString());
        }
        var background = this.tracker.insight.send_in_background;
        log.info('[tracker] event sendend (%s) (send_in_background: %s)', event_id, background);
        return tracking_result;
      })
      .catch(TimeoutError, () => {
        log.warn('[tracker] timeout (%s): %s', event_id, t("tracking.timeout"));
        return false;
      })
      .catch((err) => {
        log.warn('[tracker] error (%s) %s', event_id, err.stack, {});
        return false;
      });
  }

  // Best practice: not connect external promise in your promise system
  _track(collection, final_data) {
    let context = this.tracker.insight;
    let track   = promisify(context.track, { context });
    return track(collection, final_data);
  }
}

export class Tracker {

  constructor(opts, ids_keys) {
    opts = _.merge({}, {
      projectId          : config('tracker:projectId'),
      writeKey           : config('tracker:writeKey'),
      send_in_background : config('tracker:send_in_background')
    }, opts);

    this.ids_keys      = ids_keys;
    this.insight_opts  = opts;
    this.meta          = {
      "ip_address"      : "${keen.ip}",
      "agent_session_id": this.loadAgentSessionId(),
      "command_id"      : this.generateRandomId('command_id'),
      "user_id"         : this.loadTrackerUserId(),
      "azk_version"     : Azk.version,

      // device config
      "device_info": deviceInfo(),
    };

    log.debug(`[tracker] permission to tracker: ${this.loadTrackerPermission()}`);
  }

  get insight() {
    if (!this.__insight) {
      this.__insight = new lazy.InsightKeenIoWithMeta(this.insight_opts);
    }
    return this.__insight;
  }

  newEvent(collection, data = {}) {
    var event = new TrackerEvent(collection, this);
    event.addData(data);
    return event;
  }

  sendEvent(collection, data = {}) {
    var extra_func = null;
    if (_.isFunction(data)) {
      extra_func = data;
      data = {};
    }
    return this.newEvent(collection, data).send(extra_func);
  }

  generateRandomId(label) {
    return label + ':' + lazy.uuid.v1().replace(/-/g, "").slice(0, 15);
  }

  generateNewAgentSessionId() {
    var id = this.generateRandomId(this.ids_keys.agent_id);
    azkMeta.set(this.ids_keys.agent_id, id);
    this.meta.agent_session_id = id;
    return id;
  }

  loadAgentSessionId() {
    return azkMeta.get(this.ids_keys.agent_id);
  }

  loadTrackerUserId() {
    return azkMeta.getOrSet(this.ids_keys.user_id, this.generateRandomId(this.ids_keys.user_id));
  }

  saveTrackerPermission(answer) {
    return azkMeta.set(this.ids_keys.permission, answer);
  }

  loadTrackerPermission() {
    // opt-out: by default can track when terms of use are accepted
    return (config('tracker:disable')) ? false : azkMeta.get(this.ids_keys.permission, true);
  }

  logAnalyticsError(err) {
    log.warn('[tracker] ', err.stack || err.toString());
  }

  logAnalyticsData(event_id, collection, data) {
    log.info('[tracker] sending (%s): %j', event_id, {
      collection: collection,
      event_type: data.event_type,
      meta: {
        agent_session_id: data.meta.agent_session_id,
        command_id: data.meta.command_id,
        user_id: data.meta.user_id,
      }
    }, {});
  }
}

// Default tracker
var default_tracker = new Tracker({
  send_in_background: true,
}, {
  permission: 'tracker_permission',
  user_id   : 'tracker_user_id',
  agent_id  : 'agent_session_id',
});

export default default_tracker;