Source: newton.js

'use strict';


var __newtonUtils = {
  random: {
    chars: (function() {
      var c = 'qwertyuiopasdfghjklzxcvbnm';
      return {
        LOWER: c,
        UPPER: c.toUpperCase(),
        NUMBER: '1234567890',
        SYMBOL: '\\|!£$%&/()=?^\'<>#@-_+*'
      };
    })(),
    string: function(l, alphabet) {
      l = parseInt(l, 10);
      if (!isFinite(l)) {
        return null;
      }
      var chars = __newtonUtils.random.chars;
      alphabet = alphabet || (chars.LOWER + chars.UPPER + chars.NUMBER + chars.SYMBOL);
      var m = new MersenneTwister();

      var r = '';
      for(var i=0; i < l; i++) {
        var index = Math.floor(m.random() * alphabet.length);
        r += alphabet.charAt(index);
      }
      return r;
    }
  },
  http: {
    getQueryString: function(obj) {
      var str = [];
      for(var i in obj) {
        str.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]));
      }
      return str.join('&');
    },
    getAuthHeader: function getAuthHeader(clientIdentifier, secret, method, url, body, query) {
      var timestamp = __newtonUtils.date.getCurrentUTCTimestamp();
      body = body || '';
      if (typeof body === 'object') {
        body = JSON.stringify(body);
      }
      query = query || '';
      if (typeof query === 'object') {
        query = __newtonUtils.http.getQueryString(query);
      }

      var msg = [url, method, body, query, timestamp].join('|');
      var hash = CryptoJS.HmacSHA1(msg, secret);
      return ['iPawn identifier="',
        clientIdentifier,
        '", signature="',
        hash,
        '", version="2.0", timestamp="',
        timestamp,
        '"'
      ].join('');
    },
    doRequest: function doRequest(clientIdentifier, secret, method, url, body, query, callback) {
      var request = new XMLHttpRequest();
      if (!('withCredentials' in request)) {
        return __newtonUtils.http.doRequestuestUsingImage(clientIdentifier, secret, url, body, callback);
      }

      request.onreadystatechange = function() {
        if ((request.readyState == 4) || request.isMockedFromNewton) {
          callback(request);
        }
      };

      var urlToSign = __newtonUtils.http.getUrlForSignature(url);
      request.open(method.toUpperCase(), url, true);
      request.timeout = CONSTANT.http.CONNECTION_TIMEOUT;
      request.setRequestHeader('Authorization', __newtonUtils.http.getAuthHeader(clientIdentifier, secret, method, urlToSign, body, query));
      request.setRequestHeader('Content-type', 'application/json; charset=UTF-8');
      body = (body && (body.constructor !== String)) ? JSON.stringify(body) : body;
      _log('Sending', body, 'body to', url);
      request.send(body);
    },
    getUrlForSignature: function(url) {
      return url.replace(/^https?:\/\//,'');
    },
    doRequestuestUsingImage: function doRequestuestUsingImage(clientIdentifier, secret, url, query, callback) {
      var timestamp = __newtonUtils.date.getCurrentUTCTimestamp();

      query = (query && (query.constructor !== String)) ? JSON.stringify(query) : query;
      query = {
        events: query
      };
      var queryToSign = __newtonUtils.http.getQueryString(query);

      var urlToSign = __newtonUtils.http.getUrlForSignature(url);
      var msg = [urlToSign, 'GET', '', queryToSign, timestamp].join('|');
      var hash = CryptoJS.HmacSHA1(msg, secret) + '';

      query.identifier = clientIdentifier;
      query.signature = hash;
      query.timestamp = timestamp;
      query.version = '2.0';

      var alreadyCalled = false;
      function cb() {
        if (alreadyCalled) { return; }

        alreadyCalled = true;
        callback.call(img, {status: 204});
      }

      var img = document.createElement('img');
      img.onload = cb;
      img.onerror = cb;
      img.src = url + '?' + __newtonUtils.http.getQueryString(query);
      // document.body.appendChild(img);
    }
  },
  cookie: {
    setItem: function(key, value, end, path, domain) {
      if (!key || /^(?:expires|max\-age|path|domain|secure)$/i.test(key)) { return false; }
      var sExpires = "";
      if (end) {
        switch (end.constructor) {
          case Date:
            sExpires = "; expires=" + end.toUTCString();
            break;
        }
      }
      document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) + sExpires + (domain ? "; domain=" + domain : "") + (path ? "; path=" + path : "");
      return true;
    },
    removeItem: function (sKey, sPath, sDomain) {
      document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/;";
      return true;
    },
    getItem: function (key) {
      if (!key) { return null; }
      return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(key).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
    }
  },
  date: {
    getCurrentUTCTimestamp: function() {
      return Math.ceil(__newtonUtils.date.getCurrentUTCDate().getTime() / 1000);
    },
    getCurrentUTCDate: function() {
      var now = new Date();
      return new Date(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),  now.getUTCHours(), now.getUTCMinutes(), now.getUTCSeconds(), now.getUTCMilliseconds());
    },
    diffDate: function(date1, date2) {
      return Math.ceil((date1.getTime() - date2.getTime()) / 1000);
    }
  },
  other: {
    mergeSimpleObject: function(so1, so2) {
      var so = SimpleObject.fromJSONObject(so1.toJSONObject());
      var hash = so2.toJSONObject();
      for (var k in hash) {
        so._set(k, hash[k]);
      }
      return so;
    },
    mergeObjects: function(o1, o2) {
      var ret = {};
      for (var k in o1) {
        ret[k] = o1[k];
      }
      for (k in o2) {
        ret[k] = o2[k];
      }
      return ret;
    }
  }
};

