This is an automated email from the ASF dual-hosted git repository.

jky pushed a commit to branch mv3-migrate
in repository https://gitbox.apache.org/repos/asf/flagon.git

commit 1a59d80f881487640cfaf7297bcb54cfa174deff
Author: Jason Young <jk...@pm.me>
AuthorDate: Wed Apr 30 14:54:48 2025 -0700

    temp
---
 .../flagon-userale-ext/src/background/index.ts     |    9 +-
 .../src/background/messages/config_change.ts       |    6 +-
 .../src/background/messages/http_session.ts        |    3 +
 .../flagon-userale-ext/src/background/ports/log.ts |   14 +-
 .../flagon-userale-ext/src/background/utils.ts     |   12 -
 .../packages/flagon-userale/build/esm/main.d.ts    |  235 +++++
 .../packages/flagon-userale/build/esm/main.mjs     | 1083 ++++++++++++++++++++
 .../packages/flagon-userale/build/esm/main.mjs.map |    1 +
 .../flagon-userale/src/callbacks/authHandler.ts    |   15 -
 .../src/callbacks/callbackHandler.ts               |   40 -
 .../flagon-userale/src/callbacks/headersHandler.ts |   20 -
 .../flagon-userale/src/callbacks/logHandler.ts     |   20 -
 .../userale/packages/flagon-userale/src/config.ts  |  160 ---
 .../packages/flagon-userale/src/configure.ts       |   16 +-
 ...itialSettings.temp.ts => getInitialSettings.ts} |    0
 .../userale/packages/flagon-userale/src/iife.ts    |    0
 .../packages/flagon-userale/src/logPackager.ts     |  276 -----
 .../packages/flagon-userale/src/logSender.ts       |  164 ---
 .../packages/flagon-userale/src/loggingEngine.ts   |  125 ---
 .../userale/packages/flagon-userale/src/main.ts    |  166 +++
 .../packages/flagon-userale/src/packageLogs.ts     |  471 +++++++++
 .../packages/flagon-userale/src/sendLogs.ts        |  156 +++
 .../userale/packages/flagon-userale/src/types.d.ts |   20 +-
 .../flagon-userale/src/utils/auth/index.ts         |   81 ++
 .../flagon-userale/src/utils/headers/index.ts      |   88 ++
 .../packages/flagon-userale/src/utils/index.ts     |   30 +
 .../userale/packages/flagon-userale/tsup.config.js |   28 -
 27 files changed, 2349 insertions(+), 890 deletions(-)

diff --git 
a/products/userale/packages/flagon-userale-ext/src/background/index.ts 
b/products/userale/packages/flagon-userale-ext/src/background/index.ts
index 4e9bada..ee5cda3 100644
--- a/products/userale/packages/flagon-userale-ext/src/background/index.ts
+++ b/products/userale/packages/flagon-userale-ext/src/background/index.ts
@@ -1,9 +1,12 @@
 import * as userale from "flagon-userale";
+import { getStoredOptions,} from "~/utils/storage";
 
 console.log("Service worker loaded!");
 
+//TODO apply logging url from getstoredoptions to userale.setup
+
 userale.setup();
 
-export function add_log(log) {
-    userale.log(log);
-}
+//TODO Create browser session id similar to how http session id is created and 
export it be used in background/ports/log.ts
+
+//TODO attach tab event listeners and add log them to userale. This can mostly 
be copied from the old code, but use .log() instead of .packagecustomlog()
\ No newline at end of file
diff --git 
a/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
 
b/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
index faf86fa..a4e71a9 100644
--- 
a/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
+++ 
b/products/userale/packages/flagon-userale-ext/src/background/messages/config_change.ts
@@ -1,8 +1,10 @@
 import type { PlasmoMessaging } from "@plasmohq/messaging";
-// import * as userale from "flagon-userale";
+import * as userale from "flagon-userale";
+import { getStoredOptions } from "~/utils/storage";
  
 const handler: PlasmoMessaging.MessageHandler = async (req, res) => {
-//   userale.config(req);
+  // call 
+  userale.options(req);
 }
  
 export default handler
\ No newline at end of file
diff --git 
a/products/userale/packages/flagon-userale-ext/src/background/messages/http_session.ts
 
b/products/userale/packages/flagon-userale-ext/src/background/messages/http_session.ts
index e69de29..1bb712b 100644
--- 
a/products/userale/packages/flagon-userale-ext/src/background/messages/http_session.ts
+++ 
b/products/userale/packages/flagon-userale-ext/src/background/messages/http_session.ts
@@ -0,0 +1,3 @@
+// This is a bit complicated, but once tab logs are added in main. You can 
create a mapping of tab ids (a browser construct) to http session ids (a 
userale construct).
+// The content script should send a message containing its http session id, 
and it should be added to the mapping here, then use the mapping in 
background/index.ts to set the http session field of tab logs.
+// This is also the least import part, so save it for last.
\ No newline at end of file
diff --git 
a/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts 
b/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
index a7aac4d..3a08910 100644
--- a/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
+++ b/products/userale/packages/flagon-userale-ext/src/background/ports/log.ts
@@ -1,14 +1,16 @@
 import type { PlasmoMessaging } from "@plasmohq/messaging";
-// import { add_log } from "~/background";
+import * as userale from "flagon-userale";
  
 const handler: PlasmoMessaging.PortHandler = async (req, res) => {
-  // console.log(req);
+  console.log(req);
+  // todo apply browser session id to logs
   // // log.browserSessionId = browserSessionId;
+  // todo filter logs based off filter url in getstorageoptions
   // // req = filterUrl(req);
-  // if (req) {
-  //   console.log(req);
-  //   add_log(req);
-  // }
+  if (req) {
+    console.log(req);
+    userale.log(req);
+  }
   
 }
  
