Source: newton.js

'use strict';

var jsonpCallbacks = { };

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: {
    isXMLHttpRequestSupported: function() {
      return !!window.XMLHttpRequest;
    },
    isXMLHttpRequestWithCORSSupported: function() {
      var request = new XMLHttpRequest();
      return 'withCredentials' in request;
    },
    isXDomainRequestSupported: function() {
      return !!window.XDomainRequest;
    },
    makeAsyncRequest: function(url, method, body, query, headers, callback) {
      var request = new XMLHttpRequest();

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

      if (query) {
        url += (url.indexOf('?') > -1 ? '&' : '?') + query;
      }

      request.open(method.toUpperCase(), url, true);
      request.timeout = CONNECTION_TIMEOUT;
      for (var k in headers) {
        request.setRequestHeader(k, headers[k]);
      }
      request.setRequestHeader('Content-type', 'application/json; charset=UTF-8');
      _log('Sending', body, 'body to', url);
      request.send(body);
    },
    makeAsyncRequestWithJSONP: function(url, query, callback) {
      var scriptTag = document.createElement('script');
      scriptTag.src = url + '?' + query;
      document.body.appendChild(scriptTag);
    },
    getQueryString: function(obj) {
      var str = [];
      for(var i in obj) {
        if (obj.hasOwnProperty(i)) {
          str.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i]));
        }
      }
      return str.join('&');
    },
    getIpawnHash: function(secret, method, url, body, query, timestamp) {
      url = __newtonUtils.http.getUrlForSignature(url);
      var msg = [url, method, body, query, timestamp].join('|');
      return CryptoJS.HmacSHA1(msg, secret).toString();
    },
    getAuthHeader: function getAuthHeader(clientIdentifier, secret, method, url, body, query) {
      var timestamp = __newtonUtils.date.getCurrentUTCTimestamp();
      return ['iPawn identifier="',
        clientIdentifier,
        '", signature="',
        __newtonUtils.http.getIpawnHash(secret, method, url, body, query, timestamp),
        '", version="2.0", timestamp="',
        timestamp,
        '"'
      ].join('');
    },
    getUrlForSignature: function(url) {
      return url.replace(/^https?:\/\//,'');
    }
  },
  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(JSON.stringify(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; }
      // key = encodeURIComponent(key);
      var cookies = document.cookie.split(";");
      for (var i = 0; i < cookies.length; i++) {
        var splitted = cookies[i].split("=");
        var name = (splitted.shift() || '').trim();
        if (name === key) {
          var value = splitted.shift();
          try {
            return JSON.parse(decodeURIComponent(value));
          } catch(e) { }
        }
      }
      return null;
    }
  },
  localStorage: {
    getItem: function(k) {
      try {
        return JSON.parse(localStorage.getItem(k));
      } catch(e) {
        return null;
      }
    },
    setItem: function(k, v) { return localStorage.setItem(k, JSON.stringify(v)); },
    removeItem: function(k) { return localStorage.removeItem(k); }
  },
  date: {
    getCurrentUTCTimestamp: function() {
      return Math.ceil(Date.now() / 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 isUsingNativeCookies = navigator.cookieEnabled && !__newton__forceCookieInRam;
if (!isUsingNativeCookies) {
  __newtonUtils.cookie = createRamStorage();
}

var isUsingNativeLocalStorage = !__newton__forceLocalStorageInRam;
try {
  localStorage.setItem('newton-test-lc', 1);
  localStorage.removeItem('newton-test-lc');
} catch (e) {
  isUsingNativeLocalStorage = false;
}

function createRamStorage() {
  var __storage = {};
  return {
    setItem: function(key, value) {
      __storage[key] = value;
    },
    removeItem: function (key) {
      delete __storage[key];
    },
    getItem: function (key) {
      return __storage[key] || null;
    }
  };
}

if (!isUsingNativeLocalStorage) {
  __newtonUtils.localStorage = createRamStorage();
}

function getPersistentStorage() {
  return isUsingNativeLocalStorage ? __newtonUtils.localStorage :
    (isUsingNativeCookies ? __newtonUtils.cookie : null);
}

var QUEUED_EVENTS_KEY = 'newton-queued-events';
var FLOW_KEY = 'newton-flow';
var DEVICE_ID = 'newton-device-id';
var SESSION_KEY = 'newton-session';
var OAUTH_LOGIN_PROVIDER = 'newton-login-oauth-provider';
var CUSTOM_LOGIN_KEY = 'newton-custom-login';
var NEWTON_LOGIN_KEY = 'newton-newton-login';
var USER_LOGIN_KEY = 'newton-user-login';
var EXPIRE_DELTA_IN_MINUTES = 24 * 60; // 1 day
var ID_LENGTH = 12;
var FLOW_ID_LENGTH = 12;
var DEVICE_ID_LENGHT = 20;
var MIN_FLUSHING_PERIOD = 2; // 2 seconds
var HEARTBEAT_PERIOD = 10; // 10 seconds
var CONNECTION_TIMEOUT = 10000;
var USER_TOKEN_EXPIRE_IN_DAYS = 30;
var NEWTON_TOKEN_EXPIRE_IN_MINUTES = 30;

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

  UserManager.prototype.loadFromCookie = function() {
    this.customUserToken = __newtonUtils.cookie.getItem(CUSTOM_LOGIN_KEY) || undefined;
  };

  UserManager.prototype.setCustomToken = function(token, isPersistentOnRefreshing) {
    if (!/^C_/.test(token)) {
      throw new Error('Invalid custom token');
    }
    this.customUserToken = token;
    if (isPersistentOnRefreshing) {
      __newtonUtils.cookie.setItem(CUSTOM_LOGIN_KEY, this.customUserToken, null, '/');
    }
  };

  UserManager.prototype.setNewtonToken = function(token) {
    if (!/^N_/.test(token)) {
      throw new Error('Invalid newton token');
    }
    var expireDate = new Date();
    expireDate.setMinutes(expireDate.getMinutes() + NEWTON_TOKEN_EXPIRE_IN_MINUTES);
    __newtonUtils.cookie.setItem(NEWTON_LOGIN_KEY, token, expireDate, '/');
  };

  UserManager.prototype.setUserObjectToken = function(token) {
    if (!/^U_/.test(token)) {
      throw new Error('Invalid user object token');
    }
    var expireDate = new Date();
    expireDate.setDate(expireDate.getDate() + USER_TOKEN_EXPIRE_IN_DAYS);
    __newtonUtils.cookie.setItem(USER_LOGIN_KEY, token, expireDate, '/');
  };

  UserManager.prototype.logout = function() {
    this.customUserToken = null;
    __newtonUtils.cookie.removeItem(CUSTOM_LOGIN_KEY);
    __newtonUtils.cookie.removeItem(NEWTON_LOGIN_KEY);
    __newtonUtils.cookie.removeItem(USER_LOGIN_KEY);
  };

  UserManager.prototype.isUserLogged = function() {
    return !!this.customUserToken ||
      !!__newtonUtils.cookie.getItem(NEWTON_LOGIN_KEY) ||
      !!__newtonUtils.cookie.getItem(USER_LOGIN_KEY);
  };

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

  UserManager.prototype.getDeviceId = function() {
    if (this.deviceId) {
      return this.deviceId;
    }
    this.createNewDeviceIdAndStore();
    return this.deviceId;
  };

  UserManager.prototype.getCurrentUserToken = function() {
    if (this.customUserToken) {
      return this.customUserToken;
    }
    var newtonUserId = __newtonUtils.cookie.getItem(NEWTON_LOGIN_KEY);
    if (newtonUserId) {
      return newtonUserId;
    }
    var userUserId = __newtonUtils.cookie.getItem(USER_LOGIN_KEY);
    if (userUserId) {
      return userUserId;
    }
    return 'A_' + this.getDeviceId();
  };

  UserManager.prototype.setEventQueueManager = function(eventQueueManager) {
    this.eventQueueManager = eventQueueManager;
  };
  UserManager.prototype.setServerClientManager = function(serverClientManager) {
    this.serverClientManager = serverClientManager;
  };

  UserManager.prototype.startAutologinFlow = function(credentials, callback) {
    callback = callback || function() {};
    var url = getAuthEndpoint(credentials.secret) + '/token/refresh';
    var bhandle = (__newtonUtils.cookie.getItem(USER_LOGIN_KEY) || '');
    var content = { bhandle: bhandle };

    if (!bhandle) {
      return callback(null, false);
    }

    __newtonUtils.cookie.removeItem(NEWTON_LOGIN_KEY);

    var self = this;
    this.serverClientManager.refreshUserToken(bhandle, function(err, data) {
      if (err) {
        _log(err);
        self.eventQueueManager.addEvent({
            event_type: 'logout'
          });
        self.logout();
        return callback(null, false);
      }

      self.setNewtonToken(data.newton_token);
      self.setUserObjectToken(data.user_token);

      self.eventQueueManager.addEvent({
        event_type: 'identify',
        login_type: 'autologin'
      });
      // Make sure the cookie is written
      setTimeout(function() {
        callback(null, true);
      }, 10);
    });
  };

  UserManager.prototype.start = function(credentials, callback) {
    this.loadFromCookie();
    var token = this.getCurrentUserToken();
    if (/^U_/.test(token)) {
      this.startAutologinFlow(credentials, callback);
    }
  };

  return UserManager;
})();

var SessionManager = (function() {
  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(SESSION_KEY);
      return;
    }

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

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

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

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

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

  SessionManager.prototype.getSessionId = function() {
    // cookie can be disabilited, so avoiding some recursion, I must do this check
    if (this.sessionId) {
      return this.sessionId;
    }
    // refresh internal sessionId if needed
    this.start();

    return this.sessionId;
  };

  return SessionManager;
})();

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

    this.intervalId = undefined;
    this.onFlushEvents = undefined;
    this.requestAutologinCallback = 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.setRequestAutologinCallback = function(requestAutologinCallback) {
    this.requestAutologinCallback = requestAutologinCallback;
  };

  EventQueueManager.prototype.inflatingEvent = function(event) {
    event.session_id = this.sessionManager.getSessionId();
    event.creation_date = __newtonUtils.date.getCurrentUTCDate().toISOString();
    event.user_id = this.userManager.getCurrentUserToken();
    if (/^U_/.test(event.user_id)) {
      this.requestAutologinCallback();
    }
    return event;
  };

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

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

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

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

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

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

    this.lastFlushTimestamp = now;
    __newtonUtils.localStorage.removeItem(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;
    event.with_cookie = isUsingNativeCookies;
    event.with_ls = isUsingNativeLocalStorage;

    this.addEvent(event);
  };

  return EventQueueManager;
})();