var CONSTANT = {
  localStorage: {
    QUEUED_EVENTS_KEY: 'newton-queued-events',
    SESSION_KEY: 'newton-session',
    FLOW_KEY: 'newton-flow',
    DEVICE_ID: 'newton-device-id'
  },
  cookie: {
    SESSION_KEY: 'newton-session'
  },
  urls: {
    TRACK_BULK_URL: '/events/track_bulk',
    DEBUG_TRACK_BULK_URL: '/debug/events/track_bulk'
  },
  session: {
    EXPIRE_DELTA_IN_MINUTES: 24 * 60, // 1 day
    ID_LENGTH: 12
  },
  events: {
    FLOW_ID_LENGTH: 12,
    DEVICE_ID_LENGHT: 20,
    MIN_FLUSHING_PERIOD: 2 // 2 seconds
  },
  http: {
    CONNECTION_TIMEOUT: 10000
  }
};


function UserManager() {
  this.userId = undefined;
  this.deviceId = undefined;
}

UserManager.prototype.start = function() {
  // TODO: load from localstorage / cookie
  // now start with anonymous user id
  this.getAnonymousUserId();
  this.getUserId();
};

UserManager.prototype.logout = function() {
  this.userId = undefined;
};

UserManager.prototype.isLogged = function() {
  return this.getUserId() !== undefined;
};

UserManager.prototype.getUserId = function() {
  // force to return a copy and a string
  return this.userId === undefined ? undefined : (this.userId + '');
};

UserManager.prototype.setUserId = function(userId) {
  // force to store a string
  this.userId = userId + '';
};

UserManager.prototype.getDeviceId = function() {
  if (this.deviceId) {
    return this.deviceId;
  }
  this.deviceId = localStorage.getItem(CONSTANT.localStorage.DEVICE_ID);
  if (!this.deviceId) {
    this.deviceId = __newtonUtils.random.string(CONSTANT.events.DEVICE_ID_LENGHT);
    localStorage.setItem(CONSTANT.localStorage.DEVICE_ID, this.deviceId);
  }
  return this.deviceId;
};

