From c0256863398d5d47a838af8d37a04ecb865553e3 Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 17 Jan 2024 15:42:52 -0500 Subject: [PATCH 1/2] #397 adds registerAuthCallback function using strategy pattern; adds tests --- src/auth.js | 78 +++++++++++++++++++++++++ src/main.js | 1 + src/sendLogs.js | 29 ++++++---- test/auth_spec.js | 128 ++++++++++++++++++++++++++++++++++++++++++ test/sendLogs_spec.js | 41 +++++++++++++- 5 files changed, 265 insertions(+), 12 deletions(-) create mode 100644 src/auth.js create mode 100644 test/auth_spec.js diff --git a/src/auth.js b/src/auth.js new file mode 100644 index 00000000..cb8099a4 --- /dev/null +++ b/src/auth.js @@ -0,0 +1,78 @@ +/* + * 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 let authCallback = null; + +/** + * Fetches the most up-to-date auth header string from the auth callback + * and updates the config object with the new value. + * @param {Object} config Configuration object to be updated. + * @param {Function} authCallback Callback used to fetch the newest header. + * @returns {void} + */ +export function updateAuthHeader(config) { + 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 {Function} callback Callback used to fetch the newest header. Should return a string. + * @returns {boolean} Whether the operation succeeded. + */ +export function registerAuthCallback(callback) { + 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) { + 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; +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index 6d09c71b..f34c080f 100644 --- a/src/main.js +++ b/src/main.js @@ -32,6 +32,7 @@ window.onload = function () { export let started = false; export {defineCustomDetails as details} from './attachHandlers.js'; +export {registerAuthCallback as registerAuthCallback} from './auth.js'; export { addCallbacks as addCallbacks, removeCallbacks as removeCallbacks, diff --git a/src/sendLogs.js b/src/sendLogs.js index c2da2467..d420fa02 100644 --- a/src/sendLogs.js +++ b/src/sendLogs.js @@ -5,9 +5,9 @@ * 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. @@ -15,6 +15,8 @@ * limitations under the License. */ +import { updateAuthHeader } from "./auth.js"; + let sendIntervalId = null; /** @@ -39,7 +41,7 @@ export function initSender(logs, config) { * @return {Number} The newly created interval id. */ export function sendOnInterval(logs, config) { - return setInterval(function() { + return setInterval(function () { if (!config.on) { return; } @@ -57,8 +59,13 @@ export function sendOnInterval(logs, config) { * @param {Object} config Configuration object to be read from. */ export function sendOnClose(logs, config) { - window.addEventListener('pagehide', function () { + window.addEventListener("pagehide", function () { if (config.on && logs.length > 0) { + // NOTE: sendBeacon does not support auth headers, + // so this will fail if auth is required. + // The alternative is to use fetch() with keepalive: true + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description + // https://stackoverflow.com/a/73062712/9263449 navigator.sendBeacon(config.url, JSON.stringify(logs)); logs.splice(0); // clear log queue } @@ -76,18 +83,18 @@ export function sendOnClose(logs, config) { // @todo expose config object to sendLogs replate url with config.url export function sendLogs(logs, config, retries) { const req = new XMLHttpRequest(); - - // @todo setRequestHeader for Auth const data = JSON.stringify(logs); - req.open('POST', config.url); + req.open("POST", config.url); + + // Update headers + updateAuthHeader(config); if (config.authHeader) { - req.setRequestHeader('Authorization', config.authHeader) + req.setRequestHeader("Authorization", config.authHeader); } + req.setRequestHeader("Content-type", "application/json;charset=UTF-8"); - req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); - - req.onreadystatechange = function() { + req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { sendLogs(logs, config, retries--); diff --git a/test/auth_spec.js b/test/auth_spec.js new file mode 100644 index 00000000..86029cbe --- /dev/null +++ b/test/auth_spec.js @@ -0,0 +1,128 @@ +/* + * 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 {expect} from 'chai'; +import sinon from 'sinon'; +import {authCallback, registerAuthCallback, resetAuthCallback, updateAuthHeader, verifyCallback} from '../src/auth'; + +describe('verifyCallback', () => { + it('should not throw error for valid callback', () => { + const validCallback = sinon.stub().returns('someString'); + expect(() => verifyCallback(validCallback)).to.not.throw(); + }); + + it('should throw error for non-function callback', () => { + const nonFunctionCallback = 'notAFunction'; + expect(() => verifyCallback(nonFunctionCallback)).to.throw('Userale auth callback must be a function'); + }); + + it('should throw error for non-string callback return', () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(() => verifyCallback(invalidReturnCallback)).to.throw('Userale auth callback must return a string'); + }); + + it('should not throw error for valid callback with empty string return', () => { + const validCallback = sinon.stub().returns(''); + expect(() => verifyCallback(validCallback)).to.not.throw(); + }); +}); + +describe('registerAuthCallback', () => { + afterEach(() => { + resetAuthCallback(); + }); + + it('should register a valid callback', () => { + const validCallback = sinon.stub().returns('someString'); + expect(registerAuthCallback(validCallback)).to.be.true; + expect(authCallback).to.equal(validCallback); + }); + + it('should not register a non-function callback', () => { + const nonFunctionCallback = 'notAFunction'; + expect(registerAuthCallback(nonFunctionCallback)).to.be.false; + expect(authCallback).to.be.null; + }); + + it('should not register a callback with invalid return type', () => { + const invalidReturnCallback = sinon.stub().returns(123); + expect(registerAuthCallback(invalidReturnCallback)).to.be.false; + expect(authCallback).to.be.null; + }); + + it('should register a callback with empty string return', () => { + const validCallback = sinon.stub().returns(''); + expect(registerAuthCallback(validCallback)).to.be.true; + expect(authCallback).to.equal(validCallback); + }); +}); + +describe('updateAuthHeader', () => { + let config; + + beforeEach(() => { + // Initialize config object before each test + config = { authHeader: null }; + }); + + afterEach(() => { + resetAuthCallback(); + }); + + it('should update auth header when authCallback is provided', () => { + const validCallback = sinon.stub().returns('someString'); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal('someString'); + }); + + it('should not update auth header when authCallback is not provided', () => { + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it('should not update auth header when authCallback returns non-string', () => { + const invalidReturnCallback = sinon.stub().returns(123); + registerAuthCallback(invalidReturnCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it('should update auth header with empty string return from authCallback', () => { + const validCallback = sinon.stub().returns(''); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal(''); + }); + + it('should handle errors thrown during authCallback execution', () => { + const errorThrowingCallback = sinon.stub().throws(new Error('Callback execution failed')); + registerAuthCallback(errorThrowingCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.be.null; + }); + + it('should not update auth header after unregistering authCallback', () => { + const validCallback = sinon.stub().returns('someString'); + registerAuthCallback(validCallback); + updateAuthHeader(config, authCallback); + expect(config.authHeader).to.equal('someString'); + + // Unregister authCallback + updateAuthHeader(config, null); + expect(config.authHeader).to.equal('someString'); + }); + }); \ No newline at end of file diff --git a/test/sendLogs_spec.js b/test/sendLogs_spec.js index 35686e6c..60597244 100644 --- a/test/sendLogs_spec.js +++ b/test/sendLogs_spec.js @@ -17,7 +17,8 @@ import {expect} from 'chai'; import {JSDOM} from 'jsdom'; import sinon from 'sinon'; -import {sendOnInterval, sendOnClose} from '../src/sendLogs'; +import {initSender, sendOnInterval, sendOnClose} from '../src/sendLogs'; +import {registerAuthCallback} from '../src/auth'; import 'global-jsdom/register' describe('sendLogs', () => { @@ -110,4 +111,42 @@ describe('sendLogs', () => { global.window.dispatchEvent(new window.CustomEvent('pagehide')) sinon.assert.notCalled(sendBeaconSpy) }); + + it('sends logs with proper auth header when using registerCallback', (done) => { + let requests = [] + const originalXMLHttpRequest = global.XMLHttpRequest; + const conf = { on: true, transmitInterval: 500, url: 'test', logCountThreshold: 1 }; + const logs = []; + const clock = sinon.useFakeTimers(); + const xhr = sinon.useFakeXMLHttpRequest(); + global.XMLHttpRequest = xhr; + xhr.onCreate = (xhr) => { + requests.push(xhr); + }; + + // Mock the authCallback function + const authCallback = sinon.stub().returns('fakeAuthToken'); + + // Register the authCallback + registerAuthCallback(authCallback); + + // Initialize sender with logs and config + initSender(logs, conf); + + // Simulate log entry + logs.push({ foo: 'bar' }); + + // Trigger interval to send logs + clock.tick(conf.transmitInterval); + + // Verify that the request has the proper auth header + expect(requests.length).to.equal(1); + expect(requests[0].requestHeaders.Authorization).to.equal('fakeAuthToken'); + + // Restore XMLHttpRequest and clock + xhr.restore(); + clock.restore(); + global.XMLHttpRequest = originalXMLHttpRequest; + done() + }); }); From 5ea9251360125a6c5c0917178d5d20a626b49b3a Mon Sep 17 00:00:00 2001 From: Evan Jones Date: Wed, 24 Jan 2024 14:47:51 -0500 Subject: [PATCH 2/2] add updated build --- build/UserALEWebExtension/background.js | 19 ++--- build/UserALEWebExtension/content.js | 19 ++--- build/userale-2.4.0.js | 93 ++++++++++++++++++++++--- build/userale-2.4.0.min.js | 2 +- 4 files changed, 108 insertions(+), 25 deletions(-) diff --git a/build/UserALEWebExtension/background.js b/build/UserALEWebExtension/background.js index 126c4740..845cad69 100644 --- a/build/UserALEWebExtension/background.js +++ b/build/UserALEWebExtension/background.js @@ -347,9 +347,9 @@ function extractTimeFields(timeStamp) { * 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. @@ -397,8 +397,13 @@ function sendOnInterval(logs, config) { * @param {Object} config Configuration object to be read from. */ function sendOnClose(logs, config) { - window.addEventListener('pagehide', function () { + window.addEventListener("pagehide", function () { if (config.on && logs.length > 0) { + // NOTE: sendBeacon does not support auth headers, + // so this will fail if auth is required. + // The alternative is to use fetch() with keepalive: true + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description + // https://stackoverflow.com/a/73062712/9263449 navigator.sendBeacon(config.url, JSON.stringify(logs)); logs.splice(0); // clear log queue } @@ -416,14 +421,12 @@ function sendOnClose(logs, config) { // @todo expose config object to sendLogs replate url with config.url function sendLogs(logs, config, retries) { var req = new XMLHttpRequest(); - - // @todo setRequestHeader for Auth var data = JSON.stringify(logs); - req.open('POST', config.url); + req.open("POST", config.url); if (config.authHeader) { - req.setRequestHeader('Authorization', config.authHeader); + req.setRequestHeader("Authorization", config.authHeader); } - req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); + req.setRequestHeader("Content-type", "application/json;charset=UTF-8"); req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { diff --git a/build/UserALEWebExtension/content.js b/build/UserALEWebExtension/content.js index 2ec7e1f7..d3dbf9b8 100644 --- a/build/UserALEWebExtension/content.js +++ b/build/UserALEWebExtension/content.js @@ -913,9 +913,9 @@ function attachHandlers(config) { * 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. @@ -963,8 +963,13 @@ function sendOnInterval(logs, config) { * @param {Object} config Configuration object to be read from. */ function sendOnClose(logs, config) { - window.addEventListener('pagehide', function () { + window.addEventListener("pagehide", function () { if (config.on && logs.length > 0) { + // NOTE: sendBeacon does not support auth headers, + // so this will fail if auth is required. + // The alternative is to use fetch() with keepalive: true + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description + // https://stackoverflow.com/a/73062712/9263449 navigator.sendBeacon(config.url, JSON.stringify(logs)); logs.splice(0); // clear log queue } @@ -982,14 +987,12 @@ function sendOnClose(logs, config) { // @todo expose config object to sendLogs replate url with config.url function sendLogs(logs, config, retries) { var req = new XMLHttpRequest(); - - // @todo setRequestHeader for Auth var data = JSON.stringify(logs); - req.open('POST', config.url); + req.open("POST", config.url); if (config.authHeader) { - req.setRequestHeader('Authorization', config.authHeader); + req.setRequestHeader("Authorization", config.authHeader); } - req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); + req.setRequestHeader("Content-type", "application/json;charset=UTF-8"); req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { diff --git a/build/userale-2.4.0.js b/build/userale-2.4.0.js index 6b89cc40..4876a80e 100644 --- a/build/userale-2.4.0.js +++ b/build/userale-2.4.0.js @@ -975,9 +975,79 @@ * 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. + */ + + var authCallback = null; + + /** + * Fetches the most up-to-date auth header string from the auth callback + * and updates the config object with the new value. + * @param {Object} config Configuration object to be updated. + * @param {Function} authCallback Callback used to fetch the newest header. + * @returns {void} + */ + function updateAuthHeader(config) { + 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: ".concat(e)); + } + } + } + + /** + * Registers the provided callback to be used when updating the auth header. + * @param {Function} callback Callback used to fetch the newest header. Should return a string. + * @returns {boolean} Whether the operation succeeded. + */ + function registerAuthCallback(callback) { + 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} + */ + function verifyCallback(callback) { + if (typeof callback !== "function") { + throw new Error("Userale auth callback must be a function"); + } + var result = callback(); + if (typeof result !== "string") { + throw new Error("Userale auth callback must return a string"); + } + } + + /* + * 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. @@ -1025,8 +1095,13 @@ * @param {Object} config Configuration object to be read from. */ function sendOnClose(logs, config) { - window.addEventListener('pagehide', function () { + window.addEventListener("pagehide", function () { if (config.on && logs.length > 0) { + // NOTE: sendBeacon does not support auth headers, + // so this will fail if auth is required. + // The alternative is to use fetch() with keepalive: true + // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon#description + // https://stackoverflow.com/a/73062712/9263449 navigator.sendBeacon(config.url, JSON.stringify(logs)); logs.splice(0); // clear log queue } @@ -1044,14 +1119,15 @@ // @todo expose config object to sendLogs replate url with config.url function sendLogs(logs, config, retries) { var req = new XMLHttpRequest(); - - // @todo setRequestHeader for Auth var data = JSON.stringify(logs); - req.open('POST', config.url); + req.open("POST", config.url); + + // Update headers + updateAuthHeader(config); if (config.authHeader) { - req.setRequestHeader('Authorization', config.authHeader); + req.setRequestHeader("Authorization", config.authHeader); } - req.setRequestHeader('Content-type', 'application/json;charset=UTF-8'); + req.setRequestHeader("Content-type", "application/json;charset=UTF-8"); req.onreadystatechange = function () { if (req.readyState === 4 && req.status !== 200) { if (retries > 0) { @@ -1164,6 +1240,7 @@ exports.options = options; exports.packageCustomLog = packageCustomLog; exports.packageLog = packageLog; + exports.registerAuthCallback = registerAuthCallback; exports.removeCallbacks = removeCallbacks; exports.start = start; exports.stop = stop; diff --git a/build/userale-2.4.0.min.js b/build/userale-2.4.0.min.js index 81d0c308..6d4277b9 100644 --- a/build/userale-2.4.0.min.js +++ b/build/userale-2.4.0.min.js @@ -15,4 +15,4 @@ * limitations under the License. * @preserved */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).userale={})}(this,(function(e){"use strict";function t(e){return t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t(e)}var n="2.4.0",o=null;function r(e,t){var n=e.autostart,o=t.autostart;Object.keys(t).forEach((function(n){if("userFromParams"===n){var o=(r=t[n],i=new RegExp("[?&]"+r+"(=([^&#]*)|&|#|$)"),(a=window.location.href.match(i))&&a[2]?decodeURIComponent(a[2].replace(/\+/g," ")):null);o&&(e.userId=o)}var r,i,a;e[n]=t[n]})),!1!==n&&!1!==o||(e.autostart=!1)}var i=function(e,t,n){if(n||2===arguments.length)for(var o,r=0,i=t.length;r0?v.time(e.timeStamp):Date.now(),{milli:Math.floor(o),micro:Number((o%1).toFixed(3))}),i={target:B(e.target),path:P(e),pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:r.milli,microTime:r.micro,location:A(e),scrnRes:M(),type:e.type,logType:"raw",userAction:!0,details:n,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},a=0,s=Object.values(x);a0?v.time(e.timeStamp):Date.now());if(null==y&&(y=t,b=o,S=n,O=r,k=0),y!==t||b!==o){W={target:y,path:S,pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),count:k,duration:r-O,startTime:O,endTime:r,type:b,logType:"interval",targetChange:y!==t,typeChange:b!==o,userAction:!1,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID};for(var i=0,a=Object.values(x);i=t.logCountThreshold&&(H(e.slice(0),t,0),e.splice(0))}),t.transmitInterval)}(e,t),function(e,t){window.addEventListener("pagehide",(function(){t.on&&e.length>0&&(navigator.sendBeacon(t.url,JSON.stringify(e)),e.splice(0))}))}(e,t)}function H(e,t,n){var o=new XMLHttpRequest,r=JSON.stringify(e);o.open("POST",t.url),t.authHeader&&o.setRequestHeader("Authorization",t.authHeader),o.setRequestHeader("Content-type","application/json;charset=UTF-8"),o.onreadystatechange=function(){4===o.readyState&&200!==o.status&&n>0&&H(e,t,n--)},o.send(r)}var z,F={},Y=[],J=Date.now();window.onload=function(){z=Date.now()},e.started=!1,F.on=!1,F.useraleVersion=n,r(F,function(){var e={};null===o&&(o=function(e,t){if(null===window.sessionStorage.getItem(e))return window.sessionStorage.setItem(e,JSON.stringify(t)),t;return JSON.parse(window.sessionStorage.getItem(e))}("userAleSessionId","session_"+String(Date.now())));var t,n=document.currentScript||(t=document.getElementsByTagName("script"))[t.length-1],r=n?n.getAttribute.bind(n):function(){return null};return e.autostart="false"!==r("data-autostart"),e.url=r("data-url")||"http://localhost:8000",e.transmitInterval=+r("data-interval")||5e3,e.logCountThreshold=+r("data-threshold")||5,e.userId=r("data-user")||null,e.version=r("data-version")||null,e.logDetails="true"===r("data-log-details"),e.resolution=+r("data-resolution")||500,e.toolName=r("data-tool")||null,e.userFromParams=r("data-user-from-params")||null,e.time=function(e){var t;if(e.timeStamp&&e.timeStamp>0){var n=Date.now()-e.timeStamp;if(n<0)t=function(){return e.timeStamp/1e3};else if(n>e.timeStamp){var o=performance.timing.navigationStart;t=function(e){return e+o}}else t=function(e){return e}}else t=function(){return Date.now()};return t}(document.createEvent("CustomEvent")),e.sessionID=r("data-session")||o,e.authHeader=r("data-auth")||null,e.custIndex=r("data-index")||null,e}()),g=Y,v=F,x=[],y=null,b=null,S=null,O=null,k=0,W=null,F.autostart&&function t(n){e.started||setTimeout((function(){var o=document.readyState;!n.autostart||"interactive"!==o&&"complete"!==o?t(n):(X(n),_(Y,n),e.started=n.on=!0,C({type:"load",logType:"raw",details:{pageLoadTime:z-J}},(function(){}),!1))}),100)}(F);var U=n;e.addCallbacks=function(){for(var e=arguments.length,t=new Array(e),n=0;n0?v.time(e.timeStamp):Date.now(),{milli:Math.floor(o),micro:Number((o%1).toFixed(3))}),i={target:B(e.target),path:P(e),pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),clientTime:r.milli,microTime:r.micro,location:A(e),scrnRes:M(),type:e.type,logType:"raw",userAction:!0,details:n,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID},a=0,s=Object.values(x);a0?v.time(e.timeStamp):Date.now());if(null==y&&(y=t,b=o,S=n,O=r,k=0),y!==t||b!==o){E={target:y,path:S,pageUrl:window.location.href,pageTitle:document.title,pageReferrer:document.referrer,browser:j(),count:k,duration:r-O,startTime:O,endTime:r,type:b,logType:"interval",targetChange:y!==t,typeChange:b!==o,userAction:!1,userId:v.userId,toolVersion:v.version,toolName:v.toolName,useraleVersion:v.useraleVersion,sessionID:v.sessionID};for(var i=0,a=Object.values(x);i=t.logCountThreshold&&(z(e.slice(0),t,0),e.splice(0))}),t.transmitInterval)}(e,t),function(e,t){window.addEventListener("pagehide",(function(){t.on&&e.length>0&&(navigator.sendBeacon(t.url,JSON.stringify(e)),e.splice(0))}))}(e,t)}function z(e,t,n){var o=new XMLHttpRequest,r=JSON.stringify(e);o.open("POST",t.url),function(e){if($)try{e.authHeader=$()}catch(e){console.error("Error encountered while setting the auth header: ".concat(e))}}(t),t.authHeader&&o.setRequestHeader("Authorization",t.authHeader),o.setRequestHeader("Content-type","application/json;charset=UTF-8"),o.onreadystatechange=function(){4===o.readyState&&200!==o.status&&n>0&&z(e,t,n--)},o.send(r)}var F,U={},Y=[],J=Date.now();window.onload=function(){F=Date.now()},e.started=!1,U.on=!1,U.useraleVersion=n,r(U,function(){var e={};null===o&&(o=function(e,t){if(null===window.sessionStorage.getItem(e))return window.sessionStorage.setItem(e,JSON.stringify(t)),t;return JSON.parse(window.sessionStorage.getItem(e))}("userAleSessionId","session_"+String(Date.now())));var t,n=document.currentScript||(t=document.getElementsByTagName("script"))[t.length-1],r=n?n.getAttribute.bind(n):function(){return null};return e.autostart="false"!==r("data-autostart"),e.url=r("data-url")||"http://localhost:8000",e.transmitInterval=+r("data-interval")||5e3,e.logCountThreshold=+r("data-threshold")||5,e.userId=r("data-user")||null,e.version=r("data-version")||null,e.logDetails="true"===r("data-log-details"),e.resolution=+r("data-resolution")||500,e.toolName=r("data-tool")||null,e.userFromParams=r("data-user-from-params")||null,e.time=function(e){var t;if(e.timeStamp&&e.timeStamp>0){var n=Date.now()-e.timeStamp;if(n<0)t=function(){return e.timeStamp/1e3};else if(n>e.timeStamp){var o=performance.timing.navigationStart;t=function(e){return e+o}}else t=function(e){return e}}else t=function(){return Date.now()};return t}(document.createEvent("CustomEvent")),e.sessionID=r("data-session")||o,e.authHeader=r("data-auth")||null,e.custIndex=r("data-index")||null,e}()),g=Y,v=U,x=[],y=null,b=null,S=null,O=null,k=0,E=null,U.autostart&&function t(n){e.started||setTimeout((function(){var o=document.readyState;!n.autostart||"interactive"!==o&&"complete"!==o?t(n):(X(n),H(Y,n),e.started=n.on=!0,C({type:"load",logType:"raw",details:{pageLoadTime:F-J}},(function(){}),!1))}),100)}(U);var q=n;e.addCallbacks=function(){for(var e=arguments.length,t=new Array(e),n=0;n