var HttpWrapper = (function() {
  function HttpWrapper() {
    this.credentials = {};
  }

  HttpWrapper.prototype.isToSignWithIPawn = function(credentials) {
    this.credentials = credentials;
    return this;
  };

  HttpWrapper.prototype.makeRequestWithAResponse = function(url, content, callback) {
    var timestamp = __newtonUtils.date.getCurrentUTCTimestamp();
    if (__newtonUtils.http.isXMLHttpRequestSupported() && __newtonUtils.http.isXMLHttpRequestWithCORSSupported()) {
      var headers = { };
      content = JSON.stringify(content);
      if (Object.keys(this.credentials).length !== 0) {
        headers = {
          Authorization: __newtonUtils.http.getAuthHeader(this.credentials.identifier, this.credentials.secret, 'POST', url, content, '', timestamp)
        };
      }
      __newtonUtils.http.makeAsyncRequest(url, 'POST', content, '', headers, callback);
      return;
    } else {
      // jsonp
      var callbackId = __newtonUtils.random.string(5);
      jsonpCallbacks[callbackId] = callback;

      var query = content;
      query.callback = 'Newton.__jsonpCallback';
      query.callbackId = callbackId;

      if (Object.keys(this.credentials).length !== 0) {
        var hash = __newtonUtils.http.getIpawnHash(this.credentials.secret, 'GET', url, '', __newtonUtils.http.getQueryString(query), timestamp);
        query.identifier = this.credentials.identifier;
        query.signature = hash;
        query.timestamp = timestamp;
        query.version = '2.0';
      }

      query = __newtonUtils.http.getQueryString(query);

      __newtonUtils.http.makeAsyncRequestWithJSONP(url, query, callback);

      return;
    }
  };

  HttpWrapper.prototype.makeRequestWithoutAResponse = function(url, content, callback) {
    var timestamp = __newtonUtils.date.getCurrentUTCTimestamp();
    if (__newtonUtils.http.isXMLHttpRequestSupported() && __newtonUtils.http.isXMLHttpRequestWithCORSSupported()) {
      var headers = { };
      if (Object.keys(this.credentials).length !== 0) {
        headers = {
          Authorization: __newtonUtils.http.getAuthHeader(this.credentials.identifier, this.credentials.secret, 'POST', url, content, '', timestamp)
        };
      }
      __newtonUtils.http.makeAsyncRequest(url, 'POST', content, '', headers, callback);
      return;
    }

    var query = {
      events: content
    };

    var hash = __newtonUtils.http.getIpawnHash(this.credentials.secret, 'GET', url, '', __newtonUtils.http.getQueryString(query), timestamp);
    query.identifier = this.credentials.identifier;
    query.signature = hash;
    query.timestamp = timestamp;
    query.version = '2.0';

    query = __newtonUtils.http.getQueryString(query);

    __newtonUtils.http.makeAsyncRequestWithJSONP(url, query, callback);
  };

  return HttpWrapper;
})();

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

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


    var http = new HttpWrapper();
    http.isToSignWithIPawn(this.credentials)
      .makeRequestWithoutAResponse(this.url, events, callback);
  };

  ServerClientManager.prototype.refreshUserToken = function(bhandle, callback) {
    var url = getAuthEndpoint(this.credentials.secret) + '/token/refresh';
    var content = { bhandle: bhandle };

    if (!bhandle) {
      return callback(null, false);
    }

    var self = this;
    var http = new HttpWrapper();
    http.isToSignWithIPawn(this.credentials)
      .makeRequestWithAResponse(url, content, function(response) {
        var err;
        if(response.status > 299) {
          return callback(new Error('Invalid http request'));
        }

        var body = JSON.parse(response.responseText);
        callback(null, body);
      });
  };

  ServerClientManager.prototype.getUserMetaInfo = function(newtonToken, callback) {
    var url = getAuthEndpoint(this.credentials.secret) + '/userinfo';
    var content = { user_token: newtonToken };

    var self = this;
    var http = new HttpWrapper();
    http.isToSignWithIPawn(this.credentials)
      .makeRequestWithAResponse(url, content, function(response) {
        var err;
        if(response.status > 299) {
          return callback(new Error('Invalid http request'));
        }

        var body = JSON.parse(response.responseText);
        callback(null, body);
      });
  };

  return ServerClientManager;
})();

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

    // avoiding bind
    var self = this;
    this.heartbeatIntervalId = setInterval(function() {
      var names = Object.keys(self.timers);
      if (names.length < 1) {
        return;
      }
      var ev = {
        name: 'heartbeat',
        event_type: 'timer heartbeat',
        timers: names,
        period: HEARTBEAT_PERIOD
      };
      self.eventQueueManager.addEvent(ev);
    }, HEARTBEAT_PERIOD * 1000);
  }

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

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

    var timedEvent = {
      name: name,
      event_type: 'timer start'
    };
    if (cd) {
      timedEvent.custom_data = cd;
    }
    this.eventQueueManager.addEvent(timedEvent);
  };

  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);
    timedEvent.startDate = new Date(timedEvent.startDate * 1000);
    if (cd) {
      timedEvent.custom_data = cd;
    }
    timedEvent.event_type = 'timer stop';
    this.eventQueueManager.addEvent(timedEvent);

    delete this.timers[name];
  };

  return TimerManager;
})();

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

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

  FlowManager.prototype.flowBegin = function(name, cd) {
    var flowId = __newtonUtils.localStorage.getItem(FLOW_KEY);
    if (flowId) {
      this.flowFail('flow-start', {'started-flow-id': flowId});
    }
    flowId = __newtonUtils.random.string(FLOW_ID_LENGTH);
    __newtonUtils.localStorage.setItem(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 = __newtonUtils.localStorage.getItem(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 = __newtonUtils.localStorage.getItem(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 = __newtonUtils.localStorage.getItem(FLOW_KEY);
    if (!flowId) {
      throw new Error('No flow found');
    }
    __newtonUtils.localStorage.removeItem(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 = __newtonUtils.localStorage.getItem(FLOW_KEY);
    if (!flowId) {
      throw new Error('No flow found');
    }
    __newtonUtils.localStorage.removeItem(FLOW_KEY);
    var ev = {
      event_type: 'flow canceled',
      flow_id: flowId,
      name: reason
    };
    if (cd) {
      ev.custom_data = cd;
    }
    this.eventQueueManager.addEvent(ev);
  };
  return FlowManager;
})();

var SimpleObject = (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;
  };

  return SimpleObject;
})();

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

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

    },
    setOnceUserProperty: function(key, value) {

    },
    appendUserProperty: function(key, value) {

    },
    incrementUserProperty: function(key) {

    },
    decrementUserProperty: function(key) {

    },
    commit: function(callback) {

    }
  };
}

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