UserManager.prototype.getAnonymousUserId = function () {
  return 'A_' + this.getDeviceId();
};

UserManager.prototype.getCurrentUserId = function () {
  return this.isLogged() ? this.getUserId() : this.getAnonymousUserId();
};


function SessionManager() {
  this.sessionId = undefined;
  this.eventQueueManager = undefined;
}

SessionManager.prototype.setEventQueueManager = function(eventQueueManager) {
  this.eventQueueManager = eventQueueManager;
};

SessionManager.prototype.start = function() {
  if (this.isValid()) {
    this.sessionId = __newtonUtils.cookie.getItem(CONSTANT.cookie.SESSION_KEY);
    return;
  }

  this.sessionId = __newtonUtils.random.string(CONSTANT.session.ID_LENGTH);
  localStorage.setItem(CONSTANT.localStorage.SESSION_KEY, this.sessionId);

  var date = __newtonUtils.date.getCurrentUTCDate();
  date.setMinutes(date.getMinutes() + CONSTANT.session.EXPIRE_DELTA_IN_MINUTES);
  __newtonUtils.cookie.setItem(CONSTANT.cookie.SESSION_KEY, this.sessionId, date, '/');

  // emit start event
  if (this.eventQueueManager) {
    this.eventQueueManager.startSessionEvent(this.sessionId);
  }
};

SessionManager.prototype.isValid = function() {
  // start session
  return !!__newtonUtils.cookie.getItem(CONSTANT.cookie.SESSION_KEY);
};

SessionManager.prototype.stop = function() {
  this.sessionId = undefined;
  localStorage.removeItem(CONSTANT.localStorage.SESSION_KEY);
  __newtonUtils.cookie.removeItem(CONSTANT.cookie.SESSION_KEY);
};

SessionManager.prototype.getSessionId = function() {
  // refresh internal sessionId if needed
  this.start();

  return this.sessionId;
};


function EventQueueManager() {
  this.userManager = undefined;
  this.sessionManager = undefined;
  this.lastFlushTimestamp = 0;
  this.queuedEvents = [];

  this.intervalId = undefined;
}

EventQueueManager.prototype.setUserManager = function(userManager) {
  this.userManager = userManager;
};

EventQueueManager.prototype.setSessionManager = function(sessionManager) {
  this.sessionManager = sessionManager;
};

EventQueueManager.prototype.setOnFlushEvents = function(onFlushEvents) {
  this.onFlushEvents = onFlushEvents;
};

EventQueueManager.prototype.inflatingEvent = function(event) {
  event.session_id = this.sessionManager.getSessionId();
  event.creation_date = __newtonUtils.date.getCurrentUTCDate().toISOString();
  event.user_id = this.userManager.getCurrentUserId();
  return event;
};

EventQueueManager.prototype.addEvent = function(event) {
  this.inflatingEvent(event);

  var events = localStorage.getItem(CONSTANT.localStorage.QUEUED_EVENTS_KEY);
  events = events ? events : '';
  events += ',' + JSON.stringify(event);
  localStorage.setItem(CONSTANT.localStorage.QUEUED_EVENTS_KEY, events);

  var now = __newtonUtils.date.getCurrentUTCTimestamp();
  if (now < this.lastFlushTimestamp + CONSTANT.events.MIN_FLUSHING_PERIOD) {
    // queued and not flushed
    if (!this.timeoutId) {
      var self = this;
      this.timeoutId = setTimeout(function() {
        self.timeoutId = undefined;

        var events = localStorage.getItem(CONSTANT.localStorage.QUEUED_EVENTS_KEY);
        if (!events) return;
        localStorage.removeItem(CONSTANT.localStorage.QUEUED_EVENTS_KEY);

        self.onFlushEvents('[' + events.substring(1) + ']');
      }, CONSTANT.events.MIN_FLUSHING_PERIOD * 1000);
    }
    return;
  }

  // throw ???
  if (!this.onFlushEvents) { return; }

  this.lastFlushTimestamp = now;
  localStorage.removeItem(CONSTANT.localStorage.QUEUED_EVENTS_KEY);
  this.onFlushEvents('[' + events.substring(1) + ']');
};

