UNPKG

loupe-typescript

Version:
646 lines 25.8 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const clientPlatform = __importStar(require("platform")); const StackTrace = __importStar(require("stacktrace-js")); const exception_1 = require("./exception"); const Header_1 = require("./Header"); const localStorageMessage_1 = require("./localStorageMessage"); const LogMessageSeverity_1 = require("./LogMessageSeverity"); const MethodSourceInfo_1 = require("./MethodSourceInfo"); class LoupeAgent { constructor(window, document) { this.window = window; this.document = document; this.propagateError = false; this.maxRequestSize = 204800; this.messageInterval = 10; this.sequenceNumber = 0; this.messageStorage = []; this.storageAvailable = this.storageSupported(); this.storageFull = false; this.corsOrigin = null; this.globalKeyList = []; this.debounce = (func, waitFor) => { let timeout = 0; const debounced = (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func(...args), waitFor); }; return debounced; }; if (typeof this.window !== 'undefined' && typeof this.window.onerror !== 'undefined') { this.existingOnError = this.window.onerror; this.setUpOnError(this.window); } this.setUpClientSessionId(); this.setUpSequenceNumber(); this.addSendMessageCommandToEventQueue(); } verbose(category, caption, description, parameters, exception, details, methodSourceInfo) { this.write(LogMessageSeverity_1.LogMessageSeverity.verbose, category, caption, description, parameters, exception, details, methodSourceInfo); } information(category, caption, description, parameters, exception, details, methodSourceInfo) { this.write(LogMessageSeverity_1.LogMessageSeverity.information, category, caption, description, parameters, exception, details, methodSourceInfo); } warning(category, caption, description, parameters, exception, details, methodSourceInfo) { this.write(LogMessageSeverity_1.LogMessageSeverity.warning, category, caption, description, parameters, exception, details, methodSourceInfo); } error(category, caption, description, parameters, exception, details, methodSourceInfo) { this.write(LogMessageSeverity_1.LogMessageSeverity.error, category, caption, description, parameters, exception, details, methodSourceInfo); } critical(category, caption, description, parameters, exception, details, methodSourceInfo) { this.write(LogMessageSeverity_1.LogMessageSeverity.critical, category, caption, description, parameters, exception, details, methodSourceInfo); } write(severity, category, caption, description, parameters, exception, details, methodSourceInfo) { exception = this.sanitiseArgument(exception); details = this.sanitiseArgument(details); if (details && typeof details !== 'string') { details = JSON.stringify(details); } methodSourceInfo = this.sanitiseArgument(methodSourceInfo); if (methodSourceInfo && !(methodSourceInfo instanceof MethodSourceInfo_1.MethodSourceInfo)) { methodSourceInfo = this.buildMessageSourceInfo(methodSourceInfo); } this.createMessage(severity, category, caption, description, parameters, exception, details, methodSourceInfo); this.addSendMessageCommandToEventQueue(); } addSendMessageCommandToEventQueue() { if ((this.storageAvailable && localStorage.length) || this.messageStorage.length) { setTimeout(() => this.logMessageToServer(), this.messageInterval); } } setSessionId(value) { this.sessionId = value; } setCORSOrigin(value) { this.corsOrigin = value; } setAuthorizationHeader(header) { if (header) { if (header.name && header.value) { this.authHeader = header; } else { this.consoleLog("setAuthorizationHeader failed. The header provided appears invalid as it doesn't have name & value"); } } else { this.consoleLog('setAuthorizationHeader failed. No header object provided'); } } clientSessionHeader() { return new Header_1.Header('loupe-agent-sessionId', this.agentSessionId); } resetMessageInterval(interval) { let newInterval = interval || 10; if (newInterval < 10) { newInterval = 10; } if (newInterval < this.messageInterval) { this.messageInterval = newInterval; } } storageSupported() { const testValue = '_loupe_storage_test_'; try { localStorage.setItem(testValue, testValue); localStorage.removeItem(testValue); return true; } catch (e) { return false; } } sanitiseArgument(parameter) { if (typeof parameter === 'undefined') { return null; } return parameter; } buildMessageSourceInfo(data) { return new MethodSourceInfo_1.MethodSourceInfo(data.file || null, data.method || null, data.line || null, data.column || null); } setUpOnError(window) { if (typeof this.window.onerror === 'undefined') { this.consoleLog('Gibraltar Loupe JavaScript Logger: No onerror event; errors cannot be logged to Loupe'); return; } this.window.onerror = (event, source, lineno, colno, error) => { if (this.existingOnError) { this.existingOnError(event, source, lineno, colno, error); } setTimeout(() => this.logError(event, source, lineno, colno, error), 10); return !this.propagateError; }; } getPlatform() { const platformDetails = clientPlatform; platformDetails.size = { height: this.window.innerHeight || this.document.body.clientHeight, width: this.window.innerWidth || this.document.body.clientWidth, }; return platformDetails; } getStackTrace(error, errorMessage) { if (typeof error === 'undefined' || error === null || !error.stack) { return this.createStackFromMessage(errorMessage); } return this.createStackFromError(error); } createStackFromMessage(errorMessage) { if (StackTrace) { try { StackTrace.fromError(new Error(errorMessage)).then((stack) => { return this.stripLoupeStackFrames(stack.reverse()); }); } catch (e) { } } return []; } createStackFromError(error) { if (error.stack.substring(error.stack.length - 1) === '\n') { error.stack = error.stack.substring(0, error.stack.length - 1); } return error.stack.split('\n'); } stripLoupeStackFrames(stack) { if (stack) { const userFramesStartPosition = this.userFramesStartAt(stack); if (userFramesStartPosition > 0) { stack = stack.slice(userFramesStartPosition); } } return stack; } userFramesStartAt(stack) { const loupeMethods = ['logError', 'getStackTrace', 'createStackFromMessage', 'createStackTrace']; let position = 0; if (stack[0].toString().indexOf('Cannot access caller') > -1) { position++; } for (; position < loupeMethods.length; position++) { if (stack.length < position) { break; } let functionName = stack[position].functionName; if (!functionName) { functionName = stack[position].toString(); } if (functionName.indexOf(loupeMethods[position]) === -1) { break; } } return position; } logError(msg, url, line, column, error) { let errorName = ''; if (error) { errorName = error.name || 'Exception'; } const exception = { cause: errorName, column, line, message: msg, stackTrace: this.getStackTrace(error, msg), url, }; this.createMessage(LogMessageSeverity_1.LogMessageSeverity.error, 'JavaScript', errorName, '', null, exception, null, null); return this.logMessageToServer(); } checkForStorageQuotaReached(e) { if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED' || e.name === 'QuotaExceededError') { this.storageFull = true; return true; } return false; } setUpClientSessionId() { const currentClientSessionId = this.getClientSessionHeader(); if (currentClientSessionId) { this.agentSessionId = currentClientSessionId; } else { this.agentSessionId = this.generateUUID(); this.storeClientSessionId(this.agentSessionId); } } storeClientSessionId(sessionIdToStore) { if (this.storageAvailable && !this.storageFull) { try { sessionStorage.setItem('LoupeAgentSessionId', sessionIdToStore); } catch (e) { if (this.checkForStorageQuotaReached(e)) { return; } this.consoleLog('Unable to store clientSessionId in session storage. ' + e.message); } } } getClientSessionHeader() { try { const clientSessionId = sessionStorage.getItem('LoupeAgentSessionId'); if (clientSessionId) { return clientSessionId; } } catch (e) { this.consoleLog('Unable to retrieve clientSessionId number from session storage. ' + e.message); } return null; } setUpSequenceNumber() { const sequence = this.getSequenceNumber(); if (sequence === -1 && this.storageAvailable) { this.sequenceNumber = 0; } else { this.sequenceNumber = sequence; } } getNextSequenceNumber() { let storedSequenceNumber; if (this.storageAvailable) { storedSequenceNumber = this.getSequenceNumber(); if (storedSequenceNumber < this.sequenceNumber) { storedSequenceNumber = this.sequenceNumber; } if (storedSequenceNumber !== -1) { storedSequenceNumber++; if (this.setSequenceNumber(storedSequenceNumber)) { this.sequenceNumber = storedSequenceNumber; return this.sequenceNumber; } } } this.sequenceNumber++; return this.sequenceNumber; } getSequenceNumber() { if (this.storageAvailable) { try { const currentNumber = sessionStorage.getItem('LoupeSequenceNumber'); if (currentNumber) { return parseInt(currentNumber); } else { return 0; } } catch (e) { this.consoleLog('Unable to retrieve sequence number from session storage. ' + e.message); } } return -1; } setSequenceNumber(sequenceNumber) { try { sessionStorage.setItem('LoupeSequenceNumber', sequenceNumber.toString()); return true; } catch (e) { if (this.checkForStorageQuotaReached(e)) { this.consoleLog('Unable to store sequence number as storage quote reached: ' + e.message); return false; } this.consoleLog('Unable to store sequence number: ' + e.message); return false; } } createMessage(severity, category, caption, description, parameters, exception, details, methodSourceInfo) { const messageSequenceNumber = this.getNextSequenceNumber(); const timeStamp = this.createTimeStamp(); if (exception) { exception = this.createExceptionFromError(exception, null); } const message = new localStorageMessage_1.LocalStorageMessage(severity, category, caption, description, parameters, exception, details, methodSourceInfo, timeStamp, messageSequenceNumber, this.agentSessionId, this.sessionId); this.storeMessage(message); } storeMessage(message) { if (this.storageAvailable && !this.storageFull) { try { localStorage.setItem('Loupe-message-' + this.generateUUID(), JSON.stringify(message)); } catch (e) { this.checkForStorageQuotaReached(e); this.consoleLog('Error occured trying to add item to localStorage: ' + e.message); this.messageStorage.push(JSON.stringify(message)); } } else { if (this.messageStorage.length === 5000) { this.messageStorage.shift(); } this.messageStorage.push(JSON.stringify(message)); } } createExceptionFromError(error, cause) { if (typeof error === 'string') { return new exception_1.Exception(cause || '', null, null, error, [], this.window.location.href); } if ('url' in error) { return error; } return new exception_1.Exception(cause || '', error.columnNumber || null, error.lineNumber || null, error.message, error.stackTrace || error.stack || null, this.window.location.href); } createTimeStamp() { const now = new Date(); const tzo = -now.getTimezoneOffset(); const dif = tzo >= 0 ? '+' : '-'; const pad = (num) => { const norm = Math.abs(Math.floor(num)); return (norm < 10 ? '0' : '') + norm; }; return (now.getFullYear() + '-' + pad(now.getMonth() + 1) + '-' + pad(now.getDate()) + 'T' + pad(now.getHours()) + ':' + pad(now.getMinutes()) + ':' + pad(now.getSeconds()) + '.' + pad(now.getMilliseconds()) + dif + pad(tzo / 60) + ':' + pad(tzo % 60)); } generateUUID() { let d = Date.now(); const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = (d + Math.random() * 16) % 16 | 0; d = Math.floor(d / 16); return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); }); return uuid; } truncateDetails(storedData) { if (storedData.message.details) { const messageSizeWithoutDetails = storedData.size - storedData.message.details.length; if (messageSizeWithoutDetails < this.maxRequestSize) { const details = { message: 'User supplied details truncated as log message exceeded maximum size.' }; storedData.message.details = JSON.stringify(details); const messageSize = JSON.stringify(storedData); storedData.size = messageSize.length; } } return storedData; } dropMessage(storedData) { this.removeMessagesFromStorage([storedData.key]); const droppedCaption = storedData.message.caption; const droppedDescription = storedData.message.description; if (droppedCaption.length + droppedDescription.length < this.maxRequestSize - 400) { this.createMessage(LogMessageSeverity_1.LogMessageSeverity.error, 'Loupe', 'Dropped message', 'Message was dropped as its size exceeded our max request size. Caption was {0} and description {1}', [droppedCaption, droppedDescription]); } else { if (droppedCaption.length < this.maxRequestSize - 400) { this.createMessage(LogMessageSeverity_1.LogMessageSeverity.error, 'Loupe', 'Dropped message', 'Message was dropped as its size exceeded our max request size. Caption was {0}', [droppedCaption]); } else { this.createMessage(LogMessageSeverity_1.LogMessageSeverity.error, 'Loupe', 'Dropped message', 'Message was dropped as its size exceeded our max request size.\nUnable to log caption or description as they exceed max request size'); } } } overSizeMessage(storedData) { let messageTooLarge = false; if (storedData.size > this.maxRequestSize) { storedData = this.truncateDetails(storedData); if (storedData.size > this.maxRequestSize) { this.dropMessage(storedData); messageTooLarge = true; } } return messageTooLarge; } messageSort(a, b) { const firstDate = new Date(a.message.timeStamp); const secondDate = new Date(b.message.timeStamp); if (firstDate > secondDate) { return -1; } if (firstDate < secondDate) { return 1; } return a.message.sequence - b.message.sequence; } getMessagesToSend() { let messages = []; const keys = []; let moreMessagesInStorage = false; let messagesFromStorage = []; if (this.messageStorage.length) { messages = this.messageStorage.slice(); this.messageStorage.length = 0; } if (this.storageAvailable) { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key != null && key.indexOf('Loupe-message-') > -1) { if (this.globalKeyList.indexOf(key) === -1) { const message = localStorage.getItem(key); if (message != null) { messagesFromStorage.push({ key: localStorage.key(i), message: JSON.parse(message), size: message.length, }); } } } } } if (messagesFromStorage.length && messagesFromStorage.length > 1) { messagesFromStorage.sort(this.messageSort); } if (messagesFromStorage.length > 10) { moreMessagesInStorage = true; messagesFromStorage = messagesFromStorage.splice(0, 10); } if (this.messageInterval !== 10) { messagesFromStorage = messagesFromStorage.splice(0, 1); } let cumulativeSize = 0; for (const msg of messagesFromStorage) { if (this.overSizeMessage(msg)) { continue; } cumulativeSize += msg.size; if (cumulativeSize > this.maxRequestSize) { break; } messages.push(msg.message); if (msg.key) { keys.push(msg.key); } } if (keys.length) { Array.prototype.push.apply(this.globalKeyList, keys); } return { messages, keys, moreMessagesInStorage }; } removeKeysFromGlobalList(keys) { if (this.globalKeyList.length && keys) { const position = this.globalKeyList.indexOf(keys[0]); this.globalKeyList.splice(position, keys.length); } } removeMessagesFromStorage(keys) { if (!keys) { return; } for (const key of keys) { try { localStorage.removeItem(key); } catch (e) { this.consoleLog('Unable to remove message from localStorage: ' + e.message); } } } setMessageInterval(callFailed) { if (!callFailed && this.messageInterval === 10) { return; } if (this.messageInterval < 10000) { if (callFailed) { this.messageInterval = this.messageInterval * 10; } else { this.messageInterval = this.messageInterval / 10; if (this.messageInterval < 10) { this.messageInterval = 10; } } return; } if (this.messageInterval === 10000) { if (callFailed) { this.messageInterval = 30000; } else { this.messageInterval = 1000; } return; } if (!callFailed && this.messageInterval === 30000) { this.messageInterval = 10000; return; } if (callFailed) { if (this.messageInterval < 960000) { this.messageInterval = this.messageInterval * 2; } } else { this.messageInterval = this.messageInterval / 2; } } logMessageToServer() { const { messages, keys, moreMessagesInStorage } = this.getMessagesToSend(); if (!messages.length) { return false; } const logMessage = { logMessages: messages, session: { client: this.getPlatform(), currentAgentSessionId: this.agentSessionId, }, }; const updateMessageInterval = this.debounce(() => this.setMessageInterval, 500); return this.sendMessageToServer(logMessage, keys, moreMessagesInStorage, updateMessageInterval); } afterRequest(callFailed, moreMessages, updateMessageInterval) { updateMessageInterval(callFailed); if (this.storageFull && !callFailed) { this.storageFull = false; } if (moreMessages) { this.addSendMessageCommandToEventQueue(); } } requestSucceeded(keys, moreMessages, updateMessageInterval) { this.removeMessagesFromStorage(keys); this.afterRequest(false, moreMessages, updateMessageInterval); } requestFailed(xhr, keys, moreMessages, updateMessageInterval) { if (xhr.status === 0 || xhr.status === 401) { this.removeKeysFromGlobalList(keys); } else { this.removeMessagesFromStorage(keys); } this.consoleLog('Loupe JavaScript Logger: Failed to log to ' + this.window.location.origin + '/loupe/log'); this.consoleLog(' Status: ' + xhr.status + ': ' + xhr.statusText); this.afterRequest(true, moreMessages, updateMessageInterval); } sendMessageToServer(logMessage, keys, moreMessages, updateMessageInterval) { try { let origin = this.corsOrigin || this.window.location.origin; origin = this.stripTrailingSlash(origin); const xhr = this.createCORSRequest(origin + '/loupe/log'); if (!xhr) { this.consoleLog('Loupe JavaScript Logger: No XMLHttpRequest; error cannot be logged to Loupe'); return false; } xhr.onreadystatechange = () => { if (xhr && xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status <= 204) { this.requestSucceeded(keys, moreMessages, updateMessageInterval); } else { this.requestFailed(xhr, keys, moreMessages, updateMessageInterval); } } }; xhr.send(JSON.stringify(logMessage)); return true; } catch (e) { this.consoleLog('Loupe JavaScript Logger: Exception while attempting to log'); return false; } } stripTrailingSlash(origin) { return origin.replace(/\/$/, ''); } createCORSRequest(url) { if (typeof XMLHttpRequest === 'undefined') { return null; } const xhr = new XMLHttpRequest(); if ('withCredentials' in xhr) { xhr.open('POST', url, true); xhr.setRequestHeader('Content-type', 'application/json'); if (this.authHeader) { xhr.setRequestHeader(this.authHeader.name, this.authHeader.value); } } else { return null; } return xhr; } consoleLog(msg) { if (console && typeof console.log === 'function') { console.log(msg); } } } exports.LoupeAgent = LoupeAgent; //# sourceMappingURL=loupe.agent.js.map