function getSubdomainSuffix(hostname, secret) {
  var isSandbox = !!secret.match('_');
  var isDev = !!hostname.match(/^.*2\.newton\.pm$/);
  return (isSandbox ? '-sandbox' : '') + (isDev ? '2' : '');
}

function getAuthEndpoint(secret) {
  var isSandbox = !!secret.match('_');
  return isSandbox ? AUTH_END_SANDBOX_URL : AUTH_END_URL;
}

function getClientEndpoint(secret) {
  var isSandbox = !!secret.match('_');
  return isSandbox ? CLIENT_END_EVENT_SANDBOX_URL : CLIENT_END_EVENT_URL;
}

var _log;

function redirectTo(url) {
  window.location = url;
}

function getParameterByName(name) {
    var match = new RegExp('[?&]' + encodeURIComponent(name) + '=([^&]*)').exec(window.location.search);
    return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
}

function getCurrentUrl() {
  return window.location + '';
}

function getHostname() {
  return window.location.hostname + '';
}

function replacer(match) {
  return match.charAt(0);
}

function getUrlWithoutParamenter(url, parameterToRemove) {
  return url.replace(new RegExp('[?&]' + encodeURIComponent(parameterToRemove) + '=[^&]*'), replacer, 'mi');
}

function removeDomain(url) {
  return url.replace(/https?:\/\/[^\/]*/, '', 'i') || '/';
}