EventQueueManager.prototype.startSessionEvent = function(sessionId) {
  var event = getDeviceField();
  event.event_type = 'start session';
  event.device_id = this.userManager.getDeviceId();
  event.sdk_version = 'JS ' + publicInterface.getVersionString();
  event.user_agent = window.navigator.userAgent;

  this.addEvent(event);
};

function ServerClientManager(credentials, url) {
  this.credentials = credentials;
  this.url = url;
}


ServerClientManager.prototype.flushEvents = function(events, callback) {
  callback = callback || function() {};

  __newtonUtils.http.doRequest(
    this.credentials.identifier,
    this.credentials.secret,
    'POST',
    this.url,
    events,
    '',
    callback
  );
};

function TimerManager() {
  this.timers = {};
  this.eventQueueManager = undefined;
}

TimerManager.prototype.setEventQueueManager = function(eventQueueManager) {
  this.eventQueueManager = eventQueueManager;
};

TimerManager.prototype.startTimer = function(name, cd) {
  this.timers[name] = {
    name: name,
    startDate: __newtonUtils.date.getCurrentUTCTimestamp()
  };
  if (cd) {
    this.timers[name].custom_data = cd;
  }
};

TimerManager.prototype.stopTimer = function(name, cd) {
  var timedEvent = this.timers[name];
  if (!timedEvent) {
    throw new Error('Cannot stop a non started timedEvent');
  }

  timedEvent.end = __newtonUtils.date.getCurrentUTCDate().toISOString();
  timedEvent.delta = Math.abs(__newtonUtils.date.getCurrentUTCTimestamp() - timedEvent.startDate);
  if (cd) {
    timedEvent.custom_data = __newtonUtils.other.mergeObjects(timedEvent.custom_data, cd);
  }
  timedEvent.event_type = 'timer stop';


  this.timers[name + ''] = undefined;

  if (this.eventQueueManager) {
    this.eventQueueManager.addEvent(timedEvent);
  } // TODO: else ???
};

function FlowManager() {
  this.eventQueueManager = undefined;
}

FlowManager.prototype.setEventQueueManager = function(eventQueueManager) {
  this.eventQueueManager = eventQueueManager;
};

FlowManager.prototype.flowBegin = function(name, cd) {
  var flowId = localStorage.getItem(CONSTANT.localStorage.FLOW_KEY);
  if (flowId) {
    this.flowFail('flow-start', {'started-flow-id': flowId});
  }
  flowId = __newtonUtils.random.string(CONSTANT.events.FLOW_ID_LENGTH);
  localStorage.setItem(CONSTANT.localStorage.FLOW_KEY, flowId);

  var ev = {
    event_type: 'flow start',
    name: name + '',
    flow_id: flowId
  };
  if (cd) {
    ev.custom_data = cd;
  }
  this.eventQueueManager.addEvent(ev);
};

FlowManager.prototype.flowStep = function(name, cd) {
  var flowId = localStorage.getItem(CONSTANT.localStorage.FLOW_KEY);
  if (!flowId) {
    throw new Error('No flow found');
  }
  var ev = {
    event_type: 'flow step',
    flow_id: flowId,
    name: name
  };
  if (cd) {
    ev.custom_data = cd;
  }

  this.eventQueueManager.addEvent(ev);
};

FlowManager.prototype.flowSucceed = function(reason, cd) {
  var flowId = localStorage.getItem(CONSTANT.localStorage.FLOW_KEY);
  if (!flowId) {
    throw new Error('No flow found');
  }
  var ev = {
    event_type: 'flow succeeded',
    flow_id: flowId,
    name: reason ? reason + '' : 'ok'
  };
  if (cd) {
    ev.custom_data = cd;
  }

  this.eventQueueManager.addEvent(ev);
};