diff --git 
a/products/userale/packages/flagon-userale-ext/src/background/utils.ts 
b/products/userale/packages/flagon-userale-ext/src/background/utils.ts
deleted file mode 100644
index b9f5a5c..0000000
--- a/products/userale/packages/flagon-userale-ext/src/background/utils.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-// import allowList;
-
-// function filterUrl(log) {
-//     if (allowRegex.test(log.pageUrl as string)) {
-//       return log;
-//     }
-//     return false;
-// }
-
-
-// const options: StoredOptions = await getStoredOptions();
-// const allowRegex: RegExp = new RegExp(options.allowList);
\ No newline at end of file
diff --git a/products/userale/packages/flagon-userale/build/esm/main.d.ts 
b/products/userale/packages/flagon-userale/build/esm/main.d.ts
new file mode 100644
index 0000000..5e56576
--- /dev/null
+++ b/products/userale/packages/flagon-userale/build/esm/main.d.ts
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+declare namespace Settings {
+  type Version = string | null;
+  type UserId = string | null;
+  type SessionId = string | null;
+  type UserFromParams = string | null;
+  type ToolName = string | null;
+  type AuthHeader = CallableFunction | string | null;
+  type CustomIndex = string | null;
+  type HeaderObject = { [key: string]: string };
+  type Headers = HeaderObject | null;
+  type ConfigValueTypes =
+    | string
+    | number
+    | boolean
+    | null
+    | Version
+    | UserId
+    | SessionId
+    | UserFromParams
+    | ToolName
+    | AuthHeader
+    | CustomIndex
+    | Headers;
+
+  type TimeFunction = (() => number) | ((ts: number) => number);
+
+  export interface DefaultConfig {
+    [key: string]: ConfigValueTypes;
+  }
+
+  export interface Config extends DefaultConfig {
+    autostart: boolean;
+    authHeader: AuthHeader;
+    browserSessionId: SessionId;
+    custIndex: CustomIndex;
+    headers: Headers;
+    httpSessionId: SessionId;
+    logCountThreshold: number;
+    logDetails: boolean;
+    on?: boolean;
+    resolution: number;
+    sessionId: SessionId;
+    time: TimeFunction;
+    toolName: ToolName;
+    toolVersion?: Version;
+    transmitInterval: number;
+    url: string;
+    userFromParams: UserFromParams;
+    useraleVersion: Version;
+    userId: UserId;
+    version?: Version;
+    websocketsEnabled?: boolean;
+  }
+
+  export interface IConfiguration extends Config {
+    getInstance(): Configuration;
+    configure(newConfig: Config): void;
+  }
+}
+
+// TODO: Switch to protobuf for managing log types
+declare namespace Logging {
+  type JSONObject = {
+    [key: string]:
+      | string
+      | number
+      | boolean
+      | null
+      | undefined
+      | JSONObject
+      | Array<string | number | boolean | null | JSONObject>;
+  };
+  export type Log = JSONObject; // TODO: Intersect this with the default log 
objects (raw & interval)
+  export type CustomLog = JSONObject;
+
+  export type DynamicDetailFunction<E extends Event = Event> = (
+    e: E,
+  ) => JSONObject;
+  export type StaticDetailFunction = () => JSONObject;
+}
+
+declare namespace Events {
+  type FormElement = HTMLInputElement | HTMLSelectElement | 
HTMLTextAreaElement;
+  type ChangeEvent = Event<FormElement>;
+  export type RawEvents =
+    | "dblclick"
+    | "mouseup"
+    | "mousedown"
+    | "dragstart"
+    | "dragend"
+    | "drag"
+    | "drop"
+    | "keydown";
+  export type IntervalEvents =
+    | "click"
+    | "focus"
+    | "blur"
+    | "input"
+    | "change"
+    | "mouseover"
+    | "submit";
+  export type WindowEvents = "load" | "blur" | "focus";
+  export type BufferedEvents = "wheel" | "scroll" | "resize";
+  export type RefreshEvents = "submit";
+  export type AllowedEvents =
+    | RawEvents
+    | IntervalEvents
+    | WindowEvents
+    | BufferedEvents
+    | RefreshEvents;
+
+  export type EventDetailsMap<T extends string> = Partial<{
+    [key in T]:
+      | Logging.DynamicDetailFunction<
+          | MouseEvent
+          | KeyboardEvent
+          | InputEvent
+          | Events.ChangeEvent
+          | WheelEvent
+        >
+      | Logging.StaticDetailFunction
+      | null;
+  }>;
+
+  export type EventBoolMap<T extends string> = Partial<{
+    [key in T]: boolean;
+  }>;
+}
+
+declare namespace Callbacks {
+  export type AuthCallback = () => string;
+  export type HeadersCallback = () => Settings.HeaderObject;
+
+  export type CallbackMap = {
+    [key in string]: CallableFunction;
+  };
+}
+
+/**
+ * Defines the way information is extracted from various events.
+ * Also defines which events we will listen to.
+ * @param  {Settings.Config} options UserALE Configuration object to read from.
+ * @param   {Events.AllowedEvents}    type of html event (e.g., 'click', 
'mouseover', etc.), such as passed to addEventListener methods.
+ */
+declare function defineCustomDetails(options: Settings.DefaultConfig, type: 
Events.AllowedEvents): Logging.DynamicDetailFunction | null | undefined;
+
+/**
+ * Registers the provided callback to be used when updating the auth header.
+ * @param {Callbacks.AuthCallback} callback Callback used to fetch the newest 
header. Should return a string.
+ * @returns {boolean} Whether the operation succeeded.
+ */
+declare function registerAuthCallback(callback: Callbacks.AuthCallback): 
boolean;
+
+/**
+ * Adds named callbacks to be executed when logging.
+ * @param  {Object } newCallbacks An object containing named callback 
functions.
+ */
+declare function addCallbacks(...newCallbacks: Record<symbol | string, 
CallableFunction>[]): Callbacks.CallbackMap;
+/**
+ * Removes callbacks by name.
+ * @param  {String[]} targetKeys A list of names of functions to remove.
+ */
+declare function removeCallbacks(targetKeys: string[]): void;
+/**
+ * Transforms the provided HTML event into a log and appends it to the log 
queue.
+ * @param  {Event} e         The event to be logged.
+ * @param  {Function} detailFcn The function to extract additional log 
parameters from the event.
+ * @return {boolean}           Whether the event was logged.
+ */
+declare function packageLog(e: Event, detailFcn?: 
Logging.DynamicDetailFunction | null): boolean;
+/**
+ * Packages the provided customLog to include standard meta data and appends 
it to the log queue.
+ * @param  {Logging.CustomLog} customLog        The behavior to be logged.
+ * @param  {Logging.DynamicDetailFunction} detailFcn     The function to 
extract additional log parameters from the event.
+ * @param  {boolean} userAction     Indicates user behavior (true) or system 
behavior (false)
+ * @return {boolean}           Whether the event was logged.
+ */
+declare function packageCustomLog(customLog: Logging.CustomLog, detailFcn: 
Logging.DynamicDetailFunction | Logging.StaticDetailFunction, userAction: 
boolean): boolean;
+/**
+ * Builds a string CSS selector from the provided element
+ * @param  {EventTarget} ele The element from which the selector is built.
+ * @return {string}     The CSS selector for the element, or Unknown if it 
can't be determined.
+ */
+declare function getSelector(ele: EventTarget): string;
+/**
+ * Builds an array of elements from the provided event target, to the root 
element.
+ * @param  {Event} e Event from which the path should be built.
+ * @return {HTMLElement[]}   Array of elements, starting at the event target, 
ending at the root element.
+ */
+declare function buildPath(e: Event): string[];
+
+declare let started: boolean;
+declare let wsock: WebSocket;
+
+declare const version: string;
+/**
+ * Used to start the logging process if the
+ * autostart configuration option is set to false.
+ */
+declare function start(): void;
+/**
+ * Halts the logging process. Logs will no longer be sent.
+ */
+declare function stop(): void;
+/**
+ * Updates the current configuration
+ * object with the provided values.
+ * @param  {Partial<Settings.Config>} newConfig The configuration options to 
use.
+ * @return {Settings.Config}           Returns the updated configuration.
+ */
+declare function options(newConfig: Partial<Settings.Config> | undefined): 
Settings.Config;
+/**
+ * Appends a log to the log queue.
+ * @param  {Logging.CustomLog} customLog The log to append.
+ * @return {boolean}          Whether the operation succeeded.
+ */
+declare function log(customLog: Logging.CustomLog | undefined): boolean;
+
+export { addCallbacks, buildPath, defineCustomDetails as details, getSelector, 
log, options, packageCustomLog, packageLog, registerAuthCallback, 
removeCallbacks, start, started, stop, version, wsock };
diff --git a/products/userale/packages/flagon-userale/build/esm/main.mjs 
b/products/userale/packages/flagon-userale/build/esm/main.mjs
new file mode 100644
index 0000000..40bdfcc
--- /dev/null
+++ b/products/userale/packages/flagon-userale/build/esm/main.mjs
@@ -0,0 +1,1083 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements. See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to you under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License. You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.*/
+// package.json
+var version = "2.4.0";
+
+// src/getInitialSettings.ts
+var sessionId = null;
+var httpSessionId = null;
+function getInitialSettings() {
+  if (sessionId === null) {
+    sessionId = getsessionId(
+      "userAlesessionId",
+      "session_" + String(Date.now())
+    );
+  }
+  if (httpSessionId === null) {
+    httpSessionId = getsessionId(
+      "userAleHttpSessionId",
+      generatehttpSessionId()
+    );
+  }
+  const script = document.currentScript || function() {
+    const scripts = document.getElementsByTagName("script");
+    return scripts[scripts.length - 1];
+  }();
+  const get = script ? script.getAttribute.bind(script) : function() {
+    return null;
+  };
+  const headers = get("data-headers");
+  const settings = {
+    authHeader: get("data-auth") || null,
+    autostart: get("data-autostart") === "false" ? false : true,
+    browserSessionId: null,
+    custIndex: get("data-index") || null,
+    headers: headers ? JSON.parse(headers) : null,
+    httpSessionId,
+    logCountThreshold: +(get("data-threshold") || 5),
+    logDetails: get("data-log-details") === "true" ? true : false,
+    resolution: +(get("data-resolution") || 500),
+    sessionId: get("data-session") || sessionId,
+    time: timeStampScale(document.createEvent("CustomEvent")),
+    toolName: get("data-tool") || null,
+    toolVersion: get("data-version") || null,
+    transmitInterval: +(get("data-interval") || 5e3),
+    url: get("data-url") || "http://localhost:8000";,
+    useraleVersion: get("data-userale-version") || null,
+    userFromParams: get("data-user-from-params") || null,
+    userId: get("data-user") || null
+  };
+  return settings;
+}
+function getsessionId(sessionKey, value) {
+  if (window.sessionStorage.getItem(sessionKey) === null) {
+    window.sessionStorage.setItem(sessionKey, JSON.stringify(value));
+    return value;
+  }
+  return JSON.parse(window.sessionStorage.getItem(sessionKey) || "");
+}
+function timeStampScale(e) {
+  let tsScaler;
+  if (e.timeStamp && e.timeStamp > 0) {
+    const delta = Date.now() - e.timeStamp;
+    if (delta < 0) {
+      tsScaler = function() {
+        return e.timeStamp / 1e3;
+      };
+    } else if (delta > e.timeStamp) {
+      const navStart = performance.timeOrigin;
+      tsScaler = function(ts) {
+        return ts + navStart;
+      };
+    } else {
+      tsScaler = function(ts) {
+        return ts;
+      };
+    }
+  } else {
+    tsScaler = function() {
+      return Date.now();
+    };
+  }
+  return tsScaler;
+}
+function generatehttpSessionId() {
+  const len = 32;
+  const arr = new Uint8Array(len / 2);
+  window.crypto.getRandomValues(arr);
+  return Array.from(arr, (dec) => {
+    return dec.toString(16).padStart(2, "0");
+  }).join("");
+}
+
+// src/configure.ts
+var _Configuration = class {
+  constructor() {
+    this.autostart = false;
+    this.authHeader = null;
+    this.browserSessionId = null;
+    this.custIndex = null;
+    this.headers = null;
+    this.httpSessionId = null;
+    this.logCountThreshold = 0;
+    this.logDetails = false;
+    this.on = false;
+    this.resolution = 0;
+    this.sessionId = null;
+    this.time = () => Date.now();
+    this.toolName = null;
+    this.toolVersion = null;
+    this.transmitInterval = 0;
+    this.url = "";
+    this.userFromParams = null;
+    this.useraleVersion = null;
+    this.userId = null;
+    this.version = null;
+    this.websocketsEnabled = false;
+    if (_Configuration.instance === null) {
+      this.initialize();
+    }
+  }
+  static getInstance() {
+    if (_Configuration.instance === null) {
+      _Configuration.instance = new _Configuration();
+    }
+    return _Configuration.instance;
+  }
+  initialize() {
+    const settings = getInitialSettings();
+    this.update(settings);
+  }
+  reset() {
+    this.initialize();
+  }
+  update(newConfig) {
+    Object.keys(newConfig).forEach((option) => {
+      if (option === "userFromParams") {
+        const userParamString = newConfig[option];
+        const userId = userParamString ? 
_Configuration.getUserIdFromParams(userParamString) : null;
+        if (userId) {
+          this["userId"] = userId;
+        }
+      }
+      const hasNewUserFromParams = newConfig["userFromParams"];
+      const willNullifyUserId = option === "userId" && newConfig[option] === 
null;
+      if (willNullifyUserId && hasNewUserFromParams) {
+        return;
+      }
+      const newOption = newConfig[option];
+      if (newOption !== void 0) {
+        this[option] = newOption;
+      }
+    });
+  }
+  static getUserIdFromParams(param) {
+    const userField = param;
+    const regex = new RegExp("[?&]" + userField + "(=([^&#]*)|&|#|$)");
+    const results = window.location.href.match(regex);
+    if (results && results[2]) {
+      return decodeURIComponent(results[2].replace(/\+/g, " "));
+    }
+    return null;
+  }
+};
+var Configuration = _Configuration;
+Configuration.instance = null;
+
+// 
../../node_modules/.pnpm/detect-browser@5.3.0/node_modules/detect-browser/es/index.js
+var __spreadArray = function(to, from, pack) {
+  if (pack || arguments.length === 2)
+    for (var i = 0, l = from.length, ar; i < l; i++) {
+      if (ar || !(i in from)) {
+        if (!ar)
+          ar = Array.prototype.slice.call(from, 0, i);
+        ar[i] = from[i];
+      }
+    }
+  return to.concat(ar || Array.prototype.slice.call(from));
+};
+var BrowserInfo = function() {
+  function BrowserInfo2(name, version3, os) {
+    this.name = name;
+    this.version = version3;
+    this.os = os;
+    this.type = "browser";
+  }
+  return BrowserInfo2;
+}();
+var NodeInfo = function() {
+  function NodeInfo2(version3) {
+    this.version = version3;
+    this.type = "node";
+    this.name = "node";
+    this.os = process.platform;
+  }
+  return NodeInfo2;
+}();
+var SearchBotDeviceInfo = function() {
+  function SearchBotDeviceInfo2(name, version3, os, bot) {
+    this.name = name;
+    this.version = version3;
+    this.os = os;
+    this.bot = bot;
+    this.type = "bot-device";
+  }
+  return SearchBotDeviceInfo2;
+}();
+var BotInfo = function() {
+  function BotInfo2() {
+    this.type = "bot";
+    this.bot = true;
+    this.name = "bot";
+    this.version = null;
+    this.os = null;
+  }
+  return BotInfo2;
+}();
+var ReactNativeInfo = function() {
+  function ReactNativeInfo2() {
+    this.type = "react-native";
+    this.name = "react-native";
+    this.version = null;
+    this.os = null;
+  }
+  return ReactNativeInfo2;
+}();
+var SEARCHBOX_UA_REGEX = 
/alexa|bot|crawl(er|ing)|facebookexternalhit|feedburner|google web 
preview|nagios|postrank|pingdom|slurp|spider|yahoo!|yandex/;
+var SEARCHBOT_OS_REGEX = 
/(nuhk|curl|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask\ 
Jeeves\/Teoma|ia_archiver)/;
+var REQUIRED_VERSION_PARTS = 3;
+var userAgentRules = [
+  ["aol", /AOLShield\/([0-9\._]+)/],
+  ["edge", /Edge\/([0-9\._]+)/],
+  ["edge-ios", /EdgiOS\/([0-9\._]+)/],
+  ["yandexbrowser", /YaBrowser\/([0-9\._]+)/],
+  ["kakaotalk", /KAKAOTALK\s([0-9\.]+)/],
+  ["samsung", /SamsungBrowser\/([0-9\.]+)/],
+  ["silk", /\bSilk\/([0-9._-]+)\b/],
+  ["miui", /MiuiBrowser\/([0-9\.]+)$/],
+  ["beaker", /BeakerBrowser\/([0-9\.]+)/],
+  ["edge-chromium", /EdgA?\/([0-9\.]+)/],
+  [
+    "chromium-webview",
+    /(?!Chrom.*OPR)wv\).*Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/
+  ],
+  ["chrome", /(?!Chrom.*OPR)Chrom(?:e|ium)\/([0-9\.]+)(:?\s|$)/],
+  ["phantomjs", /PhantomJS\/([0-9\.]+)(:?\s|$)/],
+  ["crios", /CriOS\/([0-9\.]+)(:?\s|$)/],
+  ["firefox", /Firefox\/([0-9\.]+)(?:\s|$)/],
+  ["fxios", /FxiOS\/([0-9\.]+)/],
+  ["opera-mini", /Opera Mini.*Version\/([0-9\.]+)/],
+  ["opera", /Opera\/([0-9\.]+)(?:\s|$)/],
+  ["opera", /OPR\/([0-9\.]+)(:?\s|$)/],
+  ["pie", /^Microsoft Pocket Internet Explorer\/(\d+\.\d+)$/],
+  ["pie", /^Mozilla\/\d\.\d+\s\(compatible;\s(?:MSP?IE|MSInternet Explorer) 
(\d+\.\d+);.*Windows CE.*\)$/],
+  ["netfront", /^Mozilla\/\d\.\d+.*NetFront\/(\d.\d)/],
+  ["ie", /Trident\/7\.0.*rv\:([0-9\.]+).*\).*Gecko$/],
+  ["ie", /MSIE\s([0-9\.]+);.*Trident\/[4-7].0/],
+  ["ie", /MSIE\s(7\.0)/],
+  ["bb10", /BB10;\sTouch.*Version\/([0-9\.]+)/],
+  ["android", /Android\s([0-9\.]+)/],
+  ["ios", /Version\/([0-9\._]+).*Mobile.*Safari.*/],
+  ["safari", /Version\/([0-9\._]+).*Safari/],
+  ["facebook", /FB[AS]V\/([0-9\.]+)/],
+  ["instagram", /Instagram\s([0-9\.]+)/],
+  ["ios-webview", /AppleWebKit\/([0-9\.]+).*Mobile/],
+  ["ios-webview", /AppleWebKit\/([0-9\.]+).*Gecko\)$/],
+  ["curl", /^curl\/([0-9\.]+)$/],
+  ["searchbot", SEARCHBOX_UA_REGEX]
+];
+var operatingSystemRules = [
+  ["iOS", /iP(hone|od|ad)/],
+  ["Android OS", /Android/],
+  ["BlackBerry OS", /BlackBerry|BB10/],
+  ["Windows Mobile", /IEMobile/],
+  ["Amazon OS", /Kindle/],
+  ["Windows 3.11", /Win16/],
+  ["Windows 95", /(Windows 95)|(Win95)|(Windows_95)/],
+  ["Windows 98", /(Windows 98)|(Win98)/],
+  ["Windows 2000", /(Windows NT 5.0)|(Windows 2000)/],
+  ["Windows XP", /(Windows NT 5.1)|(Windows XP)/],
+  ["Windows Server 2003", /(Windows NT 5.2)/],
+  ["Windows Vista", /(Windows NT 6.0)/],
+  ["Windows 7", /(Windows NT 6.1)/],
+  ["Windows 8", /(Windows NT 6.2)/],
+  ["Windows 8.1", /(Windows NT 6.3)/],
+  ["Windows 10", /(Windows NT 10.0)/],
+  ["Windows ME", /Windows ME/],
+  ["Windows CE", /Windows CE|WinCE|Microsoft Pocket Internet Explorer/],
+  ["Open BSD", /OpenBSD/],
+  ["Sun OS", /SunOS/],
+  ["Chrome OS", /CrOS/],
+  ["Linux", /(Linux)|(X11)/],
+  ["Mac OS", /(Mac_PowerPC)|(Macintosh)/],
+  ["QNX", /QNX/],
+  ["BeOS", /BeOS/],
+  ["OS/2", /OS\/2/]
+];
+function detect(userAgent) {
+  if (!!userAgent) {
+    return parseUserAgent(userAgent);
+  }
+  if (typeof document === "undefined" && typeof navigator !== "undefined" && 
navigator.product === "ReactNative") {
+    return new ReactNativeInfo();
+  }
+  if (typeof navigator !== "undefined") {
+    return parseUserAgent(navigator.userAgent);
+  }
+  return getNodeVersion();
+}
+function matchUserAgent(ua) {
+  return ua !== "" && userAgentRules.reduce(function(matched, _a) {
+    var browser = _a[0], regex = _a[1];
+    if (matched) {
+      return matched;
+    }
+    var uaMatch = regex.exec(ua);
+    return !!uaMatch && [browser, uaMatch];
+  }, false);
+}
+function parseUserAgent(ua) {
+  var matchedRule = matchUserAgent(ua);
+  if (!matchedRule) {
+    return null;
+  }
+  var name = matchedRule[0], match = matchedRule[1];
+  if (name === "searchbot") {
+    return new BotInfo();
+  }
+  var versionParts = match[1] && 
match[1].split(".").join("_").split("_").slice(0, 3);
+  if (versionParts) {
+    if (versionParts.length < REQUIRED_VERSION_PARTS) {
+      versionParts = __spreadArray(__spreadArray([], versionParts, true), 
createVersionParts(REQUIRED_VERSION_PARTS - versionParts.length), true);
+    }
+  } else {
+    versionParts = [];
+  }
+  var version3 = versionParts.join(".");
+  var os = detectOS(ua);
+  var searchBotMatch = SEARCHBOT_OS_REGEX.exec(ua);
+  if (searchBotMatch && searchBotMatch[1]) {
+    return new SearchBotDeviceInfo(name, version3, os, searchBotMatch[1]);
+  }
+  return new BrowserInfo(name, version3, os);
+}
+function detectOS(ua) {
+  for (var ii = 0, count = operatingSystemRules.length; ii < count; ii++) {
+    var _a = operatingSystemRules[ii], os = _a[0], regex = _a[1];
+    var match = regex.exec(ua);
+    if (match) {
+      return os;
+    }
+  }
+  return null;
+}
+function getNodeVersion() {
+  var isNode = typeof process !== "undefined" && process.version;
+  return isNode ? new NodeInfo(process.version.slice(1)) : null;
+}
+function createVersionParts(count) {
+  var output = [];
+  for (var ii = 0; ii < count; ii++) {
+    output.push("0");
+  }
+  return output;
+}
+
+// src/packageLogs.ts
+var browserInfo = detect();
+var logs;
+var config;
+var intervalId;
+var intervalType;
+var intervalPath;
+var intervalTimer;
+var intervalCounter;
+var intervalLog;
+var filterHandler = null;
+var mapHandler = null;
+var cbHandlers = {};
+function addCallbacks(...newCallbacks) {
+  newCallbacks.forEach((source) => {
+    let descriptors = {};
+    descriptors = Object.keys(source).reduce((descriptors2, key) => {
+      descriptors2[key] = Object.getOwnPropertyDescriptor(source, key);
+      return descriptors2;
+    }, descriptors);
+    Object.getOwnPropertySymbols(source).forEach((sym) => {
+      const descriptor = Object.getOwnPropertyDescriptor(source, sym);
+      if (descriptor?.enumerable) {
+        descriptors[sym] = descriptor;
+      }
+    });
+    Object.defineProperties(cbHandlers, descriptors);
+  });
+  return cbHandlers;
+}
+function removeCallbacks(targetKeys) {
+  targetKeys.forEach((key) => {
+    if (Object.prototype.hasOwnProperty.call(cbHandlers, key)) {
+      delete cbHandlers[key];
+    }
+  });
+}
+function initPackager(newLogs, newConfig) {
+  logs = newLogs;
+  config = newConfig;
+  cbHandlers = {};
+  intervalId = null;
+  intervalType = null;
+  intervalPath = null;
+  intervalTimer = null;
+  intervalCounter = 0;
+  intervalLog = null;
+}
+function packageLog(e, detailFcn) {
+  if (!config.on) {
+    return false;
+  }
+  let details = null;
+  if (detailFcn) {
+    details = detailFcn(e);
+  }
+  const timeFields = extractTimeFields(
+    e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now()
+  );
+  let log2 = {
+    target: e.target ? getSelector(e.target) : null,
+    path: buildPath(e),
+    pageUrl: window.location.href,
+    pageTitle: document.title,
+    pageReferrer: document.referrer,
+    browser: detectBrowser(),
+    clientTime: timeFields.milli,
+    microTime: timeFields.micro,
+    location: getLocation(e),
+    scrnRes: getScreenRes(),
+    type: e.type,
+    logType: "raw",
+    userAction: true,
+    details,
+    userId: config.userId,
+    toolVersion: config.toolVersion,
+    toolName: config.toolName,
+    useraleVersion: config.useraleVersion,
+    sessionId: config.sessionId,
+    httpSessionId: config.httpSessionId,
+    browserSessionId: config.browserSessionId,
+    attributes: buildAttrs(e),
+    style: buildCSS(e)
+  };
+  if (typeof filterHandler === "function" && !filterHandler(log2)) {
+    return false;
+  }
+  if (typeof mapHandler === "function") {
+    log2 = mapHandler(log2, e);
+  }
+  for (const func of Object.values(cbHandlers)) {
+    if (typeof func === "function") {
+      log2 = func(log2, e);
+      if (!log2) {
+        return false;
+      }
+    }
+  }
+  logs.push(log2);
+  return true;
+}
+function packageCustomLog(customLog, detailFcn, userAction) {
+  if (!config.on) {
+    return false;
+  }
+  let details = null;
+  if (detailFcn.length === 0) {
+    const staticDetailFcn = detailFcn;
+    details = staticDetailFcn();
+  }
+  const metaData = {
+    pageUrl: window.location.href,
+    pageTitle: document.title,
+    pageReferrer: document.referrer,
+    browser: detectBrowser(),
+    clientTime: Date.now(),
+    scrnRes: getScreenRes(),
+    logType: "custom",
+    userAction,
+    details,
+    userId: config.userId,
+    toolVersion: config.toolVersion,
+    toolName: config.toolName,
+    useraleVersion: config.useraleVersion,
+    sessionId: config.sessionId,
+    httpSessionId: config.httpSessionId,
+    browserSessionId: config.browserSessionId
+  };
+  let log2 = Object.assign(metaData, customLog);
+  if (typeof filterHandler === "function" && !filterHandler(log2)) {
+    return false;
+  }
+  if (typeof mapHandler === "function") {
+    log2 = mapHandler(log2);
+  }
+  for (const func of Object.values(cbHandlers)) {
+    if (typeof func === "function") {
+      log2 = func(log2, null);
+      if (!log2) {
+        return false;
+      }
+    }
+  }
+  logs.push(log2);
+  return true;
+}
+function extractTimeFields(timeStamp) {
+  return {
+    milli: Math.floor(timeStamp),
+    micro: Number((timeStamp % 1).toFixed(3))
+  };
+}
+function packageIntervalLog(e) {
+  try {
+    const target = e.target ? getSelector(e.target) : null;
+    const path = buildPath(e);
+    const type = e.type;
+    const timestamp = Math.floor(
+      e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now()
+    );
+    if (intervalId == null) {
+      intervalId = target;
+      intervalType = type;
+      intervalPath = path;
+      intervalTimer = timestamp;
+      intervalCounter = 0;
+    }
+    if ((intervalId !== target || intervalType !== type) && intervalTimer) {
+      intervalLog = {
+        target: intervalId,
+        path: intervalPath,
+        pageUrl: window.location.href,
+        pageTitle: document.title,
+        pageReferrer: document.referrer,
+        browser: detectBrowser(),
+        count: intervalCounter,
+        duration: timestamp - intervalTimer,
+        startTime: intervalTimer,
+        endTime: timestamp,
+        type: intervalType,
+        logType: "interval",
+        targetChange: intervalId !== target,
+        typeChange: intervalType !== type,
+        userAction: false,
+        userId: config.userId,
+        toolVersion: config.toolVersion,
+        toolName: config.toolName,
+        useraleVersion: config.useraleVersion,
+        sessionId: config.sessionId,
+        httpSessionId: config.httpSessionId,
+        browserSessionId: config.browserSessionId
+      };
+      if (typeof filterHandler === "function" && !filterHandler(intervalLog)) {
+        return false;
+      }
+      if (typeof mapHandler === "function") {
+        intervalLog = mapHandler(intervalLog, e);
+      }
+      for (const func of Object.values(cbHandlers)) {
+        if (typeof func === "function") {
+          intervalLog = func(intervalLog, null);
+          if (!intervalLog) {
+            return false;
+          }
+        }
+      }
+      if (intervalLog)
+        logs.push(intervalLog);
+      intervalId = target;
+      intervalType = type;
+      intervalPath = path;
+      intervalTimer = timestamp;
+      intervalCounter = 0;
+    }
+    if (intervalId == target && intervalType == type && intervalCounter) {
+      intervalCounter = intervalCounter + 1;
+    }
+    return true;
+  } catch {
+    return false;
+  }
+}
+function getLocation(e) {
+  if (e instanceof MouseEvent) {
+    if (e.pageX != null) {
+      return { x: e.pageX, y: e.pageY };
+    } else if (e.clientX != null) {
+      return {
+        x: document.documentElement.scrollLeft + e.clientX,
+        y: document.documentElement.scrollTop + e.clientY
+      };
+    }
+  } else {
+    return { x: null, y: null };
+  }
+}
+function getScreenRes() {
+  return { width: window.innerWidth, height: window.innerHeight };
+}
+function getSelector(ele) {
+  if (ele instanceof HTMLElement || ele instanceof Element) {
+    if (ele.localName) {
+      return ele.localName + (ele.id ? "#" + ele.id : "") + (ele.className ? 
"." + ele.className : "");
+    } else if (ele.nodeName) {
+      return ele.nodeName + (ele.id ? "#" + ele.id : "") + (ele.className ? 
"." + ele.className : "");
+    }
+  } else if (ele instanceof Document) {
+    return "#document";
+  } else if (ele === globalThis) {
+    return "Window";
+  }
+  return "Unknown";
+}
+function buildPath(e) {
+  const path = e.composedPath();
+  return selectorizePath(path);
+}
+function selectorizePath(path) {
+  let i = 0;
+  let pathEle;
+  const pathSelectors = [];
+  while (pathEle = path[i]) {
+    pathSelectors.push(getSelector(pathEle));
+    ++i;
+    pathEle = path[i];
+  }
+  return pathSelectors;
+}
+function detectBrowser() {
+  return {
+    browser: browserInfo ? browserInfo.name : "",
+    version: browserInfo ? browserInfo.version : ""
+  };
+}
+function buildAttrs(e) {
+  const attributes = {};
+  const attributeBlackList = ["style"];
+  if (e.target && e.target instanceof Element) {
+    for (const attr of e.target.attributes) {
+      if (attributeBlackList.includes(attr.name))
+        continue;
+      let val = attr.value;
+      try {
+        val = JSON.parse(val);
+      } catch (error) {
+      }
+      attributes[attr.name] = val;
+    }
+  }
+  return attributes;
+}
+function buildCSS(e) {
+  const properties = {};
+  if (e.target && e.target instanceof HTMLElement) {
+    const styleObj = e.target.style;
+    for (let i = 0; i < styleObj.length; i++) {
+      const prop = styleObj[i];
+      properties[prop] = styleObj.getPropertyValue(prop);
+    }
+  }
+  return properties;
+}
+
+// src/attachHandlers.ts
+var events;
+var bufferBools;
+var bufferedEvents;
+var refreshEvents;
+var intervalEvents = [
+  "click",
+  "focus",
+  "blur",
+  "input",
+  "change",
+  "mouseover",
+  "submit"
+];
+var windowEvents = ["load", "blur", "focus"];
+function extractMouseDetails(e) {
+  return {
+    clicks: e.detail,
+    ctrl: e.ctrlKey,
+    alt: e.altKey,
+    shift: e.shiftKey,
+    meta: e.metaKey
+  };
+}
+function extractKeyboardDetails(e) {
+  return {
+    key: e.key,
+    code: e.code,
+    ctrl: e.ctrlKey,
+    alt: e.altKey,
+    shift: e.shiftKey,
+    meta: e.metaKey
+  };
+}
+function extractChangeDetails(e) {
+  return {
+    value: e.target.value
+  };
+}
+function extractWheelDetails(e) {
+  return {
+    x: e.deltaX,
+    y: e.deltaY,
+    z: e.deltaZ
+  };
+}
+function extractScrollDetails() {
+  return {
+    x: window.scrollX,
+    y: window.scrollY
+  };
+}
+function extractResizeDetails() {
+  return {
+    width: window.outerWidth,
+    height: window.outerHeight
+  };
+}
+function defineDetails(config3) {
+  events = {
+    click: extractMouseDetails,
+    dblclick: extractMouseDetails,
+    mousedown: extractMouseDetails,
+    mouseup: extractMouseDetails,
+    focus: null,
+    blur: null,
+    input: config3.logDetails ? extractKeyboardDetails : null,
+    change: config3.logDetails ? extractChangeDetails : null,
+    dragstart: null,
+    dragend: null,
+    drag: null,
+    drop: null,
+    keydown: config3.logDetails ? extractKeyboardDetails : null,
+    mouseover: null
+  };
+  bufferBools = {};
+  bufferedEvents = {
+    wheel: extractWheelDetails,
+    scroll: extractScrollDetails,
+    resize: extractResizeDetails
+  };
+  refreshEvents = {
+    submit: null
+  };
+}
+function defineCustomDetails(options2, type) {
+  const eventType = {
+    click: extractMouseDetails,
+    dblclick: extractMouseDetails,
+    mousedown: extractMouseDetails,
+    mouseup: extractMouseDetails,
+    focus: null,
+    blur: null,
+    load: null,
+    input: options2.logDetails ? extractKeyboardDetails : null,
+    change: options2.logDetails ? extractChangeDetails : null,
+    dragstart: null,
+    dragend: null,
+    drag: null,
+    drop: null,
+    keydown: options2.logDetails ? extractKeyboardDetails : null,
+    mouseover: null,
+    wheel: extractWheelDetails,
+    scroll: extractScrollDetails,
+    resize: extractResizeDetails,
+    submit: null
+  };
+  return eventType[type];
+}
+function attachHandlers(config3) {
+  try {
+    defineDetails(config3);
+    Object.keys(events).forEach(function(ev) {
+      document.addEventListener(
+        ev,
+        function(e) {
+          packageLog(e, events[ev]);
+        },
+        true
+      );
+    });
+    intervalEvents.forEach(function(ev) {
+      document.addEventListener(
+        ev,
+        function(e) {
+          packageIntervalLog(e);
+        },
+        true
+      );
+    });
+    Object.keys(bufferedEvents).forEach(
+      function(ev) {
+        bufferBools[ev] = true;
+        window.addEventListener(
+          ev,
+          function(e) {
+            if (bufferBools[ev]) {
+              bufferBools[ev] = false;
+              packageLog(e, bufferedEvents[ev]);
+              setTimeout(function() {
+                bufferBools[ev] = true;
+              }, config3.resolution);
+            }
+          },
+          true
+        );
+      }
+    );
+    Object.keys(refreshEvents).forEach(
+      function(ev) {
+        document.addEventListener(
+          ev,
+          function(e) {
+            packageLog(e, events[ev]);
+          },
+          true
+        );
+      }
+    );
+    windowEvents.forEach(function(ev) {
+      window.addEventListener(
+        ev,
+        function(e) {
+          packageLog(e, function() {
+            return { window: true };
+          });
+        },
+        true
+      );
+    });
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+// src/utils/auth/index.ts
+var authCallback = null;
+function updateAuthHeader(config3) {
+  if (authCallback) {
+    try {
+      config3.authHeader = authCallback();
+    } catch (e) {
+      console.error(`Error encountered while setting the auth header: ${e}`);
+    }
+  }
+}
+function registerAuthCallback(callback) {
+  try {
+    verifyCallback(callback);
+    authCallback = callback;
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+function verifyCallback(callback) {
+  if (typeof callback !== "function") {
+    throw new Error("Userale auth callback must be a function");
+  }
+  const result = callback();
+  if (typeof result !== "string") {
+    throw new Error("Userale auth callback must return a string");
+  }
+}
+
+// src/utils/headers/index.ts
+var headersCallback = null;
+function updateCustomHeaders(config3) {
+  if (headersCallback) {
+    try {
+      config3.headers = headersCallback();
+    } catch (e) {
+      console.error(`Error encountered while setting the headers: ${e}`);
+    }
+  }
+}
+
+// src/sendLogs.ts
+var sendIntervalId;
+function initSender(logs3, config3) {
+  if (sendIntervalId) {
+    clearInterval(sendIntervalId);
+  }
+  sendIntervalId = sendOnInterval(logs3, config3);
+  sendOnClose(logs3, config3);
+}
+function sendOnInterval(logs3, config3) {
+  return setInterval(function() {
+    if (!config3.on) {
+      return;
+    }
+    if (logs3.length >= config3.logCountThreshold) {
+      sendLogs(logs3.slice(0), config3, 0);
+      logs3.splice(0);
+    }
+  }, config3.transmitInterval);
+}
+function sendOnClose(logs3, config3) {
+  window.addEventListener("pagehide", function() {
+    if (!config3.on) {
+      return;
+    }
+    if (logs3.length > 0) {
+      if (config3.websocketsEnabled) {
+        const data = JSON.stringify(logs3);
+        wsock.send(data);
+      } else {
+        const headers = new Headers();
+        headers.set("Content-Type", "applicaiton/json;charset=UTF-8");
+        if (config3.authHeader) {
+          headers.set("Authorization", config3.authHeader.toString());
+        }
+        fetch(config3.url, {
+          keepalive: true,
+          method: "POST",
+          headers,
+          body: JSON.stringify(logs3)
+        }).catch((error) => {
+          console.error(error);
+        });
+      }
+      logs3.splice(0);
+    }
+  });
+}
+function sendLogs(logs3, config3, retries) {
+  const data = JSON.stringify(logs3);
+  if (config3.websocketsEnabled) {
+    wsock.send(data);
+  } else {
+    const req = new XMLHttpRequest();
+    req.open("POST", config3.url);
+    updateAuthHeader(config3);
+    if (config3.authHeader) {
+      req.setRequestHeader(
+        "Authorization",
+        typeof config3.authHeader === "function" ? config3.authHeader() : 
config3.authHeader
+      );
+    }
+    req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
+    updateCustomHeaders(config3);
+    if (config3.headers) {
+      Object.entries(config3.headers).forEach(([header, value]) => {
+        req.setRequestHeader(header, value);
+      });
+    }
+    req.onreadystatechange = function() {
+      if (req.readyState === 4 && req.status !== 200) {
+        if (retries > 0) {
+          sendLogs(logs3, config3, retries--);
+        }
+      }
+    };
+    req.send(data);
+  }
+}
+
+// src/main.ts
+var config2 = Configuration.getInstance();
+var logs2 = [];
+var startLoadTimestamp = Date.now();
+var endLoadTimestamp;
+try {
+  window.onload = function() {
+    endLoadTimestamp = Date.now();
+  };
+} catch (error) {
+  endLoadTimestamp = Date.now();
+}
+var started = false;
+var wsock;
+config2.update({
+  useraleVersion: version
+});
+initPackager(logs2, config2);
+getWebsocketsEnabled(config2);
+if (config2.autostart) {
+  setup(config2);
+}
+function setup(config3) {
+  if (!started) {
+    setTimeout(function() {
+      let state;
+      try {
+        state = document.readyState;
+      } catch (error) {
+        initSender(logs2, config3);
+      }
+      if (config3.autostart && (state === "interactive" || state === 
"complete")) {
+        attachHandlers(config3);
+        initSender(logs2, config3);
+        started = config3.on = true;
+        packageCustomLog(
+          {
+            type: "load",
+            details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp }
+          },
+          () => ({}),
+          false
+        );
+      } else {
+        setup(config3);
+      }
+    }, 100);
+  }
+}
+function getWebsocketsEnabled(config3) {
+  wsock = new WebSocket(config3.url.replace("http://";, "ws://"));
+  wsock.onerror = () => {
+    console.log("no websockets detected");
+  };
+  wsock.onopen = () => {
+    console.log("connection established with websockets");
+    config3.websocketsEnabled = true;
+  };
+  wsock.onclose = () => {
+    sendOnClose(logs2, config3);
+  };
+}
+var version2 = version;
+function start() {
+  if (!started || config2.autostart === false) {
+    started = config2.on = true;
+    config2.update({ autostart: true });
+  }
+}
+function stop() {
+  started = config2.on = false;
+  config2.update({ autostart: false });
+}
+function options(newConfig) {
+  if (newConfig) {
+    config2.update(newConfig);
+  }
+  return config2;
+}
+function log(customLog) {
+  if (customLog) {
+    logs2.push(customLog);
+    return true;
+  } else {
+    return false;
+  }
+}
+export {
+  addCallbacks,
+  buildPath,
+  defineCustomDetails as details,
+  getSelector,
+  log,
+  options,
+  packageCustomLog,
+  packageLog,
+  registerAuthCallback,
+  removeCallbacks,
+  start,
+  started,
+  stop,
+  version2 as version,
+  wsock
+};
+//# sourceMappingURL=main.mjs.map
\ No newline at end of file
diff --git a/products/userale/packages/flagon-userale/build/esm/main.mjs.map 
b/products/userale/packages/flagon-userale/build/esm/main.mjs.map
new file mode 100644
index 0000000..66a020b
--- /dev/null
+++ b/products/userale/packages/flagon-userale/build/esm/main.mjs.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../src/getInitialSettings.ts","../../src/configure.ts","../../../../node_modules/.pnpm/detect-browser@5.3.0/node_modules/detect-browser/es/index.js","../../src/packageLogs.ts","../../src/attachHandlers.ts","../../src/utils/auth/index.ts","../../src/utils/headers/index.ts","../../src/sendLogs.ts","../../src/main.ts"],"sourcesContent":["/*\n
 * Licensed to the Apache Software Foundation (ASF) under one or more\n * 
contributor license agreements.  See the NOTICE f [...]
\ No newline at end of file
diff --git 
a/products/userale/packages/flagon-userale/src/callbacks/authHandler.ts 
b/products/userale/packages/flagon-userale/src/callbacks/authHandler.ts
deleted file mode 100644
index 1bd36e8..0000000
--- a/products/userale/packages/flagon-userale/src/callbacks/authHandler.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { CallbackHandler } from "@/callbacks/callbackHandler";
-import { Configuration } from "@/configure";
-
-export const authHandler = new CallbackHandler<string>({
-  getValue: () => "",
-  validate: (value) => {
-    if (typeof value !== "string") {
-      throw new Error("Auth callback must return a string");
-    }
-  },
-  setValue: (config: Configuration, value: string) => {
-    config.authHeader = value;
-  },
-  description: "auth",
-});
diff --git 
a/products/userale/packages/flagon-userale/src/callbacks/callbackHandler.ts 
b/products/userale/packages/flagon-userale/src/callbacks/callbackHandler.ts
deleted file mode 100644
index c983a80..0000000
--- a/products/userale/packages/flagon-userale/src/callbacks/callbackHandler.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import type { CallbackHandlerOptions } from "@/types"
-
-export class CallbackHandler<T> {
-  private callback: (() => T) | null = null;
-  private readonly options: CallbackHandlerOptions<T>;
-
-  constructor(options: CallbackHandlerOptions<T>) {
-    this.options = options;
-  }
-
-  update(config: any): void {
-    if (this.callback) {
-      try {
-        const value = this.callback();
-        this.options.validate(value);
-        this.options.setValue(config, value);
-      } catch (e) {
-        console.error(`Error in ${this.options.description} callback: ${e}`);
-      }
-    }
-  }
-
-  register(callback: () => T): boolean {
-    try {
-      if (typeof callback !== "function") {
-        throw new Error(`${this.options.description} must be a function`);
-      }
-      const result = callback();
-      this.options.validate(result);
-      this.callback = callback;
-      return true;
-    } catch {
-      return false;
-    }
-  }
-
-  reset(): void {
-    this.callback = null;
-  }
-}
diff --git 
a/products/userale/packages/flagon-userale/src/callbacks/headersHandler.ts 
b/products/userale/packages/flagon-userale/src/callbacks/headersHandler.ts
deleted file mode 100644
index 9c5cb21..0000000
--- a/products/userale/packages/flagon-userale/src/callbacks/headersHandler.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { CallbackHandler } from "@/callbacks/callbackHandler";
-import { Config } from "@/config";
-
-export const headersHandler = new CallbackHandler<Record<string, string>>({
-  getValue: () => ({}),
-  validate: (value) => {
-    if (typeof value !== "object" || value === null) {
-      throw new Error("Headers callback must return an object");
-    }
-    for (const [k, v] of Object.entries(value)) {
-      if (typeof k !== "string" || typeof v !== "string") {
-        throw new Error("Headers must have string keys and values");
-      }
-    }
-  },
-  setValue: (config: Config, value: Record<string, string>) => {
-    config.headers = value;
-  },
-  description: "headers",
-});
diff --git 
a/products/userale/packages/flagon-userale/src/callbacks/logHandler.ts 
b/products/userale/packages/flagon-userale/src/callbacks/logHandler.ts
deleted file mode 100644
index 93845c1..0000000
--- a/products/userale/packages/flagon-userale/src/callbacks/logHandler.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { CallbackHandler } from "@/callbacks/callbackHandler";
-import { Configuration } from "@/configure";
-
-export const logHandler = new CallbackHandler<Record<string, string>>({
-  getValue: () => ({}),
-  validate: (value) => {
-    if (typeof value !== "object" || value === null) {
-      throw new Error("Headers callback must return an object");
-    }
-    for (const [k, v] of Object.entries(value)) {
-      if (typeof k !== "string" || typeof v !== "string") {
-        throw new Error("Headers must have string keys and values");
-      }
-    }
-  },
-  setValue: (config: Configuration, value: Record<string, string>) => {
-    config.headers = value;
-  },
-  description: "headers",
-});
diff --git a/products/userale/packages/flagon-userale/src/config.ts 
b/products/userale/packages/flagon-userale/src/config.ts
deleted file mode 100644
index 3da51af..0000000
--- a/products/userale/packages/flagon-userale/src/config.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the 'License'); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an 'AS IS' BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import type { Settings } from "./types";
-
-export class Config {
-  public autostart: boolean = false;
-  public authHeader: Settings.AuthHeader = null;
-  public browserSessionId: Settings.SessionId = null;
-  public custIndex: Settings.CustomIndex = null;
-  public headers: Settings.Headers = null;
-  public httpSessionId: Settings.SessionId = null;
-  public logCountThreshold: number = 5;
-  public logDetails: boolean = false;
-  public on: boolean = false;
-  public resolution: number = 500;
-  public sessionId: Settings.SessionId = null;
-  public time: Settings.TimeFunction = () => Date.now();
-  public toolName: Settings.ToolName = null;
-  public toolVersion: Settings.Version = null;
-  public transmitInterval: number = 0;
-  public url: string = "http://localhost:8000";;
-  public userFromParams: Settings.UserFromParams = null;
-  public useraleVersion: Settings.Version = null;
-  public userId: Settings.UserId = null;
-  public version: Settings.Version = null;
-
-  constructor(config: Partial<Settings.Config>) {
-    this.sessionId = this.getSessionId(
-      "userAlesessionId",
-      "session_" + String(Date.now()),
-    );
-
-    this.httpSessionId = this.getSessionId(
-      "userAleHttpSessionId",
-      this.generateHttpSessionId(),
-    );
-
-    this.timeStampScale(document.createEvent("CustomEvent"));
-
-    this.update(config);
-  }
-
-  /**
-   * Defines sessionId, stores it in sessionStorage, checks to see if there is 
a sessionId in
-   * storage when script is started. This prevents events like 'submit', which 
refresh page data
-   * from refreshing the current user session.
-   */
-  private getSessionId(sessionKey: string, value: any): string {
-    if (window.sessionStorage.getItem(sessionKey) === null) {
-      window.sessionStorage.setItem(sessionKey, JSON.stringify(value));
-      return value;
-    }
-
-    return JSON.parse(window.sessionStorage.getItem(sessionKey) || "");
-  }
-
-  /**
-   * Creates a function to normalize the timestamp of the provided event.
-   * @param  {Event} e An event containing a timeStamp property.
-   * @return {Settings.TimeFunction} The timestamp normalizing function.
-   */
-  private timeStampScale(e: Event): Settings.TimeFunction {
-    let tsScaler: Settings.TimeFunction;
-    if (e.timeStamp && e.timeStamp > 0) {
-      const delta = Date.now() - e.timeStamp;
-
-      /**
-       * Returns a timestamp depending on various browser quirks.
-       * @param  {?Number} ts A timestamp to use for normalization.
-       * @return {Number} A normalized timestamp.
-       */
-      if (delta < 0) {
-        tsScaler = function () {
-          return e.timeStamp / 1000;
-        };
-      } else if (delta > e.timeStamp) {
-        const navStart = performance.timeOrigin;
-        tsScaler = function (ts) {
-          return ts + navStart;
-        };
-      } else {
-        tsScaler = function (ts) {
-          return ts;
-        };
-      }
-    } else {
-      tsScaler = function () {
-        return Date.now();
-      };
-    }
-
-    return tsScaler;
-  }
-
-  /**
-   * Creates a cryptographically random string to represent this HTTP session.
-   * @return {String} A random 32-digit hex string.
-   */
-  private generateHttpSessionId(): string {
-    // 32 digit hex -> 128 bits of info -> 2^64 ~= 10^19 sessions needed for 
50% chance of collision
-    const len = 32;
-    const arr = new Uint8Array(len / 2);
-    window.crypto.getRandomValues(arr);
-    return Array.from(arr, (dec) => {
-      return dec.toString(16).padStart(2, "0");
-    }).join("");
-  }
-
-  /**
-   * Shallow merges a newConfig with the configuration class, updating it.
-   * Retrieves/updates the userid if userFromParams is provided.
-   * @param  {Partial<Settings.Config>} newConfig Configuration object to 
merge into the current config.
-   */
-  public update(newConfig: Partial<Settings.Config>): void {
-    const self = this as unknown as Settings.Config;
-
-    (Object.keys(newConfig) as (keyof Settings.Config)[]).forEach((key) => {
-      const value = newConfig[key];
-      if (value !== undefined) {
-        self[key] = value;
-      }
-    });
-  }
-
-  /**
-   * Attempts to extract the userid from the query parameters of the URL.
-   * @param  {string} param The name of the query parameter containing the 
userid.
-   * @return {string | null}       The extracted/decoded userid, or null if 
none is found.
-   */
-  public getUserId(param?: string) {
-    if( this.userFromParams) {
-      const userField = param;
-      const regex = new RegExp("[?&]" + userField + "(=([^&#]*)|&|#|$)");
-      const results = window.location.href.match(regex);
-  
-      if (results && results[2]) {
-        return decodeURIComponent(results[2].replace(/\+/g, " "));
-      }
-      return null;
-    } else {
-      return this.userId
-    }
-  }
-
-}
diff --git a/products/userale/packages/flagon-userale/src/configure.ts 
b/products/userale/packages/flagon-userale/src/configure.ts
index a6661dd..f41645d 100644
--- a/products/userale/packages/flagon-userale/src/configure.ts
+++ b/products/userale/packages/flagon-userale/src/configure.ts
@@ -64,12 +64,8 @@ export class Configuration {
   }
 
   private initialize(): void {
-    try {
-      const settings = getInitialSettings();
-      this.update(settings);
-    } catch (error) {
-      console.log(error);
-    }
+    const settings = getInitialSettings();
+    this.update(settings);
   }
 
   /**
@@ -124,12 +120,4 @@ export class Configuration {
     }
     return null;
   }
-
-  /**
-   * 
-   * @return {bool}
-   */
-  public isWebSocket(): boolean {
-    return this.url.startsWith("ws://") || this.url.startsWith("wss://");
-  }
 }
diff --git 
a/products/userale/packages/flagon-userale/src/getInitialSettings.temp.ts 
b/products/userale/packages/flagon-userale/src/getInitialSettings.ts
similarity index 100%
rename from 
products/userale/packages/flagon-userale/src/getInitialSettings.temp.ts
rename to products/userale/packages/flagon-userale/src/getInitialSettings.ts
diff --git a/products/userale/packages/flagon-userale/src/iife.ts 
b/products/userale/packages/flagon-userale/src/iife.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/products/userale/packages/flagon-userale/src/logPackager.ts 
b/products/userale/packages/flagon-userale/src/logPackager.ts
deleted file mode 100644
index e47c694..0000000
--- a/products/userale/packages/flagon-userale/src/logPackager.ts
+++ /dev/null
@@ -1,276 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Callbacks, Logging } from "@/types";
-import { Configuration } from "@/configure";
-
-export class LogPackager {
-  private logs: Array<Logging.Log> = [];
-  private config!: Configuration;
-  private cbHandlers: Callbacks.CallbackMap = {};
-  private intervalId: string | null = null;
-  private intervalType: string | null = null;
-  private intervalPath: string[] | null = null;
-  private intervalTimer: number | null = null;
-  private intervalCounter: number = 0;
-  private intervalLog: Logging.Log | null = null;
-
-  public addCallbacks(...newCallbacks: Record<string | symbol, 
CallableFunction>[]): Callbacks.CallbackMap {
-    newCallbacks.forEach((source) => {
-      let descriptors: { [key in string | symbol]: any } = {};
-      Object.keys(source).forEach((key) => {
-        descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
-      });
-      Object.getOwnPropertySymbols(source).forEach((sym) => {
-        const descriptor = Object.getOwnPropertyDescriptor(source, sym);
-        if (descriptor?.enumerable) descriptors[sym] = descriptor;
-      });
-      Object.defineProperties(this.cbHandlers, descriptors);
-    });
-    return this.cbHandlers;
-  }
-
-  public removeCallbacks(targetKeys: string[]): void {
-    targetKeys.forEach((key) => {
-      if (Object.prototype.hasOwnProperty.call(this.cbHandlers, key)) {
-        delete this.cbHandlers[key];
-      }
-    });
-  }
-
-  constructor(newLogs: Array<Logging.Log>, newConfig: Configuration) {
-    this.logs = newLogs;
-    this.config = newConfig;
-    this.cbHandlers = {};
-    this.intervalId = null;
-    this.intervalType = null;
-    this.intervalPath = null;
-    this.intervalTimer = null;
-    this.intervalCounter = 0;
-    this.intervalLog = null;
-  }
-
-  public packageLog(e: Event, detailFcn?: Logging.DynamicDetailFunction | 
null): boolean {
-    if (!this.config.on) return false;
-
-    const details = detailFcn ? detailFcn(e) : null;
-    const timeFields = this.extractTimeFields(e.timeStamp && e.timeStamp > 0 ? 
this.config.time(e.timeStamp) : Date.now());
-
-    let log: Logging.Log = {
-      target: e.target ? this.getSelector(e.target) : null,
-      path: this.buildPath(e),
-      pageUrl: window.location.href,
-      pageTitle: document.title,
-      pageReferrer: document.referrer,
-      userAgent: window.navigator.userAgent,
-      clientTime: timeFields.milli,
-      microTime: timeFields.micro,
-      location: this.getLocation(e),
-      scrnRes: this.getScreenRes(),
-      type: e.type,
-      logType: "raw",
-      userAction: true,
-      details,
-      userId: this.config.userId,
-      toolVersion: this.config.toolVersion,
-      toolName: this.config.toolName,
-      useraleVersion: this.config.useraleVersion,
-      sessionId: this.config.sessionId,
-      httpSessionId: this.config.httpSessionId,
-      browserSessionId: this.config.browserSessionId,
-      attributes: this.buildAttrs(e),
-      style: this.buildCSS(e),
-    };
-
-    for (const func of Object.values(this.cbHandlers)) {
-      if (typeof func === "function") {
-        log = func(log, e);
-        if (!log) return false;
-      }
-    }
-
-    this.logs.push(log);
-    return true;
-  }
-
-  public packageCustomLog(customLog: Logging.CustomLog, detailFcn: 
Logging.DynamicDetailFunction | Logging.StaticDetailFunction, userAction: 
boolean): boolean {
-    if (!this.config.on) return false;
-
-    const details = detailFcn.length === 0 ? (detailFcn as 
Logging.StaticDetailFunction)() : null;
-    let log: Logging.Log = Object.assign({
-      pageUrl: window.location.href,
-      pageTitle: document.title,
-      pageReferrer: document.referrer,
-      userAgent: window.navigator.userAgent,
-      clientTime: Date.now(),
-      scrnRes: this.getScreenRes(),
-      logType: "custom",
-      userAction,
-      details,
-      userId: this.config.userId,
-      toolVersion: this.config.toolVersion,
-      toolName: this.config.toolName,
-      useraleVersion: this.config.useraleVersion,
-      sessionId: this.config.sessionId,
-      httpSessionId: this.config.httpSessionId,
-      browserSessionId: this.config.browserSessionId,
-    }, customLog);
-
-    for (const func of Object.values(this.cbHandlers)) {
-      if (typeof func === "function") {
-        log = func(log, null);
-        if (!log) return false;
-      }
-    }
-
-    this.logs.push(log);
-    return true;
-  }
-
-  private extractTimeFields(timeStamp: number) {
-    return {
-      milli: Math.floor(timeStamp),
-      micro: Number((timeStamp % 1).toFixed(3)),
-    };
-  }
-
-  public packageIntervalLog(e: Event): boolean {
-    try {
-      const target = e.target ? this.getSelector(e.target) : null;
-      const path = this.buildPath(e);
-      const type = e.type;
-      const timestamp = Math.floor(e.timeStamp && e.timeStamp > 0 ? 
this.config.time(e.timeStamp) : Date.now());
-
-      if (this.intervalId == null) {
-        this.intervalId = target;
-        this.intervalType = type;
-        this.intervalPath = path;
-        this.intervalTimer = timestamp;
-        this.intervalCounter = 0;
-      }
-
-      if ((this.intervalId !== target || this.intervalType !== type) && 
this.intervalTimer) {
-        this.intervalLog = {
-          target: this.intervalId,
-          path: this.intervalPath,
-          pageUrl: window.location.href,
-          pageTitle: document.title,
-          pageReferrer: document.referrer,
-          userAgent: window.navigator.userAgent,
-          count: this.intervalCounter,
-          duration: timestamp - this.intervalTimer,
-          startTime: this.intervalTimer,
-          endTime: timestamp,
-          type: this.intervalType,
-          logType: "interval",
-          targetChange: this.intervalId !== target,
-          typeChange: this.intervalType !== type,
-          userAction: false,
-          userId: this.config.userId,
-          toolVersion: this.config.toolVersion,
-          toolName: this.config.toolName,
-          useraleVersion: this.config.useraleVersion,
-          sessionId: this.config.sessionId,
-          httpSessionId: this.config.httpSessionId,
-          browserSessionId: this.config.browserSessionId,
-        };
-
-        for (const func of Object.values(this.cbHandlers)) {
-          if (typeof func === "function") {
-            this.intervalLog = func(this.intervalLog, null);
-            if (!this.intervalLog) return false;
-          }
-        }
-
-        if (this.intervalLog) this.logs.push(this.intervalLog);
-
-        this.intervalId = target;
-        this.intervalType = type;
-        this.intervalPath = path;
-        this.intervalTimer = timestamp;
-        this.intervalCounter = 0;
-      }
-
-      if (this.intervalId === target && this.intervalType === type) {
-        this.intervalCounter++;
-      }
-
-      return true;
-    } catch {
-      return false;
-    }
-  }
-
-  private getLocation(e: Event): { x: number | null; y: number | null } {
-    if (e instanceof MouseEvent) {
-      return e.pageX != null
-        ? { x: e.pageX, y: e.pageY }
-        : { x: document.documentElement.scrollLeft + e.clientX, y: 
document.documentElement.scrollTop + e.clientY };
-    }
-    return { x: null, y: null };
-  }
-
-  private getScreenRes() {
-    return { width: window.innerWidth, height: window.innerHeight };
-  }
-
-  private getSelector(ele: EventTarget): string {
-    if (ele instanceof HTMLElement || ele instanceof Element) {
-      return ele.localName
-        ? ele.localName + (ele.id ? "#" + ele.id : "") + (ele.className ? "." 
+ ele.className : "")
-        : ele.nodeName + (ele.id ? "#" + ele.id : "") + (ele.className ? "." + 
ele.className : "");
-    } else if (ele instanceof Document) return "#document";
-    else if (ele === globalThis) return "Window";
-    return "Unknown";
-  }
-
-  private buildPath(e: Event): string[] {
-    return this.selectorizePath(e.composedPath());
-  }
-
-  private selectorizePath(path: EventTarget[]): string[] {
-    return path.map((ele) => this.getSelector(ele));
-  }
-
-  private buildAttrs(e: Event): Record<string, any> {
-    const attributes: Record<string, any> = {};
-    const blacklist = ["style"];
-    if (e.target instanceof Element) {
-      for (const attr of e.target.attributes) {
-        if (blacklist.includes(attr.name)) continue;
-        try {
-          attributes[attr.name] = JSON.parse(attr.value);
-        } catch {
-          attributes[attr.name] = attr.value;
-        }
-      }
-    }
-    return attributes;
-  }
-
-  private buildCSS(e: Event): Record<string, string> {
-    const properties: Record<string, string> = {};
-    if (e.target instanceof HTMLElement) {
-      const style = e.target.style;
-      for (let i = 0; i < style.length; i++) {
-        const prop = style[i];
-        properties[prop] = style.getPropertyValue(prop);
-      }
-    }
-    return properties;
-  }
-}
\ No newline at end of file
diff --git a/products/userale/packages/flagon-userale/src/logSender.ts 
b/products/userale/packages/flagon-userale/src/logSender.ts
deleted file mode 100644
index d6c01ce..0000000
--- a/products/userale/packages/flagon-userale/src/logSender.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { Configuration } from "@/configure";
-import { Logging } from "@/types";
-import { updateAuthHeader, updateCustomHeaders } from "@/utils";
-
-export class LogSender {
-  private sendIntervalId: NodeJS.Timeout | undefined;
-  private wsock: WebSocket | undefined;
-
-  /**
-   * Initializes the log queue processors.
-   * @param logs Array of logs to append to.
-   * @param config Configuration object to use when logging.
-   */
-  constructor(logs: Array<Logging.Log>, config: Configuration) {
-    if (this.sendIntervalId) {
-      clearInterval(this.sendIntervalId);
-    }
-
-    this.sendIntervalId = this.sendOnInterval(logs, config);
-
-    if(config.isWebSocket()) {
-      this.wsock = new WebSocket(config.url);
-      this.wsock.onerror = () => {
-        console.log("no websockets detected");
-      };
-      this.wsock.onopen = () => {
-        console.log("connection established with websockets");
-      };
-      this.wsock.onclose = () => {
-        this.sendOnClose(logs, config);
-      };
-
-    } else {
-      this.sendOnClose(logs, config);
-    }
-
-  }
-
-  /**
-   * Checks the provided log array on an interval, flushing the logs
-   * if the queue has reached the threshold specified by the provided config.
-   * @param logs Array of logs to read from.
-   * @param config Configuration singleton to be read from.
-   * @return The newly created interval id.
-   */
-  private sendOnInterval(
-    logs: Array<Logging.Log>,
-    config: Configuration
-  ): NodeJS.Timeout {
-    this.sendIntervalId = setInterval(() => {
-      if (!config.on) return;
-
-      if (logs.length >= config.logCountThreshold) {
-        this.sendLogs([...logs], config, 0); // Send a copy
-        logs.splice(0); // Clear array reference (no reassignment)
-      }
-    }, config.transmitInterval);
-
-    return this.sendIntervalId;
-  }
-
-  /**
-   * Attempts to flush the remaining logs when the window is closed.
-   * @param logs Array of logs to be flushed.
-   * @param config Configuration singleton to be read from.
-   */
-  public sendOnClose(
-    logs: Array<Logging.Log>,
-    config: Configuration
-  ): void {
-    window.addEventListener("pagehide", () => {
-      if (!config.on) return;
-
-      if (logs.length > 0) {
-        if (config.isWebSocket() && this.wsock?.readyState === WebSocket.OPEN) 
{
-          const data = JSON.stringify(logs);
-          this.wsock.send(data);
-        } else {
-          const headers: HeadersInit = new Headers();
-          headers.set("Content-Type", "application/json;charset=UTF-8");
-
-          if (config.authHeader) {
-            headers.set("Authorization", config.authHeader.toString());
-          }
-
-          fetch(config.url, {
-            keepalive: true,
-            method: "POST",
-            headers: headers,
-            body: JSON.stringify(logs),
-          }).catch((error) => {
-            console.error(error);
-          });
-        }
-        logs.splice(0); // Clear log queue
-      }
-    });
-  }
-
-  /**
-   * Sends the provided array of logs to the specified url,
-   * retrying the request up to the specified number of retries.
-   * @param logs Array of logs to send.
-   * @param config Configuration singleton.
-   * @param retries Maximum number of attempts to send the logs.
-   */
-  public sendLogs(
-    logs: Array<Logging.Log>,
-    config: Configuration,
-    retries: number
-  ): void {
-    const data = JSON.stringify(logs);
-
-    if (config.isWebSocket() && this.wsock?.readyState === WebSocket.OPEN) {
-      this.wsock.send(data);
-    } else {
-      const req = new XMLHttpRequest();
-      req.open("POST", config.url);
-
-      updateAuthHeader(config);
-      if (config.authHeader) {
-        req.setRequestHeader(
-          "Authorization",
-          typeof config.authHeader === "function"
-            ? config.authHeader()
-            : config.authHeader
-        );
-      }
-      req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
-
-      updateCustomHeaders(config);
-      if (config.headers) {
-        Object.entries(config.headers).forEach(([header, value]) => {
-          req.setRequestHeader(header, value);
-        });
-      }
-
-      req.onreadystatechange = () => {
-        if (req.readyState === 4 && req.status !== 200 && retries > 0) {
-          this.sendLogs(logs, config, retries - 1);
-        }
-      };
-
-      req.send(data);
-    }
-  }
-}
diff --git a/products/userale/packages/flagon-userale/src/loggingEngine.ts 
b/products/userale/packages/flagon-userale/src/loggingEngine.ts
deleted file mode 100644
index d6ff573..0000000
--- a/products/userale/packages/flagon-userale/src/loggingEngine.ts
+++ /dev/null
@@ -1,125 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements...
- */
-
-const startLoadTimestamp = Date.now();
-
-import { version } from "../package.json";
-import { attachHandlers, defineCustomDetails } from "@/attachHandlers";
-import { logPackager } from "@/logPackager";
-import { logSender } from "@/logSender";
-import { registerAuthCallback } from "@/utils";
-
-import type { Settings, Logging } from "@/types";
-
-
-export class LoggingEngine {
-  private config = Configuration.getInstance();
-  private logs: Array<Logging.Log> = [];
-  private endLoadTimestamp: number;
-  public static version = userAleVersion;
-  private started = false;
-
-  constructor() {
-    this.endLoadTimestamp = Date.now();
-    try {
-      window.onload = () => {
-        this.endLoadTimestamp = Date.now();
-      };
-    } catch {
-      this.endLoadTimestamp = Date.now();
-    }
-
-    this.config.update({
-      useraleVersion: userAleVersion,
-    });
-    initPackager(this.logs, this.config);
-
-    if (this.config.autostart) {
-      this.setup();
-    }
-  }
-
-  private setup(): void {
-    if (!this.started) {
-      setTimeout(() => {
-        let state: DocumentReadyState;
-        try {
-          state = document.readyState;
-        } catch {
-          return;
-        }
-
-        if (this.config.autostart && (state === "interactive" || state === 
"complete")) {
-          attachHandlers(this.config);
-          initSender(this.logs, this.config);
-          this.started = this.config.on = true;
-          packageCustomLog(
-            {
-              type: "load",
-              details: { pageLoadTime: this.endLoadTimestamp - 
this.startLoadTimestamp },
-            },
-            () => ({}),
-            false
-          );
-        } else {
-          this.setup();
-        }
-      }, 100);
-    }
-  }
-  /**
-   * Used to start the logging process if the
-   * autostart configuration option is set to false.
-   */
-  public start(): void {
-    if (!this.started || this.config.autostart === false) {
-      this.started = this.config.on = true;
-      this.config.update({ autostart: true });
-    }
-  }
-  /**
-   * Halts the logging process. Logs will no longer be sent.
-   */
-  public stop(): void {
-    this.started = this.config.on = false;
-    this.config.update({ autostart: false });
-  }
-
-  /**
-   * Updates the current configuration
-   * object with the provided values.
-   * @param  {Partial<Settings.Config>} newConfig The configuration options to 
use.
-   * @return {Settings.Config}           Returns the updated configuration.
-   */
-  public options(newConfig?: Partial<Settings.Config>): Settings.Config {
-    if (newConfig) {
-      this.config.update(newConfig);
-    }
-    return this.config;
-  }
-
-  /**
-   * Appends a log to the log queue.
-   * @param  {Logging.CustomLog} customLog The log to append.
-   * @return {boolean}          Whether the operation succeeded.
-   */
-  public log(customLog?: Logging.CustomLog): boolean {
-    if (customLog) {
-      this.logs.push(customLog);
-      return true;
-    }
-    return false;
-  }
-}
-
-// Export auxiliary utilities
-export { 
-  defineCustomDetails as details,
-  registerAuthCallback,
-  addCallbacks,
-  removeCallbacks,
-  packageLog,
-  packageCustomLog,
-};
diff --git a/products/userale/packages/flagon-userale/src/main.ts 
b/products/userale/packages/flagon-userale/src/main.ts
new file mode 100644
index 0000000..9f75c21
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/main.ts
@@ -0,0 +1,166 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { version as userAleVersion } from "../package.json";
+import { Configuration } from "@/configure";
+import { attachHandlers } from "@/attachHandlers";
+import { initPackager, packageCustomLog } from "@/packageLogs";
+import { initSender, sendOnClose } from "@/sendLogs";
+
+import type { Settings, Logging } from "@/types";
+
+const config = Configuration.getInstance();
+const logs: Array<Logging.Log> = [];
+
+const startLoadTimestamp = Date.now();
+let endLoadTimestamp: number;
+try {
+  window.onload = function() {
+    endLoadTimestamp = Date.now();
+  };
+} catch (error) {
+  endLoadTimestamp = Date.now();
+}
+
+export let started = false;
+export let wsock: WebSocket;
+export { defineCustomDetails as details } from "@/attachHandlers";
+export { registerAuthCallback as registerAuthCallback } from "@/utils";
+export {
+  addCallbacks as addCallbacks,
+  removeCallbacks as removeCallbacks,
+  packageLog as packageLog,
+  packageCustomLog as packageCustomLog,
+  getSelector as getSelector,
+  buildPath as buildPath,
+} from "@/packageLogs";
+
+config.update({
+  useraleVersion: userAleVersion,
+});
+initPackager(logs, config);
+// getWebsocketsEnabled(config);
+if (config.autostart) {
+  setup(config);
+}
+
+/**
+ * Hooks the global event listener, and starts up the
+ * logging interval.
+ * @param  {Configuration} config Configuration settings for the logger
+ */
+function setup(config: Configuration) {
+  if (!started) {
+    setTimeout(function() {
+      let state;
+      try {
+        state = document.readyState;
+      } catch (error) {
+        initSender(logs, config);
+      }
+
+      if (
+        config.autostart &&
+        (state === "interactive" || state === "complete")
+      ) {
+        attachHandlers(config);
+        initSender(logs, config);
+        started = config.on = true;
+        packageCustomLog(
+          {
+            type: "load",
+            details: { pageLoadTime: endLoadTimestamp - startLoadTimestamp },
+          },
+          () => ({}),
+          false,
+        );
+      } else {
+        setup(config);
+      }
+    }, 100);
+  }
+}
+
+/**
+ * Checks to see if the specified backend URL supporsts Websockets
+ * and updates the config accordingly
+ */
+function getWebsocketsEnabled(config: Configuration) {
+  wsock = new WebSocket(config.url.replace("http://";, "ws://"));
+  wsock.onerror = () => {
+    console.log("no websockets detected");
+  };
+  wsock.onopen = () => {
+    console.log("connection established with websockets");
+    config.websocketsEnabled = true;
+  };
+  wsock.onclose = () => {
+    sendOnClose(logs, config);
+  };
+}
+
+// Export the Userale API
+export const version = userAleVersion;
+
+/**
+ * Used to start the logging process if the
+ * autostart configuration option is set to false.
+ */
+export function start(): void {
+  if (!started || config.autostart === false) {
+    started = config.on = true;
+    config.update({ autostart: true });
+  }
+}
+
+/**
+ * Halts the logging process. Logs will no longer be sent.
+ */
+export function stop(): void {
+  started = config.on = false;
+  config.update({ autostart: false });
+}
+
+/**
+ * Updates the current configuration
+ * object with the provided values.
+ * @param  {Partial<Settings.Config>} newConfig The configuration options to 
use.
+ * @return {Settings.Config}           Returns the updated configuration.
+ */
+export function options(
+  newConfig: Partial<Settings.Config> | undefined,
+): Settings.Config {
+  if (newConfig) {
+    config.update(newConfig);
+  }
+
+  return config;
+}
+
+/**
+ * Appends a log to the log queue.
+ * @param  {Logging.CustomLog} customLog The log to append.
+ * @return {boolean}          Whether the operation succeeded.
+ */
+export function log(customLog: Logging.CustomLog | undefined) {
+  if (customLog) {
+    logs.push(customLog);
+    return true;
+  } else {
+    return false;
+  }
+}
diff --git a/products/userale/packages/flagon-userale/src/packageLogs.ts 
b/products/userale/packages/flagon-userale/src/packageLogs.ts
new file mode 100644
index 0000000..2cb0cbe
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/packageLogs.ts
@@ -0,0 +1,471 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { detect } from "detect-browser";
+import { Callbacks, Logging } from "@/types";
+import { Configuration } from "@/configure";
+const browserInfo = detect();
+
+export let logs: Array<Logging.Log>;
+let config: Configuration;
+
+// Interval Logging Globals
+let intervalId: string | null;
+let intervalType: string | null;
+let intervalPath: string[] | null;
+let intervalTimer: number | null;
+let intervalCounter: number | null;
+let intervalLog: Logging.Log | null;
+
+export const filterHandler: CallableFunction | null = null;
+export const mapHandler: CallableFunction | null = null;
+export let cbHandlers: Callbacks.CallbackMap = {};
+
+/**
+ * Adds named callbacks to be executed when logging.
+ * @param  {Object } newCallbacks An object containing named callback 
functions.
+ */
+export function addCallbacks(
+  ...newCallbacks: Record<symbol | string, CallableFunction>[]
+) {
+  newCallbacks.forEach((source) => {
+    let descriptors: { [key in string | symbol]: any } = {};
+
+    descriptors = Object.keys(source).reduce((descriptors, key) => {
+      descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
+      return descriptors;
+    }, descriptors);
+
+    Object.getOwnPropertySymbols(source).forEach((sym) => {
+      const descriptor = Object.getOwnPropertyDescriptor(source, sym);
+      if (descriptor?.enumerable) {
+        descriptors[sym] = descriptor;
+      }
+    });
+    Object.defineProperties(cbHandlers, descriptors);
+  });
+  return cbHandlers;
+}
+
+/**
+ * Removes callbacks by name.
+ * @param  {String[]} targetKeys A list of names of functions to remove.
+ */
+export function removeCallbacks(targetKeys: string[]) {
+  targetKeys.forEach((key) => {
+    if (Object.prototype.hasOwnProperty.call(cbHandlers, key)) {
+      delete cbHandlers[key];
+    }
+  });
+}
+
+/**
+ * Assigns the config and log container to be used by the logging functions.
+ * @param  {Array<Logging.Log>} newLogs   Log container.
+ * @param  {Object} newConfig Configuration to use while logging.
+ */
+export function initPackager(
+  newLogs: Array<Logging.Log>,
+  newConfig: Configuration,
+) {
+  logs = newLogs;
+  config = newConfig;
+  cbHandlers = {};
+  intervalId = null;
+  intervalType = null;
+  intervalPath = null;
+  intervalTimer = null;
+  intervalCounter = 0;
+  intervalLog = null;
+}
+
+/**
+ * Transforms the provided HTML event into a log and appends it to the log 
queue.
+ * @param  {Event} e         The event to be logged.
+ * @param  {Function} detailFcn The function to extract additional log 
parameters from the event.
+ * @return {boolean}           Whether the event was logged.
+ */
+export function packageLog(
+  e: Event,
+  detailFcn?: Logging.DynamicDetailFunction | null,
+) {
+  if (!config.on) {
+    return false;
+  }
+
+  let details = null;
+  if (detailFcn) {
+    details = detailFcn(e);
+  }
+
+  const timeFields = extractTimeFields(
+    e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now(),
+  );
+
+  let log: Logging.Log = {
+    target: e.target ? getSelector(e.target) : null,
+    path: buildPath(e),
+    pageUrl: window.location.href,
+    pageTitle: document.title,
+    pageReferrer: document.referrer,
+    browser: detectBrowser(),
+    clientTime: timeFields.milli,
+    microTime: timeFields.micro,
+    location: getLocation(e),
+    scrnRes: getScreenRes(),
+    type: e.type,
+    logType: "raw",
+    userAction: true,
+    details: details,
+    userId: config.userId,
+    toolVersion: config.toolVersion,
+    toolName: config.toolName,
+    useraleVersion: config.useraleVersion,
+    sessionId: config.sessionId,
+    httpSessionId: config.httpSessionId,
+    browserSessionId: config.browserSessionId,
+    attributes: buildAttrs(e),
+    style: buildCSS(e),
+  };
+
+  if (typeof filterHandler === "function" && !filterHandler(log)) {
+    return false;
+  }
+
+  if (typeof mapHandler === "function") {
+    log = mapHandler(log, e);
+  }
+
+  for (const func of Object.values(cbHandlers)) {
+    if (typeof func === "function") {
+      log = func(log, e);
+      if (!log) {
+        return false;
+      }
+    }
+  }
+
+  logs.push(log);
+  return true;
+}
+
+/**
+ * Packages the provided customLog to include standard meta data and appends 
it to the log queue.
+ * @param  {Logging.CustomLog} customLog        The behavior to be logged.
+ * @param  {Logging.DynamicDetailFunction} detailFcn     The function to 
extract additional log parameters from the event.
+ * @param  {boolean} userAction     Indicates user behavior (true) or system 
behavior (false)
+ * @return {boolean}           Whether the event was logged.
+ */
+export function packageCustomLog(
+  customLog: Logging.CustomLog,
+  detailFcn: Logging.DynamicDetailFunction | Logging.StaticDetailFunction,
+  userAction: boolean,
+): boolean {
+  if (!config.on) {
+    return false;
+  }
+
+  let details = null;
+  if (detailFcn.length === 0) {
+    // In the case of a union, the type checker will default to the more 
stringent
+    // type, i.e. the DetailFunction that expects an argument for safety 
purposes.
+    // To avoid this, we must explicitly check the type by asserting it 
receives
+    // no arguments (detailFcn.length === 0) and then cast it to the
+    // StaticDetailFunction type.
+    const staticDetailFcn = detailFcn as Logging.StaticDetailFunction;
+    details = staticDetailFcn();
+  }
+
+  const metaData = {
+    pageUrl: window.location.href,
+    pageTitle: document.title,
+    pageReferrer: document.referrer,
+    browser: detectBrowser(),
+    clientTime: Date.now(),
+    scrnRes: getScreenRes(),
+    logType: "custom",
+    userAction: userAction,
+    details: details,
+    userId: config.userId,
+    toolVersion: config.toolVersion,
+    toolName: config.toolName,
+    useraleVersion: config.useraleVersion,
+    sessionId: config.sessionId,
+    httpSessionId: config.httpSessionId,
+    browserSessionId: config.browserSessionId,
+  };
+
+  let log = Object.assign(metaData, customLog);
+
+  if (typeof filterHandler === "function" && !filterHandler(log)) {
+    return false;
+  }
+
+  if (typeof mapHandler === "function") {
+    log = mapHandler(log);
+  }
+
+  for (const func of Object.values(cbHandlers)) {
+    if (typeof func === "function") {
+      log = func(log, null);
+      if (!log) {
+        return false;
+      }
+    }
+  }
+
+  logs.push(log);
+
+  return true;
+}
+
+/**
+ * Extract the millisecond and microsecond portions of a timestamp.
+ * @param  {Number} timeStamp The timestamp to split into millisecond and 
microsecond fields.
+ * @return {Object}           An object containing the millisecond
+ *                            and microsecond portions of the timestamp.
+ */
+export function extractTimeFields(timeStamp: number) {
+  return {
+    milli: Math.floor(timeStamp),
+    micro: Number((timeStamp % 1).toFixed(3)),
+  };
+}
+
+/**
+ * Track intervals and gather details about it.
+ * @param {Object} e
+ * @return boolean
+ */
+export function packageIntervalLog(e: Event) {
+  try {
+    const target = e.target ? getSelector(e.target) : null;
+    const path = buildPath(e);
+    const type = e.type;
+    const timestamp = Math.floor(
+      e.timeStamp && e.timeStamp > 0 ? config.time(e.timeStamp) : Date.now(),
+    );
+
+    // Init - this should only happen once on initialization
+    if (intervalId == null) {
+      intervalId = target;
+      intervalType = type;
+      intervalPath = path;
+      intervalTimer = timestamp;
+      intervalCounter = 0;
+    }
+
+    if ((intervalId !== target || intervalType !== type) && intervalTimer) {
+      // When to create log? On transition end
+      // @todo Possible for intervalLog to not be pushed in the event the 
interval never ends...
+
+      intervalLog = {
+        target: intervalId,
+        path: intervalPath,
+        pageUrl: window.location.href,
+        pageTitle: document.title,
+        pageReferrer: document.referrer,
+        browser: detectBrowser(),
+        count: intervalCounter,
+        duration: timestamp - intervalTimer, // microseconds
+        startTime: intervalTimer,
+        endTime: timestamp,
+        type: intervalType,
+        logType: "interval",
+        targetChange: intervalId !== target,
+        typeChange: intervalType !== type,
+        userAction: false,
+        userId: config.userId,
+        toolVersion: config.toolVersion,
+        toolName: config.toolName,
+        useraleVersion: config.useraleVersion,
+        sessionId: config.sessionId,
+        httpSessionId: config.httpSessionId,
+        browserSessionId: config.browserSessionId,
+      };
+
+      if (typeof filterHandler === "function" && !filterHandler(intervalLog)) {
+        return false;
+      }
+
+      if (typeof mapHandler === "function") {
+        intervalLog = mapHandler(intervalLog, e);
+      }
+
+      for (const func of Object.values(cbHandlers)) {
+        if (typeof func === "function") {
+          intervalLog = func(intervalLog, null);
+          if (!intervalLog) {
+            return false;
+          }
+        }
+      }
+
+      if (intervalLog) logs.push(intervalLog);
+
+      // Reset
+      intervalId = target;
+      intervalType = type;
+      intervalPath = path;
+      intervalTimer = timestamp;
+      intervalCounter = 0;
+    }
+
+    // Interval is still occuring, just update counter
+    if (intervalId == target && intervalType == type && intervalCounter) {
+      intervalCounter = intervalCounter + 1;
+    }
+
+    return true;
+  } catch {
+    return false;
+  }
+}
+
+/**
+ * Extracts coordinate information from the event
+ * depending on a few browser quirks.
+ * @param  {Event} e The event to extract coordinate information from.
+ * @return {Object}   An object containing nullable x and y coordinates for 
the event.
+ */
+export function getLocation(e: Event) {
+  if (e instanceof MouseEvent) {
+    if (e.pageX != null) {
+      return { x: e.pageX, y: e.pageY };
+    } else if (e.clientX != null) {
+      return {
+        x: document.documentElement.scrollLeft + e.clientX,
+        y: document.documentElement.scrollTop + e.clientY,
+      };
+    }
+  } else {
+    return { x: null, y: null };
+  }
+}
+
+/**
+ * Extracts innerWidth and innerHeight to provide estimates of screen 
resolution
+ * @return {Object} An object containing the innerWidth and InnerHeight
+ */
+export function getScreenRes() {
+  return { width: window.innerWidth, height: window.innerHeight };
+}
+
+/**
+ * Builds a string CSS selector from the provided element
+ * @param  {EventTarget} ele The element from which the selector is built.
+ * @return {string}     The CSS selector for the element, or Unknown if it 
can't be determined.
+ */
+export function getSelector(ele: EventTarget) {
+  if (ele instanceof HTMLElement || ele instanceof Element) {
+    if (ele.localName) {
+      return (
+        ele.localName +
+        (ele.id ? "#" + ele.id : "") +
+        (ele.className ? "." + ele.className : "")
+      );
+    } else if (ele.nodeName) {
+      return (
+        ele.nodeName +
+        (ele.id ? "#" + ele.id : "") +
+        (ele.className ? "." + ele.className : "")
+      );
+    }
+  } else if (ele instanceof Document) {
+    return "#document";
+  } else if (ele === globalThis) {
+    return "Window";
+  }
+  return "Unknown";
+}
+
+/**
+ * Builds an array of elements from the provided event target, to the root 
element.
+ * @param  {Event} e Event from which the path should be built.
+ * @return {HTMLElement[]}   Array of elements, starting at the event target, 
ending at the root element.
+ */
+export function buildPath(e: Event) {
+  const path = e.composedPath();
+  return selectorizePath(path);
+}
+
+/**
+ * Builds a CSS selector path from the provided list of elements.
+ * @param  {EventTarget[]} path Array of HTML Elements from which the path 
should be built.
+ * @return {string[]}      Array of string CSS selectors.
+ */
+export function selectorizePath(path: EventTarget[]) {
+  let i = 0;
+  let pathEle;
+  const pathSelectors: string[] = [];
+  while ((pathEle = path[i])) {
+    pathSelectors.push(getSelector(pathEle));
+    ++i;
+    pathEle = path[i];
+  }
+  return pathSelectors;
+}
+
+export function detectBrowser() {
+  return {
+    browser: browserInfo ? browserInfo.name : "",
+    version: browserInfo ? browserInfo.version : "",
+  };
+}
+
+/**
+ * Builds an object containing attributes of an element.
+ * Attempts to parse all attribute values as JSON text.
+ * @param  {Event} e Event from which the target element's attributes should 
be extracted.
+ * @return {Record<string, any>} Object with element attributes as key-value 
pairs.
+ */
+export function buildAttrs(e: Event): Record<string, any> {
+  const attributes: Record<string, any> = {};
+  const attributeBlackList = ["style"];
+
+  if (e.target && e.target instanceof Element) {
+    for (const attr of e.target.attributes) {
+      if (attributeBlackList.includes(attr.name)) continue;
+      let val: any = attr.value;
+      try {
+        val = JSON.parse(val);
+      } catch (error) {
+        // Ignore parsing errors, fallback to raw string value
+      }
+      attributes[attr.name] = val;
+    }
+  }
+
+  return attributes;
+}
+
+/**
+ * Builds an object containing all CSS properties of an element.
+ * @param  {Event} e Event from which the target element's properties should 
be extracted.
+ * @return {Record<string, string>} Object with all CSS properties as 
key-value pairs.
+ */
+export function buildCSS(e: Event): Record<string, string> {
+  const properties: Record<string, string> = {};
+  if (e.target && e.target instanceof HTMLElement) {
+    const styleObj = e.target.style;
+    for (let i = 0; i < styleObj.length; i++) {
+      const prop = styleObj[i];
+      properties[prop] = styleObj.getPropertyValue(prop);
+    }
+  }
+  return properties;
+}
diff --git a/products/userale/packages/flagon-userale/src/sendLogs.ts 
b/products/userale/packages/flagon-userale/src/sendLogs.ts
new file mode 100644
index 0000000..2c9e6fc
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/sendLogs.ts
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Configuration } from "@/configure";
+import { Logging } from "@/types";
+import { updateAuthHeader, updateCustomHeaders } from "@/utils";
+import { wsock } from "./main";
+
+let sendIntervalId: string | number | NodeJS.Timeout | undefined;
+
+/**
+ * Initializes the log queue processors.
+ * @param  {Array<Logging.Log>} logs   Array of logs to append to.
+ * @param  {Configuration} config Configuration object to use when logging.
+ */
+export function initSender(logs: Array<Logging.Log>, config: Configuration) {
+  if (sendIntervalId) {
+    clearInterval(sendIntervalId);
+  }
+
+  sendIntervalId = sendOnInterval(logs, config);
+  sendOnClose(logs, config);
+}
+
+/**
+ * Checks the provided log array on an interval, flushing the logs
+ * if the queue has reached the threshold specified by the provided config.
+ * @param  {Array<Logging.Log>} logs   Array of logs to read from.
+ * @param  {Configuration} config Configuration singleton to be read from.
+ * @return {Number}        The newly created interval id.
+ */
+export function sendOnInterval(
+  logs: Array<Logging.Log>,
+  config: Configuration,
+): NodeJS.Timeout {
+  return setInterval(function() {
+    if (!config.on) {
+      return;
+    }
+
+    if (logs.length >= config.logCountThreshold) {
+      sendLogs(logs.slice(0), config, 0); // Send a copy
+      logs.splice(0); // Clear array reference (no reassignment)
+    }
+  }, config.transmitInterval);
+}
+
+/**
+ * Attempts to flush the remaining logs when the window is closed.
+ * @param  {Array<Logging.Log>} logs   Array of logs to be flushed.
+ * @param  {Configuration} config Configuration singleton to be read from.
+ */
+export function sendOnClose(
+  logs: Array<Logging.Log>,
+  config: Configuration,
+): void {
+  window.addEventListener("pagehide", function() {
+    if (!config.on) {
+      return;
+    }
+
+    if (logs.length > 0) {
+      if (config.websocketsEnabled) {
+        const data = JSON.stringify(logs);
+        wsock.send(data);
+      } else {
+        const headers: HeadersInit = new Headers();
+        headers.set("Content-Type", "applicaiton/json;charset=UTF-8");
+
+        if (config.authHeader) {
+          headers.set("Authorization", config.authHeader.toString());
+        }
+
+        fetch(config.url, {
+          keepalive: true,
+          method: "POST",
+          headers: headers,
+          body: JSON.stringify(logs),
+        }).catch((error) => {
+          console.error(error);
+        });
+      }
+      logs.splice(0); // clear log queue
+    }
+  });
+}
+
+/**
+ * Sends the provided array of logs to the specified url,
+ * retrying the request up to the specified number of retries.
+ * @param  {Array<Logging.Log>} logs    Array of logs to send.
+ * @param  {Configuration} config     configuration singleton.
+ * @param  {Number} retries Maximum number of attempts to send the logs.
+ */
+
+// @todo expose config object to sendLogs replate url with config.url
+export function sendLogs(
+  logs: Array<Logging.Log>,
+  config: Configuration,
+  retries: number,
+) {
+  const data = JSON.stringify(logs);
+
+  if (config.websocketsEnabled) {
+    wsock.send(data);
+  } else {
+    const req = new XMLHttpRequest();
+
+    req.open("POST", config.url);
+
+    // Update headers
+    updateAuthHeader(config);
+    if (config.authHeader) {
+      req.setRequestHeader(
+        "Authorization",
+        typeof config.authHeader === "function"
+          ? config.authHeader()
+          : config.authHeader,
+      );
+    }
+    req.setRequestHeader("Content-type", "application/json;charset=UTF-8");
+
+    // Update custom headers last to allow them to over-write the defaults. 
This assumes
+    // the user knows what they are doing and may want to over-write the 
defaults.
+    updateCustomHeaders(config);
+    if (config.headers) {
+      Object.entries(config.headers).forEach(([header, value]) => {
+        req.setRequestHeader(header, value);
+      });
+    }
+
+    req.onreadystatechange = function() {
+      if (req.readyState === 4 && req.status !== 200) {
+        if (retries > 0) {
+          sendLogs(logs, config, retries--);
+        }
+      }
+    };
+
+    req.send(data);
+  }
+}
diff --git a/products/userale/packages/flagon-userale/src/types.d.ts 
b/products/userale/packages/flagon-userale/src/types.d.ts
index 5534a15..193f9b0 100644
--- a/products/userale/packages/flagon-userale/src/types.d.ts
+++ b/products/userale/packages/flagon-userale/src/types.d.ts
@@ -65,6 +65,7 @@ export declare namespace Settings {
     useraleVersion: Version;
     userId: UserId;
     version?: Version;
+    websocketsEnabled?: boolean;
   }
 
   export interface IConfiguration extends Config {
@@ -142,10 +143,19 @@ export declare namespace Events {
   }>;
 }
 