function assertStringIsAValidUrl(str) {
  // FROM http://stackoverflow.com/a/14582229/1956082
  var pattern = /^https?:\/\/[\.\w\d]+(:\d+)?/;
  if (!pattern.test(str)) {
    throw new Error('string should be a valid url');
  }
}

function replaceState(state, title, url) {
  window.history.replaceState(state, title, url);
}


/**
 * 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 hostname = getHostname().toLowerCase();
  var identifier = getIdentifier(hostname);
  var endpoint = getClientEndpoint(secret);
  var credentials = {identifier: identifier, secret: secret};

  var userManager = new UserManager();
  var sessionManager = new SessionManager();
  var eventQueueManager = new EventQueueManager();
  var serverClientManager = new ServerClientManager(credentials, endpoint);
  var timerManager = new TimerManager();
  var flowManager = new FlowManager();

  function autologinFlowFunction(callback) {
    callback = callback || function() {};
    userManager.startAutologinFlow(credentials, callback);
  }

  sessionManager.setEventQueueManager(eventQueueManager);
  eventQueueManager.setSessionManager(sessionManager);
  eventQueueManager.setUserManager(userManager);
  eventQueueManager.setRequestAutologinCallback(autologinFlowFunction);
  eventQueueManager.setOnFlushEvents(function(events) {
    serverClientManager.flushEvents(events, function(response) {
      if (response.status === 205) {
        // need to start the autologin flow!
        userManager.startAutologinFlow(credentials, function(err, isLogged) {
          if (err || !isLogged) {
            loginCallback(err, isLogged);
          }
        });
      }
    });
  });
  timerManager.setEventQueueManager(eventQueueManager);
  flowManager.setEventQueueManager(eventQueueManager);
  userManager.setEventQueueManager(eventQueueManager);
  userManager.setServerClientManager(serverClientManager);

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

  var newtonUserTokenFromLoginFlow = getParameterByName('_nt');
  var userObjectUserTokenFromLoginFlow = getParameterByName('_uot');
  var errorFromLoginFlow = getParameterByName('_ne');
  var isReturningFromLoginFlow = !!((newtonUserTokenFromLoginFlow && userObjectUserTokenFromLoginFlow) || errorFromLoginFlow);

  if (isReturningFromLoginFlow) {
    if (window.history) {
      var cleanedUrl = removeDomain(getUrlWithoutParamenter(getUrlWithoutParamenter(getUrlWithoutParamenter(getUrlWithoutParamenter(getCurrentUrl(), '_nmc'), '_ne'), '_nt'), '_uot'));
      replaceState(window.history.state, document.title, cleanedUrl);
    }

    if (newtonUserTokenFromLoginFlow && userObjectUserTokenFromLoginFlow) {
      userManager.setNewtonToken(newtonUserTokenFromLoginFlow);
      userManager.setUserObjectToken(userObjectUserTokenFromLoginFlow);

      var oauthProvider = __newtonUtils.cookie.getItem(OAUTH_LOGIN_PROVIDER);
      if (oauthProvider && !errorFromLoginFlow) {
        setTimeout(function() {
          var ev = {
            event_type: 'identify',
            login_type: oauthProvider
          };
          eventQueueManager.addEvent(ev);
        }, 0);
      }
    }
  }


  sessionManager.start();
  userManager.start(credentials, function(err, isLogged) {
    if (err || !isLogged) {
      loginCallback(err, isLogged);
    }
  });

  var oauthProviders = [
    'facebook',
    'google'
  ];

  var loginCallback = function() { };

  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) { },
    startSetTransaction: function() {
      return createUserSetTransaction();
    },
    getUserProperty: function(key, callback) {

    },
    /**
     * Log the user out
     * @method
     * @instance
     * @memberOf NewtonSDK
     */
    userLogout: function() {
      if (userManager.isUserLogged()) {
        eventQueueManager.addEvent({
          event_type: 'logout'
        });
      }
      userManager.logout();
    },
    isLoginFlowRunning: function() {
      return false;
    },
    /**
     * Return a list of available and supported oauth provider
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @return {Array} the list
     */
    getOAuthProviders: function() {
      return oauthProviders;
    },
    /**
     * Login utilities
     * @class LoginBuilder
     * @name LoginBuilder
     * @private
     */
    /**
     * Custom Login
     * @class CustomLoginFlow
     * @name CustomLoginFlow
     * @private
     */
    /**
     * Custom Login
     * @class OAuthLoginFlow
     * @name OAuthLoginFlow
     * @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'),
        /**
         * Set return url. Used in OAuthLoginFlow
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {string} returnUrl
         *
         * @return {LoginBuilder} the same instance
         */
        setReturnUrl: setWrapper('returnUrl'),
        /**
         * Set return url. Used in OAuthLoginFlow
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {string} errorUrl
         *
         * @return {LoginBuilder} the same instance
         */
        setErrorUrl: setWrapper('errorUrl'),
        /**
         * Set return url. Used in OAuthLoginFlow
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {string} waitingUrl
         *
         * @return {LoginBuilder} the same instance
         */
        setWaitingUrl: setWrapper('waitingUrl'),
        /**
         * Set oauth provider needed for OAuthLoginFlow
         * @method
         * @instance
         * @memberOf LoginBuilder
         * @param {string} oauth provider
         *
         * @return {LoginBuilder} the same instance
         */
        setOAuthProvider: setWrapper('oauthProvider'),
        /**
         * Persist the custom login among the page refreshing. A session cookie is set
         * @method
         * @instance
         * @memberOf LoginBuilder
         *
         * @return {LoginBuilder} the same instance
         */
        setAllowCustomLoginSession: setWrapper('allowCustomLoginSession'),
        /**
         * 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);

          //  Storing here to prevent the changes
          var customId = 'C_' + props.customId;
          var cd = props.custom_data;
          var callback = props.callback;
          var allowCustomLoginSession = props.allowCustomLoginSession;

          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.isUserLogged()) {
                throw new Error('User already logged in');
              }
              userManager.setCustomToken(customId, allowCustomLoginSession);

              var ev = {
                event_type: 'identify',
                login_type: 'CUSTOM'
              };
              if (cd) {
                ev.custom_data = cd.toJSONObject();
              }
              eventQueueManager.addEvent(ev);
              setTimeout(callback, 0);
            }
          };
        },
        /**
         * Create a custom login flow.
         * @method
         * @instance
         * @memberOf LoginBuilder
         *
         * @return {OAuthLoginFlow}
         */
        getOAuthFlow: function() {
          assertCustomDataIsNullOrIsInstanceOfSimpleObject(props.custom_data);

          var oauthProvider = props.oauthProvider;
          if (oauthProviders.indexOf(oauthProvider) < 0) {
            throw new Error('Unkown oauth provider');
          }
          var returnUrl = props.returnUrl || getCurrentUrl();
          assertStringIsAValidUrl(returnUrl);

          var errorUrl = props.errorUrl || getCurrentUrl();
          assertStringIsAValidUrl(errorUrl);

          var waitingUrl = props.waitingUrl;
          if (waitingUrl) {
            assertStringIsAValidUrl(waitingUrl);
          }

          return {
            /**
             * Return a string representation
             * @method
             * @instance
             * @memberOf OAuthLoginFlow
             *
             * @return {string}
             */
            toString : function() {
              return 'OAuthLoginFlow(' + JSON.stringify(props) + ')';
            },
            /**
             * Start the oauth login flow
             * @method
             * @instance
             * @memberOf OAuthLoginFlow
             */
            startLoginFlow: function() {
              var userId = userManager.isUserLogged() ? userManager.getCurrentUserToken() : undefined;

              __newtonUtils.cookie.setItem(OAUTH_LOGIN_PROVIDER, oauthProvider);

              var url = getAuthEndpoint(secret) + '/' + oauthProvider + '/start';


              url = url + '?';
              if (waitingUrl) {
                url = url + 'waitingUrl=' + encodeURIComponent(waitingUrl) + '&';
              }

              url = url + 'returnUrl=' + encodeURIComponent(returnUrl) +
                '&errorUrl=' + encodeURIComponent(errorUrl) +
                '&application=' + hostname;
              if (userId) {
                url += '&userId=' + encodeURIComponent(userId);
              }
              // used to set cookie or localStorage
              setTimeout(function() {
                redirectTo(url);
              }, 0);
            }
          };
        }
      };
    },
    /**
     * Invoked on login complete successfully or not.
     * @method
     * @instance
     * @memberOf NewtonSDK
     */
    setLoginCallback: function(callback) {
      loginCallback = callback;

      if (isReturningFromLoginFlow) {
        var error;
        if (errorFromLoginFlow) {
          error = new Error(errorFromLoginFlow);
        }
        loginCallback(error, !error);
      }
    },
    /**
     * Finalize login flow
     * Used only for customized waiting page
     * @method
     * @instance
     * @memberOf NewtonSDK
     */
    finalizeLoginFlow: function() {
      var state = getParameterByName('state');

      var oauthProvider = (state || '').split('_')[0];
      if (!state || !oauthProvider) {
        if (/^auth-api(-sandbox)?2?\.newton\.pm$/.test(getHostname())) {
          return redirectTo('https://' + getHostname() + '/error_page.html?step=2');
        }
        return loginCallback(new Error('No login flow found'));
      }

      var url = getAuthEndpoint(secret) + '/' + oauthProvider + '/finalize';

      var body = {
        code: getParameterByName('code'),
        state: state
      };

      var http = new HttpWrapper();
      http.makeRequestWithAResponse(url, body, function(response) {
        var parsed;
        try {
          parsed = JSON.parse(response.responseText);
        } catch(e) {
          console.log(e);
          return loginCallback(new Error('Unknown error'));
        }

        // newton default loading page
        if (/^auth-api(-sandbox)?2?\.newton\.pm$/.test(getHostname())) {
          return redirectTo(parsed.url);
        }

        if (parsed.error) {
          return loginCallback(new Error(parsed.error));
        }

        if (!parsed.uoToken || !parsed.newtonToken) {
          return loginCallback(new Error('Unknown error'));
        }
        userManager.setUserObjectToken(parsed.uoToken);
        userManager.setNewtonToken(parsed.newtonToken);

        var ev = {
          event_type: 'identify',
          login_type: oauthProvider
        };
        eventQueueManager.addEvent(ev);

        setTimeout(function() {
          loginCallback(undefined, true);
        }, 0);
      });
    },
    /**
     * Retreive the user metainfo
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @param callback
     *
     */
    getUserMetaInfo: function(callback) {
      var userToken = userManager.getCurrentUserToken();
      var type = userToken.charAt(0);
      switch(type) {
        case 'A':
          return callback(new Error('User is unlogged'));
        case 'C':
          return callback(null, {login: {created: false, flow: 'custom'}});
        case 'U':
          autologinFlowFunction(function(err) {
            if (err) return callback(err);

            serverClientManager.getUserMetaInfo(userManager.getCurrentUserToken(), callback);
          });
          break;
        case 'N':
          serverClientManager.getUserMetaInfo(userManager.getCurrentUserToken(), callback);
          break;
        default:
          callback(new Error('Unknown user type'));
      }
    },
    /**
     * Return true if Newton has a set userId
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @return {Boolean} - true if the userId is set
     */
    isUserLogged: function() {
      return userManager.isUserLogged();
    },
    /**
     * This callback is used on NewtonSDK#isUserLoggedAsyncCallback
     * @callback NewtonSDK~isUserLoggedAsyncCallback
     * @param {Error} [err] - If something was wrong
     * @param {Boolean} [isLogged] - If the user is logged
     */
    /**
     * Make sure the knowledge about login user state
     * @method
     * @instance
     * @memberOf NewtonSDK
     * @param {NewtonSDK~isUserLoggedAsyncCallback}
     */
    isUserLoggedAsync: function(callback) {
      return userManager.startAutologinFlow(credentials, callback);
    },
    /**
     * Return The user token. This is not the user Id
     * @method
     * @instance
     * @memberOf NewtonSDK
     *
     * @return {String} - The user token
     */
    getUserToken: function() {
      return userManager.getCurrentUserToken();
    },
    /**
     * 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(' ');
  },
  __jsonpCallback: function(param) {
    var callback = jsonpCallbacks[param.callbackId];
     delete jsonpCallbacks[param.callbackId];
     callback({status: param.statusCode, responseText: JSON.stringify(param.content)});
  }
};