FlowManager.prototype.flowFail = function(reason, cd) {
  var flowId = localStorage.getItem(CONSTANT.localStorage.FLOW_KEY);
  if (!flowId) {
    throw new Error('No flow found');
  }
  localStorage.removeItem(CONSTANT.localStorage.FLOW_KEY);
  var ev = {
    event_type: 'flow failed',
    flow_id: flowId,
    name: reason
  };
  if (cd) {
    ev.custom_data = cd;
  }

  this.eventQueueManager.addEvent(ev);
};

FlowManager.prototype.flowCancel = function(reason, cd) {
  var flowId = localStorage.getItem(CONSTANT.localStorage.FLOW_KEY);
  if (!flowId) {
    throw new Error('No flow found');
  }
  localStorage.removeItem(CONSTANT.localStorage.FLOW_KEY);
  var ev = {
    event_type: 'flow canceled',
    flow_id: flowId,
    name: reason
  };
  if (cd) {
    ev.custom_data = cd;
  }
  this.eventQueueManager.addEvent(ev);
};

/*
 * UseSetTransaction
 */
function createUserSetTransaction() {
  return {
    setUserProperty: function(key, value) {

    },
    setOnceUserProperty: function(key, value) {

    },
    incrementUserProperty: function(key) {

    },
    decrementUserProperty: function(key) {

    },
    commit: function() {

    }
  };
}

/**
 * @class
 */
function SimpleObject() {
  this.prop = {};
}
/**
 * Never use directly. Use the alias instead
 * @method
 * @memberOf SimpleObject
 *
 * @param {String} key - The key
 * @param {String|null|Number|Boolean} value - The value
 */
SimpleObject.prototype._set = function (key, value) {
  // TODO: do this better. instance only when is needed
  var e = new Error('cannot set ' + value + ' as ' + key);
  e.key = key;
  e.value = value;

  if (value === undefined) {
    throw e;
  }
  if (value !== null) {
    var valueConstructor = value.constructor;
    if (valueConstructor !== Number && valueConstructor !== String && valueConstructor !== Boolean) {
      throw e;
    }
  }
  this.prop[key] = value;
};
/**
 * @method setString
 * @memberOf SimpleObject
 * @instance
 * @alias SimpleObject#_set
 */
SimpleObject.prototype.setString = SimpleObject.prototype._set;
/**
 * @method setInt
 * @memberOf SimpleObject
 * @instance
 * @alias SimpleObject#_set
 */
SimpleObject.prototype.setInt = SimpleObject.prototype._set;
/**
 * @method setBool
 * @memberOf SimpleObject
 * @instance
 * @alias SimpleObject#_set
 */
SimpleObject.prototype.setBool = SimpleObject.prototype._set;
/**
 * @method setFloat
 * @memberOf SimpleObject
 * @instance
 * @alias SimpleObject#_set
 */
SimpleObject.prototype.setFloat = SimpleObject.prototype._set;
/**
 * @method setNull
 * @memberOf SimpleObject
 * @instance
 *
 * @param {String} key - The key
 */
SimpleObject.prototype.setNull = function(key) {
  this._set(key, null);
};
/**
 * @method toJSONObject
 * @memberOf SimpleObject
 * @instance
 *
 * @return {Object} - The object stored in this SimpleObject
 */
SimpleObject.prototype.toJSONObject = function() {
  var a = {};
  for (var k in this.prop) {
    a[k] = this.prop[k];
  }
  return a;
};
/**
 * @method fromJSONObject
 * @memberOf SimpleObject
 * @static
 *
 * @param {Object} obj - The object you would convert
 * @return {SimpleObject}
 */