-export interface CallbackHandlerOptions<T> {
-  getValue: () => T;
-  validate: (value: T) => void;
-  setValue: (config: any, value: T) => void;
-  description: string; // for error messages
+export declare namespace Callbacks {
+  export type AuthCallback = () => string;
+  export type HeadersCallback = () => Settings.HeaderObject;
+
+  export type CallbackMap = {
+    [key in string]: CallableFunction;
+  };
 }
 
+export declare namespace Extension {
+  export type PluginConfig = { urlWhitelist: string };
+  export type ConfigPayload = {
+    useraleConfig: Partial<Settings.Config>;
+    pluginConfig: PluginConfig;
+  };
+}
diff --git a/products/userale/packages/flagon-userale/src/utils/auth/index.ts 
b/products/userale/packages/flagon-userale/src/utils/auth/index.ts
new file mode 100644
index 0000000..12dc7ba
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/utils/auth/index.ts
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Configuration } from "@/configure";
+import { Callbacks } from "@/types";
+
+export let authCallback: Callbacks.AuthCallback | null = null;
+
+/**
+ * Fetches the most up-to-date auth header string from the auth callback
+ * and updates the config object with the new value.
+ * @param {Configuration} config Configuration object to be updated.
+ * @param {Function} authCallback Callback used to fetch the newest header.
+ * @returns {void}
+ */
+export function updateAuthHeader(config: Configuration) {
+  if (authCallback) {
+    try {
+      config.authHeader = authCallback();
+    } catch (e) {
+      // We should emit the error, but otherwise continue as this could be a 
temporary issue
+      // due to network connectivity or some logic inside the authCallback 
which is the user's
+      // responsibility.
+      console.error(`Error encountered while setting the auth header: ${e}`);
+    }
+  }
+}
+
+/**
+ * Registers the provided callback to be used when updating the auth header.
+ * @param {Callbacks.AuthCallback} callback Callback used to fetch the newest 
header. Should return a string.
+ * @returns {boolean} Whether the operation succeeded.
+ */
+export function registerAuthCallback(callback: Callbacks.AuthCallback) {
+  try {
+    verifyCallback(callback);
+    authCallback = callback;
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+/**
+ * Verify that the provided callback is a function which returns a string
+ * @param {Function} callback Callback used to fetch the newest header. Should 
return a string.
+ * @throws {Error} If the callback is not a function or does not return a 
string.
+ * @returns {void}
+ */
+export function verifyCallback(callback: Callbacks.AuthCallback) {
+  if (typeof callback !== "function") {
+    throw new Error("Userale auth callback must be a function");
+  }
+  const result = callback();
+  if (typeof result !== "string") {
+    throw new Error("Userale auth callback must return a string");
+  }
+}
+
+/**
+ * Resets the authCallback to null. Used for primarily for testing, but could 
be used
+ * to remove the callback in production.
+ * @returns {void}
+ */
+export function resetAuthCallback() {
+  authCallback = null;
+}
diff --git 
a/products/userale/packages/flagon-userale/src/utils/headers/index.ts 
b/products/userale/packages/flagon-userale/src/utils/headers/index.ts
new file mode 100644
index 0000000..106f1f7
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/utils/headers/index.ts
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Configuration } from "@/configure";
+import { Callbacks } from "@/types";
+
+export let headersCallback: Callbacks.HeadersCallback | null = null;
+
+/**
+ * Fetches the most up-to-date custom headers object from the headers callback
+ * and updates the config object with the new value.
+ * @param {Configuration} config Configuration object to be updated.
+ * @param {Callbacks.HeadersCallback} headersCallback Callback used to fetch 
the newest headers.
+ * @returns {void}
+ */
+export function updateCustomHeaders(config: Configuration) {
+  if (headersCallback) {
+    try {
+      config.headers = headersCallback();
+    } catch (e) {
+      // We should emit the error, but otherwise continue as this could be a 
temporary issue
+      // due to network connectivity or some logic inside the headersCallback 
which is the user's
+      // responsibility.
+      console.error(`Error encountered while setting the headers: ${e}`);
+    }
+  }
+}
+
+/**
+ * Registers the provided callback to be used when updating the auth header.
+ * @param {Callbacks.HeadersCallback} callback Callback used to fetch the 
newest headers. Should return an object.
+ * @returns {boolean} Whether the operation succeeded.
+ */
+export function registerHeadersCallback(callback: Callbacks.HeadersCallback) {
+  try {
+    verifyCallback(callback);
+    headersCallback = callback;
+    return true;
+  } catch (e) {
+    return false;
+  }
+}
+
+/**
+ * Verify that the provided callback is a function which returns a string
+ * @param {Callbacks.HeadersCallback} callback Callback used to fetch the 
newest header. Should return an object.
+ * @throws {Error} If the callback is not a function or does not return a 
string.
+ * @returns {void}
+ */
+export function verifyCallback(callback: Callbacks.HeadersCallback) {
+  if (typeof callback !== "function") {
+    throw new Error("Userale headers callback must be a function");
+  }
+  const result = callback();
+  if (typeof result !== "object") {
+    throw new Error("Userale headers callback must return an object");
+  }
+  for (const [key, value] of Object.entries(result)) {
+    if (typeof key !== "string" || typeof value !== "string") {
+      throw new Error(
+        "Userale header callback must return an object with string keys and 
values",
+      );
+    }
+  }
+}
+
+/**
+ * Resets the authCallback to null. Used for primarily for testing, but could 
be used
+ * to remove the callback in production.
+ * @returns {void}
+ */
+export function resetHeadersCallback() {
+  headersCallback = null;
+}
diff --git a/products/userale/packages/flagon-userale/src/utils/index.ts 
b/products/userale/packages/flagon-userale/src/utils/index.ts
new file mode 100644
index 0000000..f472640
--- /dev/null
+++ b/products/userale/packages/flagon-userale/src/utils/index.ts
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export {
+  authCallback,
+  updateAuthHeader,
+  registerAuthCallback,
+  resetAuthCallback,
+  verifyCallback as verifyAuthCallback,
+} from "./auth";
+export {
+  headersCallback,
+  updateCustomHeaders,
+  registerHeadersCallback,
+  resetHeadersCallback,
+  verifyCallback as verifyHeadersCallback,
+} from "./headers";
diff --git a/products/userale/packages/flagon-userale/tsup.config.js 
b/products/userale/packages/flagon-userale/tsup.config.js
index 9ccd7ed..7e734de 100644
--- a/products/userale/packages/flagon-userale/tsup.config.js
+++ b/products/userale/packages/flagon-userale/tsup.config.js
@@ -22,34 +22,6 @@ export default defineConfig([
 
   http://www.apache.org/licenses/LICENSE-2.0
 
-  Unless required by applicable law or agreed to in writing, software
-  distributed under the License is distributed on an "AS IS" BASIS,
-  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  See the License for the specific language governing permissions and
-  limitations under the License.*/`,
-    },
-  },
-  {
-    tsconfig: './tsconfig.json',
-    entry: ['src/iife.ts'],
-    outDir: 'build/iife',
-    format: ['iife'],
-    name: 'userale',
-    target: 'es2021',
-    dts: true,
-    sourcemap: true,
-    clean: true,
-    minify: false,
-    banner: {
-      js: `/* Licensed to the Apache Software Foundation (ASF) under one or 
more
-  contributor license agreements. See the NOTICE file distributed with
-  this work for additional information regarding copyright ownership.
-  The ASF licenses this file to you under the Apache License, Version 2.0
-  (the "License"); you may not use this file except in compliance with
-  the License. You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

Reply via email to