@loopkit/javascript
Version:
JavaScript SDK for LoopKit analytics platform
1,474 lines (1,463 loc) • 72 kB
JavaScript
/**
* LoopKit JavaScript SDK v1.1.6
* (c) 2025 LoopKit Team
* Released under the MIT License.
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.LoopKit = {}));
})(this, (function (exports) { 'use strict';
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _arrayWithoutHoles(r) {
if (Array.isArray(r)) return _arrayLikeToArray(r);
}
function asyncGeneratorStep(n, t, e, r, o, a, c) {
try {
var i = n[a](c),
u = i.value;
} catch (n) {
return void e(n);
}
i.done ? t(u) : Promise.resolve(u).then(r, o);
}
function _asyncToGenerator(n) {
return function () {
var t = this,
e = arguments;
return new Promise(function (r, o) {
var a = n.apply(t, e);
function _next(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "next", n);
}
function _throw(n) {
asyncGeneratorStep(a, r, o, _next, _throw, "throw", n);
}
_next(void 0);
});
};
}
function _classCallCheck(a, n) {
if (!(a instanceof n)) throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, r) {
for (var t = 0; t < r.length; t++) {
var o = r[t];
o.enumerable = o.enumerable || false, o.configurable = true, "value" in o && (o.writable = true), Object.defineProperty(e, _toPropertyKey(o.key), o);
}
}
function _createClass(e, r, t) {
return r && _defineProperties(e.prototype, r), t && _defineProperties(e, t), Object.defineProperty(e, "prototype", {
writable: false
}), e;
}
function _defineProperty(e, r, t) {
return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, {
value: t,
enumerable: true,
configurable: true,
writable: true
}) : e[r] = t, e;
}
function _iterableToArray(r) {
if ("undefined" != typeof Symbol && null != r[Symbol.iterator] || null != r["@@iterator"]) return Array.from(r);
}
function _nonIterableSpread() {
throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function ownKeys(e, r) {
var t = Object.keys(e);
if (Object.getOwnPropertySymbols) {
var o = Object.getOwnPropertySymbols(e);
r && (o = o.filter(function (r) {
return Object.getOwnPropertyDescriptor(e, r).enumerable;
})), t.push.apply(t, o);
}
return t;
}
function _objectSpread2(e) {
for (var r = 1; r < arguments.length; r++) {
var t = null != arguments[r] ? arguments[r] : {};
r % 2 ? ownKeys(Object(t), true).forEach(function (r) {
_defineProperty(e, r, t[r]);
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
});
}
return e;
}
function _regenerator() {
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/babel/babel/blob/main/packages/babel-helpers/LICENSE */
var e,
t,
r = "function" == typeof Symbol ? Symbol : {},
n = r.iterator || "@@iterator",
o = r.toStringTag || "@@toStringTag";
function i(r, n, o, i) {
var c = n && n.prototype instanceof Generator ? n : Generator,
u = Object.create(c.prototype);
return _regeneratorDefine(u, "_invoke", function (r, n, o) {
var i,
c,
u,
f = 0,
p = o || [],
y = false,
G = {
p: 0,
n: 0,
v: e,
a: d,
f: d.bind(e, 4),
d: function (t, r) {
return i = t, c = 0, u = e, G.n = r, a;
}
};
function d(r, n) {
for (c = r, u = n, t = 0; !y && f && !o && t < p.length; t++) {
var o,
i = p[t],
d = G.p,
l = i[2];
r > 3 ? (o = l === n) && (u = i[(c = i[4]) ? 5 : (c = 3, 3)], i[4] = i[5] = e) : i[0] <= d && ((o = r < 2 && d < i[1]) ? (c = 0, G.v = n, G.n = i[1]) : d < l && (o = r < 3 || i[0] > n || n > l) && (i[4] = r, i[5] = n, G.n = l, c = 0));
}
if (o || r > 1) return a;
throw y = true, n;
}
return function (o, p, l) {
if (f > 1) throw TypeError("Generator is already running");
for (y && 1 === p && d(p, l), c = p, u = l; (t = c < 2 ? e : u) || !y;) {
i || (c ? c < 3 ? (c > 1 && (G.n = -1), d(c, u)) : G.n = u : G.v = u);
try {
if (f = 2, i) {
if (c || (o = "next"), t = i[o]) {
if (!(t = t.call(i, u))) throw TypeError("iterator result is not an object");
if (!t.done) return t;
u = t.value, c < 2 && (c = 0);
} else 1 === c && (t = i.return) && t.call(i), c < 2 && (u = TypeError("The iterator does not provide a '" + o + "' method"), c = 1);
i = e;
} else if ((t = (y = G.n < 0) ? u : r.call(n, G)) !== a) break;
} catch (t) {
i = e, c = 1, u = t;
} finally {
f = 1;
}
}
return {
value: t,
done: y
};
};
}(r, o, i), true), u;
}
var a = {};
function Generator() {}
function GeneratorFunction() {}
function GeneratorFunctionPrototype() {}
t = Object.getPrototypeOf;
var c = [][n] ? t(t([][n]())) : (_regeneratorDefine(t = {}, n, function () {
return this;
}), t),
u = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(c);
function f(e) {
return Object.setPrototypeOf ? Object.setPrototypeOf(e, GeneratorFunctionPrototype) : (e.__proto__ = GeneratorFunctionPrototype, _regeneratorDefine(e, o, "GeneratorFunction")), e.prototype = Object.create(u), e;
}
return GeneratorFunction.prototype = GeneratorFunctionPrototype, _regeneratorDefine(u, "constructor", GeneratorFunctionPrototype), _regeneratorDefine(GeneratorFunctionPrototype, "constructor", GeneratorFunction), GeneratorFunction.displayName = "GeneratorFunction", _regeneratorDefine(GeneratorFunctionPrototype, o, "GeneratorFunction"), _regeneratorDefine(u), _regeneratorDefine(u, o, "Generator"), _regeneratorDefine(u, n, function () {
return this;
}), _regeneratorDefine(u, "toString", function () {
return "[object Generator]";
}), (_regenerator = function () {
return {
w: i,
m: f
};
})();
}
function _regeneratorDefine(e, r, n, t) {
var i = Object.defineProperty;
try {
i({}, "", {});
} catch (e) {
i = 0;
}
_regeneratorDefine = function (e, r, n, t) {
if (r) i ? i(e, r, {
value: n,
enumerable: !t,
configurable: !t,
writable: !t
}) : e[r] = n;else {
function o(r, n) {
_regeneratorDefine(e, r, function (e) {
return this._invoke(r, n, e);
});
}
o("next", 0), o("throw", 1), o("return", 2);
}
}, _regeneratorDefine(e, r, n, t);
}
function _toConsumableArray(r) {
return _arrayWithoutHoles(r) || _iterableToArray(r) || _unsupportedIterableToArray(r) || _nonIterableSpread();
}
function _toPrimitive(t, r) {
if ("object" != typeof t || !t) return t;
var e = t[Symbol.toPrimitive];
if (void 0 !== e) {
var i = e.call(t, r);
if ("object" != typeof i) return i;
throw new TypeError("@@toPrimitive must return a primitive value.");
}
return (String )(t);
}
function _toPropertyKey(t) {
var i = _toPrimitive(t, "string");
return "symbol" == typeof i ? i : i + "";
}
function _typeof(o) {
"@babel/helpers - typeof";
return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) {
return typeof o;
} : function (o) {
return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o;
}, _typeof(o);
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
/**
* Configuration validation utilities
*/
var ConfigValidator = /*#__PURE__*/function () {
function ConfigValidator() {
_classCallCheck(this, ConfigValidator);
}
return _createClass(ConfigValidator, null, [{
key: "validate",
value:
/**
* Validate LoopKit configuration
*/
function validate(config) {
if (config.batchSize !== undefined && config.batchSize <= 0) {
throw new Error('batchSize must be greater than 0');
}
if (config.flushInterval !== undefined && config.flushInterval < 0) {
throw new Error('flushInterval must be >= 0');
}
if (config.maxQueueSize !== undefined && config.maxQueueSize <= 0) {
throw new Error('maxQueueSize must be greater than 0');
}
if (config.requestTimeout !== undefined && config.requestTimeout <= 0) {
throw new Error('requestTimeout must be greater than 0');
}
if (config.maxRetries !== undefined && config.maxRetries < 0) {
throw new Error('maxRetries must be >= 0');
}
if (config.sessionTimeout !== undefined && config.sessionTimeout <= 0) {
throw new Error('sessionTimeout must be greater than 0');
}
}
}]);
}();
/**
* Logging utility for LoopKit SDK
*/
var Logger = /*#__PURE__*/function () {
function Logger(config) {
_classCallCheck(this, Logger);
this.config = config;
}
/**
* Update configuration
*/
return _createClass(Logger, [{
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Log an error message
*/
}, {
key: "error",
value: function error(message, data) {
this.log('error', message, data);
}
/**
* Log a warning message
*/
}, {
key: "warn",
value: function warn(message, data) {
this.log('warn', message, data);
}
/**
* Log an info message
*/
}, {
key: "info",
value: function info(message, data) {
this.log('info', message, data);
}
/**
* Log a debug message
*/
}, {
key: "debug",
value: function debug(message, data) {
this.log('debug', message, data);
}
/**
* Internal logging method
*/
}, {
key: "log",
value: function log(level, message, data) {
if (!this.shouldLog(level)) {
return;
}
var now = new Date();
var fullTimestamp = now.toLocaleDateString('en-US') + ' ' + now.toLocaleTimeString('en-US') + '.' + now.getMilliseconds();
if (data) {
var logData = _objectSpread2(_objectSpread2({}, data), {}, {
timestamp: fullTimestamp,
version: "1.1.6"
});
var logMessage = "[LoopKit] ".concat(message);
console[level](logMessage, logData);
} else {
console[level]("[LoopKit] ".concat(message), {
timestamp: fullTimestamp,
version: "1.1.6"
});
}
}
/**
* Check if we should log at the given level
*/
}, {
key: "shouldLog",
value: function shouldLog(level) {
if (!this.config.debug) {
return false;
}
var levels = ['error', 'warn', 'info', 'debug'];
var configLevel = this.config.logLevel || 'debug';
var configLevelIndex = levels.indexOf(configLevel);
var messageLevelIndex = levels.indexOf(level);
return messageLevelIndex <= configLevelIndex;
}
}]);
}();
/**
* ID generation utilities
*/
var IdGenerator = /*#__PURE__*/function () {
function IdGenerator() {
_classCallCheck(this, IdGenerator);
}
return _createClass(IdGenerator, [{
key: "generateSessionId",
value:
/**
* Generate a unique session ID
*/
function generateSessionId() {
return 'sess_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
}
/**
* Generate a unique anonymous ID
*/
}, {
key: "generateAnonymousId",
value: function generateAnonymousId() {
return 'anon_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
}
/**
* Generate a unique event ID
*/
}, {
key: "generateEventId",
value: function generateEventId() {
return 'evt_' + Math.random().toString(36).substr(2, 9) + '_' + Date.now();
}
}]);
}();
/**
* Storage manager for localStorage operations
*/
var StorageManager = /*#__PURE__*/function () {
function StorageManager(config, logger) {
_classCallCheck(this, StorageManager);
this.config = config;
this.logger = logger;
}
/**
* Update configuration
*/
return _createClass(StorageManager, [{
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Get localStorage instance with fallback
*/
}, {
key: "getStorage",
value: function getStorage() {
var storage = typeof global !== 'undefined' && global.localStorage ? global.localStorage : typeof localStorage !== 'undefined' ? localStorage : null;
return storage;
}
/**
* Persist event queue to localStorage with version tracking
*/
}, {
key: "persistQueue",
value: function persistQueue(queue) {
if (!this.config.enableLocalStorage) {
return;
}
var storage = this.getStorage();
if (storage) {
try {
var data = {
version: typeof "1.1.6" !== 'undefined' ? "1.1.6" : '1.0.4',
events: queue,
timestamp: Date.now()
};
storage.setItem('loopkit_queue', JSON.stringify(data));
} catch (error) {
this.logger.warn('Failed to persist queue', {
error: error
});
}
}
}
/**
* Load persisted queue from localStorage with version checking
*/
}, {
key: "loadQueue",
value: function loadQueue() {
if (!this.config.enableLocalStorage) {
return [];
}
var storage = this.getStorage();
if (storage) {
try {
var stored = storage.getItem('loopkit_queue');
if (stored) {
var data = JSON.parse(stored);
// Handle legacy format (just an array) - clear it
if (Array.isArray(data)) {
this.logger.debug('Legacy event format detected, clearing queue');
if (storage) {
try {
storage.removeItem('loopkit_queue');
} catch (error) {
this.logger.warn('Failed to clear persisted queue', {
error: error
});
}
}
return [];
}
// Handle new format with version checking
if (data && _typeof(data) === 'object' && data.version && data.events) {
var currentVersion = typeof "1.1.6" !== 'undefined' ? "1.1.6" : '1.0.4';
if (data.version !== currentVersion) {
this.logger.debug("Version mismatch detected (stored: ".concat(data.version, ", current: ").concat(currentVersion, "), clearing queue"));
if (storage) {
try {
storage.removeItem('loopkit_queue');
} catch (error) {
this.logger.warn('Failed to clear persisted queue', {
error: error
});
}
}
return [];
}
if (Array.isArray(data.events)) {
this.logger.debug("Loaded ".concat(data.events.length, " events from storage (version: ").concat(data.version, ")"));
return data.events;
}
}
}
} catch (error) {
this.logger.warn('Failed to load persisted queue, clearing corrupted data', {
error: error
});
this.clearQueue();
}
}
return [];
}
/**
* Clear persisted queue from localStorage
*/
}, {
key: "clearQueue",
value: function clearQueue() {
if (!this.config.enableLocalStorage) {
return;
}
var storage = this.getStorage();
if (storage) {
try {
storage.removeItem('loopkit_queue');
} catch (error) {
this.logger.warn('Failed to clear persisted queue', {
error: error
});
}
}
}
/**
* Load anonymous ID from localStorage
*/
}, {
key: "loadAnonymousId",
value: function loadAnonymousId() {
if (!this.config.enableLocalStorage) {
return null;
}
var storage = this.getStorage();
if (storage) {
try {
var stored = storage.getItem('loopkit_anonymousId');
if (stored) {
return stored;
}
} catch (error) {
this.logger.warn('Failed to load anonymous ID', {
error: error
});
}
}
return null;
}
/**
* Save anonymous ID to localStorage
*/
}, {
key: "saveAnonymousId",
value: function saveAnonymousId(anonymousId) {
if (!this.config.enableLocalStorage) {
return;
}
var storage = this.getStorage();
if (storage) {
try {
storage.setItem('loopkit_anonymousId', anonymousId);
} catch (error) {
this.logger.warn('Failed to save anonymous ID', {
error: error
});
}
}
}
/**
* Clear anonymous ID from localStorage
*/
}, {
key: "clearAnonymousId",
value: function clearAnonymousId() {
if (!this.config.enableLocalStorage) {
return;
}
var storage = this.getStorage();
if (storage) {
try {
storage.removeItem('loopkit_anonymousId');
} catch (error) {
this.logger.warn('Failed to clear anonymous ID', {
error: error
});
}
}
}
/**
* Clear all LoopKit data from localStorage
*/
}, {
key: "clearAll",
value: function clearAll() {
this.clearQueue();
this.clearAnonymousId();
}
}]);
}();
/**
* Session manager for handling user sessions and anonymous IDs
*/
var SessionManager = /*#__PURE__*/function () {
function SessionManager(config, logger, idGenerator, storageManager) {
_classCallCheck(this, SessionManager);
this.config = config;
this.logger = logger;
this.idGenerator = idGenerator;
this.storageManager = storageManager;
this.lastActivityTime = Date.now();
this.sessionStartTime = Date.now();
// Initialize anonymous ID
this.anonymousId = this.loadOrCreateAnonymousId();
// Initialize session
this.sessionId = this.idGenerator.generateSessionId();
this.startSession();
this.logger.debug("Session initialized", {
sessionId: this.sessionId,
anonymousId: this.anonymousId
});
}
/**
* Set callback for session events
*/
return _createClass(SessionManager, [{
key: "setSessionEventCallback",
value: function setSessionEventCallback(callback) {
this.sessionEventCallback = callback;
}
/**
* Get current session ID
*/
}, {
key: "getSessionId",
value: function getSessionId() {
this.updateActivity();
return this.sessionId;
}
/**
* Get anonymous ID
*/
}, {
key: "getAnonymousId",
value: function getAnonymousId() {
this.updateActivity();
return this.anonymousId;
}
/**
* Start new session
*/
}, {
key: "startSession",
value: function startSession() {
var previousSessionId = this.sessionId;
this.sessionId = this.idGenerator.generateSessionId();
this.sessionStartTime = Date.now();
this.lastActivityTime = this.sessionStartTime;
this.logger.debug("New session started: ".concat(this.sessionId));
// Track session_start event if callback is available and session tracking is enabled
if (this.config.enableSessionTracking && this.sessionEventCallback) {
this.sessionEventCallback('session_start', {
sessionId: this.sessionId,
previousSessionId: previousSessionId !== this.sessionId ? previousSessionId : undefined
});
}
}
/**
* End current session
*/
}, {
key: "endSession",
value: function endSession() {
var sessionDuration = Date.now() - this.sessionStartTime;
var endedSessionId = this.sessionId;
this.logger.debug("Session ended: ".concat(endedSessionId));
// Track session_end event if callback is available and session tracking is enabled
if (this.config.enableSessionTracking && this.sessionEventCallback) {
this.sessionEventCallback('session_end', {
sessionId: endedSessionId,
duration: Math.round(sessionDuration / 1000) // duration in seconds
});
}
// Start new session
this.startSession();
}
/**
* Check if session is active
*/
}, {
key: "isSessionActive",
value: function isSessionActive() {
if (!this.config.enableSessionTracking) {
return true; // Always active if session tracking is disabled
}
var timeSinceLastActivity = Date.now() - this.lastActivityTime;
var sessionTimeoutMs = this.config.sessionTimeout * 1000;
return timeSinceLastActivity < sessionTimeoutMs;
}
/**
* Update activity timestamp
*/
}, {
key: "updateActivity",
value: function updateActivity() {
var now = Date.now();
var timeSinceLastActivity = now - this.lastActivityTime;
var sessionTimeoutMs = this.config.sessionTimeout * 1000;
// If session has timed out, end current session and start a new one
if (this.config.enableSessionTracking && timeSinceLastActivity >= sessionTimeoutMs) {
this.logger.debug('Session timeout detected, ending current session and starting new one');
this.endSession();
} else {
this.lastActivityTime = now;
}
}
/**
* Update configuration
*/
}, {
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Reset session state
*/
}, {
key: "reset",
value: function reset() {
this.logger.debug('Resetting session manager');
// End current session before starting new one
if (this.config.enableSessionTracking && this.sessionEventCallback) {
var sessionDuration = Date.now() - this.sessionStartTime;
this.sessionEventCallback('session_end', {
sessionId: this.sessionId,
duration: Math.round(sessionDuration / 1000),
reason: 'reset'
});
}
this.startSession();
// Don't reset anonymous ID on reset - it should persist across sessions
}
/**
* Load existing anonymous ID or create new one
*/
}, {
key: "loadOrCreateAnonymousId",
value: function loadOrCreateAnonymousId() {
var existingId = this.storageManager.loadAnonymousId();
if (!existingId) {
existingId = this.idGenerator.generateAnonymousId();
this.storageManager.saveAnonymousId(existingId);
this.logger.debug("Generated new anonymous ID: ".concat(existingId));
} else {
this.logger.debug("Loaded existing anonymous ID: ".concat(existingId));
}
return existingId;
}
}]);
}();
/**
* Queue manager for handling event batching and flushing
*/
var QueueManager = /*#__PURE__*/function () {
function QueueManager(config, logger, storageManager) {
_classCallCheck(this, QueueManager);
this.eventQueue = [];
this.isInitialized = false;
this.config = config;
this.logger = logger;
this.storageManager = storageManager;
this.loadPersistedQueue();
this.isInitialized = true;
}
/**
* Add event to queue
*/
return _createClass(QueueManager, [{
key: "enqueueEvent",
value: function enqueueEvent(event) {
if (!this.isInitialized) {
this.logger.warn('QueueManager not initialized');
return;
}
// Determine event type based on properties
var type;
if ('name' in event) {
type = 'track';
} else if ('groupId' in event) {
type = 'group';
} else {
type = 'identify';
}
var queuedEvent = {
type: type,
event: event
};
// Check queue size limit
if (this.eventQueue.length >= this.config.maxQueueSize) {
this.logger.warn("Queue size limit reached (".concat(this.config.maxQueueSize, "), dropping oldest event"));
this.eventQueue.shift(); // Remove oldest event
}
// Add to queue
this.eventQueue.push(queuedEvent);
this.logger.debug("Event queued. Queue size: ".concat(this.eventQueue.length));
// Persist to localStorage
this.persistQueue();
// Auto-flush if batch size reached
if (this.eventQueue.length >= this.config.batchSize) {
this.logger.debug("Batch size reached (".concat(this.config.batchSize, "), auto-flushing"));
this.flush(this.networkManager);
}
}
/**
* Flush events to API
*/
}, {
key: "flush",
value: (function () {
var _flush = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(networkManager) {
var _this = this;
var eventsToFlush, tracks, identifies, groups, promises, endpoint, payload, _endpoint, _payload, _endpoint2, _payload2, _t;
return _regenerator().w(function (_context) {
while (1) switch (_context.n) {
case 0:
if (networkManager) {
_context.n = 1;
break;
}
this.logger.warn('NetworkManager not set, cannot flush');
return _context.a(2);
case 1:
if (!(this.eventQueue.length === 0)) {
_context.n = 2;
break;
}
this.logger.debug('No events to flush');
return _context.a(2);
case 2:
eventsToFlush = _toConsumableArray(this.eventQueue);
this.eventQueue = [];
this.persistQueue();
_context.p = 3;
// Group events by type
tracks = [];
identifies = [];
groups = [];
eventsToFlush.forEach(function (_ref) {
var type = _ref.type,
event = _ref.event;
switch (type) {
case 'track':
tracks.push(event);
break;
case 'identify':
identifies.push(event);
break;
case 'group':
groups.push(event);
break;
}
});
// Send to separate API endpoints
promises = [];
if (tracks.length > 0) {
endpoint = "".concat(this.config.baseURL, "/tracks");
payload = {
tracks: tracks
};
promises.push(networkManager.sendEvents(endpoint, payload));
}
if (identifies.length > 0) {
_endpoint = "".concat(this.config.baseURL, "/identifies");
_payload = {
identifies: identifies
};
promises.push(networkManager.sendEvents(_endpoint, _payload));
}
if (groups.length > 0) {
_endpoint2 = "".concat(this.config.baseURL, "/groups");
_payload2 = {
groups: groups
};
promises.push(networkManager.sendEvents(_endpoint2, _payload2));
}
// TODO: Migrate to Promise.allSettled and only requeue events that failed
_context.n = 4;
return Promise.all(promises);
case 4:
this.logger.debug("Successfully flushed ".concat(eventsToFlush.length, " events"));
// Call onAfterTrack for each event
if (this.config.onAfterTrack) {
eventsToFlush.forEach(function (_ref2) {
var type = _ref2.type,
event = _ref2.event;
if (type === 'track') {
_this.config.onAfterTrack(event, true);
}
});
}
_context.n = 6;
break;
case 5:
_context.p = 5;
_t = _context.v;
this.logger.error('Failed to flush events', {
error: _t
});
// Re-queue failed events at the front
this.eventQueue = [].concat(_toConsumableArray(eventsToFlush), _toConsumableArray(this.eventQueue));
this.persistQueue();
// Call onAfterTrack for failed events
if (this.config.onAfterTrack) {
eventsToFlush.forEach(function (_ref3) {
var type = _ref3.type,
event = _ref3.event;
if (type === 'track') {
_this.config.onAfterTrack(event, false);
}
});
}
if (this.config.onError) {
this.config.onError(_t);
}
throw _t;
case 6:
return _context.a(2);
}
}, _callee, this, [[3, 5]]);
}));
function flush(_x) {
return _flush.apply(this, arguments);
}
return flush;
}()
/**
* Get current queue
*/
)
}, {
key: "getQueue",
value: function getQueue() {
return _toConsumableArray(this.eventQueue);
}
/**
* Get queue size
*/
}, {
key: "getQueueSize",
value: function getQueueSize() {
return this.eventQueue.length;
}
/**
* Clear queue
*/
}, {
key: "clearQueue",
value: function clearQueue() {
this.eventQueue = [];
this.persistQueue();
this.logger.debug('Queue cleared');
}
/**
* Reset queue state
*/
}, {
key: "reset",
value: function reset() {
this.clearQueue();
this.stopAutoFlush();
this.storageManager.clearQueue();
}
/**
* Update configuration
*/
}, {
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Set network manager
*/
}, {
key: "setNetworkManager",
value: function setNetworkManager(networkManager) {
this.networkManager = networkManager;
}
/**
* Schedule auto-flush
*/
}, {
key: "scheduleFlush",
value: function scheduleFlush() {
var _this2 = this;
if (this.config.flushInterval > 0) {
this.flushTimer = setInterval(function () {
if (_this2.eventQueue.length > 0 && _this2.networkManager) {
_this2.logger.debug('Auto-flush triggered');
_this2.flush(_this2.networkManager);
}
}, this.config.flushInterval * 1000);
}
}
/**
* Restart auto-flush with new config
*/
}, {
key: "restartAutoFlush",
value: function restartAutoFlush() {
this.stopAutoFlush();
this.scheduleFlush();
}
/**
* Stop auto-flush timer
*/
}, {
key: "stopAutoFlush",
value: function stopAutoFlush() {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = undefined;
}
}
/**
* Persist queue to storage
*/
}, {
key: "persistQueue",
value: function persistQueue() {
if (this.config.enableLocalStorage) {
try {
this.storageManager.persistQueue(this.eventQueue);
} catch (error) {
this.logger.warn('Failed to persist queue', {
error: error
});
}
}
}
/**
* Load persisted queue from storage
*/
}, {
key: "loadPersistedQueue",
value: function loadPersistedQueue() {
if (this.config.enableLocalStorage) {
try {
var persistedQueue = this.storageManager.loadQueue();
if (Array.isArray(persistedQueue) && persistedQueue.length > 0) {
this.eventQueue = persistedQueue;
this.logger.debug("Loaded ".concat(this.eventQueue.length, " persisted events"));
}
} catch (error) {
this.logger.warn('Failed to load persisted queue', {
error: error
});
}
}
}
}]);
}();
/**
* Privacy utilities for handling user preferences
*/
var PrivacyUtils = /*#__PURE__*/function () {
function PrivacyUtils() {
_classCallCheck(this, PrivacyUtils);
}
return _createClass(PrivacyUtils, null, [{
key: "isDoNotTrackEnabled",
value:
/**
* Check if Do Not Track is enabled in the browser
*/
function isDoNotTrackEnabled() {
if (typeof navigator === 'undefined') {
return false;
}
// Check various DNT implementations
var dnt = navigator.doNotTrack || window.doNotTrack || navigator.msDoNotTrack;
return dnt === '1' || dnt === 'yes' || dnt === true;
}
}]);
}();
/**
* Event tracker for handling all event types
*/
var EventTracker = /*#__PURE__*/function () {
function EventTracker(config, logger, queueManager, sessionManager, idGenerator) {
_classCallCheck(this, EventTracker);
this.config = config;
this.logger = logger;
this.queueManager = queueManager;
this.sessionManager = sessionManager;
this.idGenerator = idGenerator;
}
/**
* Track an event
*/
return _createClass(EventTracker, [{
key: "track",
value: function track(eventName) {
var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
// Validate input
if (!eventName || typeof eventName !== 'string') {
this.logger.warn('Event name is required and must be a string');
return;
}
// Check DNT
if (this.config.respectDoNotTrack && PrivacyUtils.isDoNotTrackEnabled()) {
this.logger.debug('Do Not Track is enabled, skipping event tracking');
return;
}
try {
// Create track event
var trackEvent = _objectSpread2(_objectSpread2({}, this.createBaseEvent(options.timestamp)), {}, {
name: eventName,
properties: _objectSpread2({}, properties)
}, context.userId && {
userId: context.userId
});
// Apply onBeforeTrack callback if present
var finalEvent = trackEvent;
if (this.config.onBeforeTrack) {
try {
var result = this.config.onBeforeTrack(trackEvent);
if (result) {
finalEvent = result;
}
} catch (error) {
this.logger.error('Error in onBeforeTrack callback', {
error: error
});
}
}
// Add to queue
this.queueManager.enqueueEvent(finalEvent);
this.logger.debug("Tracked event: ".concat(eventName), finalEvent);
} catch (error) {
this.logger.error('Failed to track event', {
eventName: eventName,
error: error
});
if (this.config.onError) {
this.config.onError(error);
}
}
}
/**
* Identify a user
*/
}, {
key: "identify",
value: function identify(userId) {
var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
// Validate input
if (!userId || typeof userId !== 'string') {
this.logger.warn('User ID is required and must be a string');
return;
}
// Check DNT
if (this.config.respectDoNotTrack && PrivacyUtils.isDoNotTrackEnabled()) {
this.logger.debug('Do Not Track is enabled, skipping user identification');
return;
}
try {
// Create identify event
var identifyEvent = _objectSpread2(_objectSpread2({}, this.createBaseEvent()), {}, {
userId: userId,
properties: _objectSpread2({}, properties)
});
// Add to queue
this.queueManager.enqueueEvent(identifyEvent);
this.logger.debug("Identified user: ".concat(userId), identifyEvent);
} catch (error) {
this.logger.error('Failed to identify user', {
userId: userId,
error: error
});
if (this.config.onError) {
this.config.onError(error);
}
}
}
/**
* Associate user with a group
*/
}, {
key: "group",
value: function group(groupId) {
var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var groupType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'organization';
var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
// Validate input
if (!groupId || typeof groupId !== 'string') {
this.logger.warn('Group ID is required and must be a string');
return;
}
// Check DNT
if (this.config.respectDoNotTrack && PrivacyUtils.isDoNotTrackEnabled()) {
this.logger.debug('Do Not Track is enabled, skipping group association');
return;
}
try {
// Create group event
var groupEvent = _objectSpread2(_objectSpread2({}, this.createBaseEvent()), {}, {
groupId: groupId,
groupType: groupType,
properties: _objectSpread2({}, properties)
}, context.userId && {
userId: context.userId
});
// Add to queue
this.queueManager.enqueueEvent(groupEvent);
this.logger.debug("Associated with group: ".concat(groupId), groupEvent);
} catch (error) {
this.logger.error('Failed to associate with group', {
groupId: groupId,
error: error
});
if (this.config.onError) {
this.config.onError(error);
}
}
}
/**
* Update configuration
*/
}, {
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Create base event structure
*/
}, {
key: "createBaseEvent",
value: function createBaseEvent(timestamp) {
var now = timestamp || new Date().toISOString();
return {
anonymousId: this.sessionManager.getAnonymousId(),
sessionId: this.sessionManager.getSessionId(),
timestamp: now,
system: this.createSystemInfo()
};
}
/**
* Create system information
*/
}, {
key: "createSystemInfo",
value: function createSystemInfo() {
var system = {
sdk: {
name: '@loopkit/javascript',
version: "1.1.6"
}
};
// Add browser context if in browser environment
if (typeof window !== 'undefined') {
system.context = {
page: {
url: window.location.href,
path: window.location.pathname,
search: window.location.search,
title: document.title,
referrer: document.referrer
},
userAgent: navigator.userAgent,
screen: {
width: window.screen.width,
height: window.screen.height
},
viewport: {
width: window.innerWidth,
height: window.innerHeight
}
};
}
return system;
}
}]);
}();
/**
* Network manager for handling API communication
*/
var NetworkManager = /*#__PURE__*/function () {
function NetworkManager(config, logger) {
_classCallCheck(this, NetworkManager);
this.config = config;
this.logger = logger;
}
/**
* Send events to API with retry logic
*/
return _createClass(NetworkManager, [{
key: "sendEvents",
value: (function () {
var _sendEvents = _asyncToGenerator(/*#__PURE__*/_regenerator().m(function _callee(endpoint, payload) {
var retryCount,
headers,
requestOptions,
controller,
eventCount,
response,
delay,
status,
statusText,
httpError,
_delay,
result,
_args = arguments,
_t,
_t3;
return _regenerator().w(function (_context) {
while (1) switch (_context.n) {
case 0:
retryCount = _args.length > 2 && _args[2] !== undefined ? _args[2] : 0;
_context.p = 1;
headers = {
'Content-Type': 'application/json',
Authorization: "Bearer ".concat(this.config.apiKey)
}; // Add compression header if enabled
if (this.config.enableCompression) {
headers['Accept-Encoding'] = 'gzip, deflate';
}
requestOptions = {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
keepalive: true
}; // Add timeout if supported
if (this.config.requestTimeout && this.config.requestTimeout > 0) {
controller = new AbortController();
requestOptions.signal = controller.signal;
setTimeout(function () {
return controller.abort();
}, this.config.requestTimeout);
}
// Get event count from payload for logging
eventCount = this.getEventCountFromPayload(payload);
this.logger.debug("Sending ".concat(eventCount, " event(s) to ").concat(endpoint), {
retryCount: retryCount,
payload: payload
});
response = null;
_context.p = 2;
_context.n = 3;
return fetch(endpoint, requestOptions);
case 3:
response = _context.v;
_context.n = 7;
break;
case 4:
_context.p = 4;
_t = _context.v;
// This is a network-level error (connection failed, etc.)
// Preserve the original error message and re-throw immediately
this.logger.error("Network error during sendEvents (attempt ".concat(retryCount + 1, ")"), {
endpoint: endpoint,
error: _t instanceof Error ? _t.message : _t,
retryCount: retryCount
});
// Retry logic for network errors
if (!(retryCount < this.config.maxRetries)) {
_context.n = 6;
break;
}
delay = this.calculateRetryDelay(retryCount);
this.logger.debug("Retrying in ".concat(delay, "ms (attempt ").concat(retryCount + 2, ")"));
_context.n = 5;
return this.sleep(delay);
case 5:
return _context.a(2, this.sendEvents(endpoint, payload, retryCount + 1));
case 6:
throw _t;
case 7:
if (!(!response || !response.ok)) {
_context.n = 10;
break;
}
status = (response === null || response === void 0 ? void 0 : response.status) || 'unknown';
statusText = (response === null || response === void 0 ? void 0 : response.statusText) || 'unknown error';
httpError = new Error("HTTP ".concat(status, ": ").concat(statusText));
this.logger.error("HTTP error during sendEvents (attempt ".concat(retryCount + 1, ")"), {
endpoint: endpoint,
status: status,
statusText: statusText,
retryCount: retryCount
});
// Retry logic for HTTP errors
if (!(retryCount < this.config.maxRetries)) {
_context.n = 9;
break;
}
_delay = this.calculateRetryDelay(retryCount);
this.logger.debug("Retrying in ".concat(_delay, "ms (attempt ").concat(retryCount + 2, ")"));
_context.n = 8;
return this.sleep(_delay);
case 8:
return _context.a(2, this.sendEvents(endpoint, payload, retryCount + 1));
case 9:
throw httpError;
case 10:
_context.p = 10;
_context.n = 11;
return response.json();
case 11:
result = _context.v;
_context.n = 13;
break;
case 12:
_context.p = 12;
_context.v;
// If response.json() fails, return empty object
result = {};
case 13:
this.logger.debug('Events sent successfully', {
result: result
});
return _context.a(2, result);
case 14:
_context.p = 14;
_t3 = _context.v;
// This catch should only handle non-retry errors that weren't already handled above
// In practice, this should not be reached for network/HTTP errors as they are handled above
this.logger.error("Unexpected error in sendEvents", {
endpoint: endpoint,
error: _t3 instanceof Error ? _t3.message : _t3,
retryCount: retryCount
});
throw _t3;
case 15:
return _context.a(2);
}
}, _callee, this, [[10, 12], [2, 4], [1, 14]]);
}));
function sendEvents(_x, _x2) {
return _sendEvents.apply(this, arguments);
}
return sendEvents;
}()
/**
* Update configuration
*/
)
}, {
key: "updateConfig",
value: function updateConfig(config) {
this.config = config;
}
/**
* Calculate retry delay based on backoff strategy
*/
}, {
key: "calculateRetryDelay",
value: function calculateRetryDelay(retryCount) {
var baseDelay = 1000; // 1 second base delay
if (this.config.retryBackof