SimpleObject.fromJSONObject = function(obj) {
  if (!obj || obj.constructor !== Object) {
    var e = new Error('obj is not a simple object compatible');
    e.obj = obj;
    throw e;
  }

  var so = new SimpleObject();
  for (var key in obj) {
    so._set(key, obj[key]);
  }
  return so;
};

function assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData) {
  if (customData && customData.constructor !== SimpleObject) {
    throw new Error('Custom data should be an instance of SimpleObject');
  }
}
function getHashFromSimpleObjectOrUndefined(customData) {
  return customData ? customData.toJSONObject() : undefined;
}
function assertIsALabel(name) {
  /* jshint -W044 */
  name = name + '';
  if (!(name && name.match(/^[\w-]{1,20}$/))) {
    throw new Error('label should be match /^[\w-]{1,20}$/');
  }
}


function getIdentifier() {
  return window.location.hostname + '|JS';
}

function getEndpoint(hostname, secret) {
  return hostname.match(/^static2\.newton\.pm$/) ?
    'https://client-api2.newton.pm' + CONSTANT.urls.TRACK_BULK_URL :
      (secret.match('_') ?
            'https://client-api-sandbox.newton.pm' + CONSTANT.urls.DEBUG_TRACK_BULK_URL :
            'https://client-api.newton.pm' + CONSTANT.urls.TRACK_BULK_URL);
}

var _log;

/**
 * NewtonSDK
 *
 * @class NewtonSDK
 * @name NewtonSDK
 * @constructor
 * @private
 */
