loupe-typescript
Version:
Loupe Agent for Web clients
646 lines • 25.8 kB
JavaScript
"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