function createNewtonInstance(secret) {
  var debug = false;
  _log = function() {
    if (debug) { console.log(arguments); }
  };

  var ravenUrl;
  secret = secret || {};
  if (secret.constructor === Object ) {
    ravenUrl = secret.ravenUrl;
    secret = secret.secret;
  }

  if (!secret) {
    throw new Error('secret should be given');
  }

  var identifier = getIdentifier();
  var endpoint = getEndpoint(window.location.hostname, secret);

  var userManager = new UserManager();
  var sessionManager = new SessionManager();
  var eventQueueManager = new EventQueueManager();
  var serverClientManager = new ServerClientManager(
    {identifier: identifier, secret: secret},
    endpoint
  );
  var timerManager = new TimerManager();
  var flowManager = new FlowManager();

  sessionManager.setEventQueueManager(eventQueueManager);
  eventQueueManager.setSessionManager(sessionManager);
  eventQueueManager.setUserManager(userManager);
  eventQueueManager.setOnFlushEvents(function(events) {
    serverClientManager.flushEvents(events);
  });
  timerManager.setEventQueueManager(eventQueueManager);
  flowManager.setEventQueueManager(eventQueueManager);

  var isRavenConfigured = false;
  function setRavenUrl(ravenUrl) {
    if (!Raven) {
      throw new Error('Raven should be included');
    }

    isRavenConfigured = true;
    Raven.config(ravenUrl).install();
  }
  if (ravenUrl) {
    setRavenUrl(ravenUrl);
  }

  sessionManager.start();
  userManager.start();

  return {
    logInfo: function(line) { },
    logNotice: function(line) { },
    logWarning: function(line) { },
    logError: function(line) { },
    makeABTestDecision: function(name, defaultDecision, params, callback) { },
    ABTestGoalAchived: function(abTestId) { },
    IAPValidateTransaction: function(params, receip, callback) { },
    logDebug: function(line) { },
    startSet: function() {
      return createUserSetTransaction();
    },
    userLogout: function() {
      if (userManager.isLogged()) {
        eventQueueManager.addEvent({
          event_type: 'logout'
        });
      }
      userManager.logout();
    },
    isLoginFlowRunning: function() {
      return false;
    },
    /**
     * Login utilities
     * @class LoginBuilder
     * @name LoginBuilder
     * @private
     */
    /**
     * Custom Login
     * @class CustomLoginFlow
     * @name CustomLoginFlow
     * @private
     */
    /**
     * Return a login builder
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @return LoginBuilder
     */
    getLoginBuilder: function() {
      var props = {};
      function setWrapper(c) {
        return function(k) {
          props[c] = k;
          return this;
        };
      }

      return {
        /**
         * Set callback
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param callback
         *
         * @return {LoginBuilder} the same instance
         */
        setCallback: setWrapper('callback'),
        /**
         * Set login data. Useful to track campaign parameters
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {SimpleObject} loginData - the custom data for the login event
         *
         * @return {LoginBuilder} the same instance
         */
        setLoginData: setWrapper('custom_data'),
        /**
         * Set custom id. Used in custom flow.
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {string} customID - custom user id
         *
         * @return {LoginBuilder} the same instance
         */
        setCustomID: setWrapper('customId'),
        /**
         * Create a custom login flow.
         * @method
         * @instance
         * @memberOf LoginBuilder
         *
         * @return {CustomLoginFlow}
         */
        getCustomFlow: function() {
          if (!props.callback || !props.customId) {
            throw new Error('callback and customId must be provided');
          }
          assertCustomDataIsNullOrIsInstanceOfSimpleObject(props.custom_data);
          return {
            /**
             * Return a string representation
             * @method
             * @instance
             * @memberOf CustomLoginFlow
             *
             * @return {string}
             */
            toString : function() {
              return 'CustomLoginFlow(' + JSON.stringify(props) + ')';
            },
            /**
             * Start the custom login flow
             * @method
             * @instance
             * @memberOf CustomLoginFlow
             *
             */
            startLoginFlow: function() {
              if (userManager.isLogged()) {
                throw new Error('User already logged in');
              }
              userManager.setUserId(props.customId);

              var ev = {
                event_type: 'identify',
                login_type: 'CUSTOM'
              };
              if (props.custom_data) {
                ev.custom_data = props.custom_data.toJSONObject();
              }
              eventQueueManager.addEvent(ev);
              setTimeout(props.callback, 0);
            }
          };
        }
      };
    },
    /**
     * Login through userId. No check is made.
     * @method
     * @instance
     * @deprecated This method will throw an exception when the real implementation is done
     * @memberOf NewtonSDK
     *
     * @param {String} userId - The user id or null to logout
     */
    __temporaryCustomLogin: function(_userId) {
      if (userManager.isLogged()) {
        throw new Error('User already logged in');
      }

      userManager.setUserId(_userId);

      eventQueueManager.addEvent({
        event_type: 'identify',
        login_type: 'CUSTOM'
      });
    },
    /**
     * Return true if Newton has a set userId
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @return {Boolean} - true if the userId is set
     */
    isUserLogged: function() {
      return userManager.isLogged();
    },
    /**
     * This callback is used on NewtonSDK#flushEvents
     * @callback NewtonSDK~flushEventsCallback
     * @param {Error} [err] - If something was wrong
     */
    /**
     * Flush event queue
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @deprecated
     *
     * @param {NewtonSDK~flushEventsCallback} callback - The callback called when the request is done
     */
    flushEvents: function(callback) {
      callback = callback || (function() {});
      callback();
    },
    /**
     * Log exception to Sentry if is configured
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's not a begun flow
     *
     * @param {Error} error - The error
     */
    logException: function(error) {
      if (isRavenConfigured) {
        Raven.captureException(error);
      } else {
        console.log('Raven is not configured yet.', error);
      }
    },
    /**
     * End a flow with a cancellation
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's not a begun flow
     *
     * @param {String} name - The reason of the cancellation
     * @param {SimpleObject} [customData] - Some data
     */
    flowCancel: function(reason, customData) {
      _log('flow cancel', arguments);
      assertIsALabel(reason);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      flowManager.flowCancel(reason + '', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * End a flow with a failure
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's not a begun flow
     *
     * @param {String} name - The reason of the failure
     * @param {SimpleObject} [customData] - Some data
     */
    flowFail: function(reason, customData) {
      _log('flow fails', arguments);
      assertIsALabel(reason);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      flowManager.flowFail(reason +'', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * End a flow with a success
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's not a begun flow
     *
     * @param {String} [name="ok"] - A name that identify the end
     * @param {SimpleObject} [customData] - Some data
     */
    flowSucceed: function(reason, customData) {
      _log('flow succeeds', arguments);
      if (!reason || reason.constructor === SimpleObject) {
        customData = reason;
        reason = 'ok';
      }
      assertIsALabel(reason);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      var cd = getHashFromSimpleObjectOrUndefined(customData);
      flowManager.flowSucceed(reason + '', cd);
    },
    /**
     * Make a step for a flow
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's not a begun flow
     *
     * @param {String} name - The name of the step
     * @param {SimpleObject} [customData] - Some data
     */
    flowStep: function(stepName, customData) {
      _log('flow step', arguments);
      assertIsALabel(stepName);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      flowManager.flowStep(stepName + '', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * Begin a flow. Close a previous flow with a fail if need.
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @param {String} name - The name of the flow
     * @param {SimpleObject} [customData] - Some data
     */
    flowBegin: function(name, customData) {
      _log('flow begin', arguments);
      assertIsALabel(name);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      flowManager.flowBegin(name + '', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * Stop a timed event.
     * The event has a merge of this customData and the customData passed on timedEventStart method
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @throws Throw if there's no started event the given name
     *
     * @param {String} name - The name of this event
     * @param {SimpleObject} [customData] - Some data
     */
    timedEventStop: function(name, customData) {
      _log('stop timed event', arguments);
      assertIsALabel(name);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      timerManager.stopTimer(name + '', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * Start a timed event
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @param {String} name - The name of the timed event
     * @param {SimpleObject} [customData] - Some data
     */
    timedEventStart: function(name, customData) {
      _log('start timed event', arguments);
      assertIsALabel(name);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);

      timerManager.startTimer(name + '', getHashFromSimpleObjectOrUndefined(customData));
    },
    /**
     * Queue an event
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @param {String} name - The name of this event
     * @param {SimpleObject} [customData] - Some data attached to this event
     */
    sendEvent: function(name, customData) {
      _log('add event', arguments);
      assertIsALabel(name);
      assertCustomDataIsNullOrIsInstanceOfSimpleObject(customData);
      var ev = {
        event_type: 'custom event',
        name: name + ''
      };
      var cd = getHashFromSimpleObjectOrUndefined(customData);
      if (cd) {
        ev.custom_data = cd;
      }
      eventQueueManager.addEvent(ev);
    },
    setDebug: function(_debug) {
      debug = !!_debug;
    }
  };
}

var newtonInstance;
/**
 * The Newton SDK public Interface
 * @namespace Newton
 */
var publicInterface = {
  /**
   * Get the shared instance
   * @throws {Error} - Throw if getSharedInstanceWithConfig hasn't been called
   * @memberOf Newton
   * @return {NewtonSDK}
   */
  getSharedInstance: function() {
    if (!newtonInstance) {
      throw new Error('Call getSharedInstanceWithConfig before');
    }
    return newtonInstance;
  },
  /**
   * Get the shared instance. Create one if no sharedInstance found
   * @memberOf Newton
   * @return {NewtonSDK}
   */
  getSharedInstanceWithConfig: function(conf_or_secret) {
    if (!newtonInstance) {
      newtonInstance = createNewtonInstance(conf_or_secret);
    }
    return newtonInstance;
  },
  /**
   * SimpleObject
   * @memberOf Newton
   * @alias SimpleObject
   */
  SimpleObject: SimpleObject,
  /**
   * Get current Newton version
   * @memberOf Newton
   * @return {String}
   */
  getVersionString: function getVersionString() {
    return [
      autorevision.VCS_TAG,
      autorevision.VCS_SHORT_HASH,
      autorevision.VCS_DATE
    ].join(' ');
  }
};