@backtrace/sdk-core
Version:
Backtrace-JavaScript SDK core library
1,465 lines (1,434 loc) • 117 kB
JavaScript
'use strict';
class BacktraceReportSubmissionResult {
message;
get result() {
return this._result;
}
status = 'Ok';
_result;
constructor(statusOrResponse, message) {
this.message = message;
if (this.isSubmissionResponse(statusOrResponse)) {
this.status = statusOrResponse;
return;
}
this._result = statusOrResponse;
}
static OnLimitReached(target = 'Server') {
return new BacktraceReportSubmissionResult('Limit reached', `${target} report limit reached`);
}
static SdkDisabled() {
return new BacktraceReportSubmissionResult('Disabled SDK');
}
static Unsupported(message) {
return new BacktraceReportSubmissionResult('Unsupported', message);
}
static ReportSkipped() {
return new BacktraceReportSubmissionResult('Report skipped');
}
static OnInternalServerError(message) {
return new BacktraceReportSubmissionResult('Server Error', message);
}
static OnInvalidToken() {
return new BacktraceReportSubmissionResult('Invalid token');
}
static OnUnknownError(message) {
return new BacktraceReportSubmissionResult('Unknown', message);
}
static OnNetworkingError(message) {
return new BacktraceReportSubmissionResult('Network Error', message);
}
static Ok(response) {
return new BacktraceReportSubmissionResult(response);
}
isSubmissionResponse(statusOrResponse) {
return typeof statusOrResponse === 'string';
}
}
function jsonEscaper() {
const ancestors = [];
const keys = [];
// in TypeScript add "this: any" param to avoid compliation errors - as follows
// return function (this: any, field: any, value: any) {
return function (key, value) {
if (value === null) {
return value;
}
const valueType = typeof value;
if (valueType === 'bigint') {
return value.toString();
}
if (valueType !== 'object') {
return value;
}
// `this` is the object that value is contained in,
// i.e., its direct parent.
while (ancestors.length > 0 && ancestors[ancestors.length - 1] !== this) {
ancestors.pop();
keys.pop();
}
if (ancestors.includes(value)) {
return `[Circular].${keys.filter((k) => !!k).join('.')}.${key}`;
}
keys.push(key);
ancestors.push(value);
return value;
};
}
class SubmissionUrlInformation {
static SUBMIT_PREFIX = 'submit.backtrace.io/';
/**
* Convert url/token from credentials to JSON submission URL
* @param url credentials URL
* @param token credentials token
* @returns JSON submissionURL
*/
static toJsonReportSubmissionUrl(url, token) {
// if the token doesn't exist - use URL
if (!token) {
return url;
}
// if the url points to submit, we should always use it without any modifications
if (url.includes(this.SUBMIT_PREFIX)) {
return url;
}
// if the URL has token in the URL, the user probably added a token once again
// in this case, don't do anything
if (url.indexOf(token) !== -1) {
return url;
}
const result = new URL(`/post`, url);
result.searchParams.append('format', 'json');
result.searchParams.append('token', token);
return result.href;
}
/**
* Converts full submission JSON URL to PlCrashReporter submission URL
* @param submissionUrl Backtrace Submission URL
*/
static toPlCrashReporterSubmissionUrl(submissionUrl) {
return this.changeSubmissionFormat(submissionUrl, 'plcrash');
}
/**
* Converts full submission JSON URL to minidump submission URL
* @param submissionUrl Backtrace Submission URL
*/
static toMinidumpSubmissionUrl(submissionUrl) {
return this.changeSubmissionFormat(submissionUrl, 'minidump');
}
static toAttachmentSubmissionUrl(submissionUrl, rxid, attachmentName) {
const query = `object=${rxid}&attachment_name=${attachmentName}`;
if (submissionUrl.includes('?')) {
return (submissionUrl += `&` + query);
}
return (submissionUrl += '?' + query);
}
/**
* Find the universe based on the submission URL
* @param submissionUrl submission URL - full submission URL to Backtrace.
* @returns universe name
*/
static findUniverse(submissionUrl) {
const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX);
if (submitIndex !== -1) {
// submit format URL
// submit.backtrace.io/universe/token/format
// we can expect the universe name just after the hostname
const universeStartIndex = submitIndex + this.SUBMIT_PREFIX.length;
const endOfUniverseName = submissionUrl.indexOf('/', universeStartIndex);
return submissionUrl.substring(universeStartIndex, endOfUniverseName);
}
// the universe name should be available in the hostname
// for example abc.sp.backtrace.io or zyx.in.backtrace.io or foo.backtrace.io
const domainIndex = submissionUrl.indexOf('.backtrace.io');
if (domainIndex === -1) {
return undefined;
}
const protocolSeparator = '://';
let protocolEndIndex = submissionUrl.indexOf(protocolSeparator);
if (protocolEndIndex === -1) {
protocolEndIndex = 0;
}
else {
protocolEndIndex += protocolSeparator.length;
}
const hostname = submissionUrl.substring(protocolEndIndex, domainIndex);
const endOfUniverseName = hostname.indexOf('.');
return endOfUniverseName === -1 ? hostname : hostname.substring(0, endOfUniverseName);
}
static findToken(submissionUrl) {
const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX);
if (submitIndex !== -1) {
const submissionUrlParts = submissionUrl.split('/');
// submit format URL
// submit.backtrace.io/universe/token/format
// by spliting the submission URL by `/` and dropping the last
// part of the URL, the last element on the list is the token.
return submissionUrlParts[submissionUrlParts.length - 2] ?? null;
}
const url = new URL(submissionUrl);
return url.searchParams.get('token');
}
static changeSubmissionFormat(submissionUrl, desiredFormat) {
const submitIndex = submissionUrl.indexOf(this.SUBMIT_PREFIX);
if (submitIndex !== -1) {
const queryParametersIndex = submissionUrl.indexOf('?');
const queryParameters = queryParametersIndex === -1 ? '' : submissionUrl.substring(queryParametersIndex);
const pathname = submissionUrl.substring(submitIndex + this.SUBMIT_PREFIX.length, queryParametersIndex === -1 ? undefined : queryParametersIndex);
const pathParts = pathname.split('/');
// path parts are prefixed with '/' character. Expected and valid submit format is:
// /universe/token/format
// splitting pathname should generate at least 4 elements ('', universe, token, format)
// if pathParts length is not equal to 4 then the invalid were passed.
const expectedMinimalPathParts = 3;
if (pathParts.length < expectedMinimalPathParts) {
return submissionUrl;
}
pathParts[2] = desiredFormat;
return (submissionUrl.substring(0, submitIndex + this.SUBMIT_PREFIX.length) +
pathParts.join('/') +
queryParameters);
}
else {
const url = new URL(submissionUrl);
url.searchParams.set('format', desiredFormat);
return url.href;
}
}
}
class RequestBacktraceReportSubmission {
_requestHandler;
_submissionUrl;
constructor(options, _requestHandler) {
this._requestHandler = _requestHandler;
this._submissionUrl = SubmissionUrlInformation.toJsonReportSubmissionUrl(options.url, options.token);
}
send(data, attachments, abortSignal) {
const json = JSON.stringify(data, jsonEscaper());
return this._requestHandler.postError(this._submissionUrl, json, attachments, abortSignal);
}
async sendAttachment(rxid, attachment, abortSignal) {
if (!this._requestHandler.postAttachment) {
return BacktraceReportSubmissionResult.Unsupported('postAttachment is not implemented');
}
return await this._requestHandler.postAttachment(SubmissionUrlInformation.toAttachmentSubmissionUrl(this._submissionUrl, rxid, attachment.name), attachment, abortSignal);
}
}
const DEFAULT_TIMEOUT = 15_000;
class ConnectionError {
/**
* Verifies if an Error is a connection error
* @param err error
* @returns true if the error was caused by ETIMEDOUT or ECONNRESET or ECONNABORTED
*/
static isConnectionError(err) {
const error = err;
return error.code === 'ETIMEDOUT' || error.code === 'ECONNRESET' || error.code === 'ECONNABORTED';
}
}
class MetricsUrlInformation {
static generateSummedEventsUrl(hostname, submissionUrl, credentialsToken) {
const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken);
if (!submissionInformation) {
return undefined;
}
return this.generateEventsServiceUrl(hostname, 'summed-events', submissionInformation.universe, submissionInformation.token);
}
static generateUniqueEventsUrl(hostname, submissionUrl, credentialsToken) {
const submissionInformation = this.findSubmissionInformation(submissionUrl, credentialsToken);
if (!submissionInformation) {
return undefined;
}
return this.generateEventsServiceUrl(hostname, 'unique-events', submissionInformation.universe, submissionInformation.token);
}
static generateEventsServiceUrl(hostname, eventServiceName, universe, token) {
return new URL(`/api/${eventServiceName}/submit?universe=${universe}&token=${token}`, hostname).toString();
}
static findSubmissionInformation(submissionUrl, token) {
const universe = SubmissionUrlInformation.findUniverse(submissionUrl);
if (!universe) {
return undefined;
}
token = token ?? SubmissionUrlInformation.findToken(submissionUrl);
if (!token) {
return undefined;
}
return { universe, token };
}
}
class BacktraceCoreApi {
_requestHandler;
_summedMetricsSubmissionUrl;
_uniqueMetricsSubmissionUrl;
_requestBacktraceReportSubmission;
constructor(options, _requestHandler) {
this._requestHandler = _requestHandler;
this._summedMetricsSubmissionUrl = MetricsUrlInformation.generateSummedEventsUrl(options.metrics?.url ?? 'https://events.backtrace.io', options.url, options.token);
this._uniqueMetricsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl(options.metrics?.url ?? 'https://events.backtrace.io', options.url, options.token);
this._requestBacktraceReportSubmission =
options.requestBacktraceReportSubmission ??
new RequestBacktraceReportSubmission({
url: options.url,
}, this._requestHandler);
}
sendReport(data, attachments, abortSignal) {
return this._requestBacktraceReportSubmission.send(data, attachments, abortSignal);
}
sendAttachment(rxid, attachment, abortSignal) {
return this._requestBacktraceReportSubmission.sendAttachment(rxid, attachment, abortSignal);
}
sendUniqueMetrics(metrics, abortSignal) {
if (!this._uniqueMetricsSubmissionUrl) {
throw new Error('Unique metrics URL is not available.');
}
return this._requestHandler.post(this._uniqueMetricsSubmissionUrl, JSON.stringify(metrics), abortSignal);
}
sendSummedMetrics(metrics, abortSignal) {
if (!this._summedMetricsSubmissionUrl) {
throw new Error('Summed metrics URL is not available.');
}
return this._requestHandler.post(this._summedMetricsSubmissionUrl, JSON.stringify(metrics), abortSignal);
}
}
class Events {
_callbacks = {};
on(event, callback) {
this.addCallback(event, { callback });
return this;
}
once(event, callback) {
this.addCallback(event, { callback, once: true });
return this;
}
off(event, callback) {
this.removeCallback(event, callback);
return this;
}
emit(event, ...args) {
const callbacks = this._callbacks[event];
if (!callbacks || !callbacks.length) {
return false;
}
for (const { callback, once } of [...callbacks]) {
try {
callback(...args);
}
catch {
// Do nothing
}
if (once) {
this.removeCallback(event, callback);
}
}
return true;
}
addCallback(event, callback) {
const list = this._callbacks[event];
if (list) {
list.push(callback);
}
else {
this._callbacks[event] = [callback];
}
}
removeCallback(event, callback) {
const list = this._callbacks[event];
if (!list) {
return;
}
const index = list.findIndex((el) => el.callback === callback);
if (index === -1) {
return;
}
list.splice(index, 1);
if (!list.length) {
delete this._callbacks[event];
}
}
}
class TimeHelper {
static now() {
return Date.now();
}
static toTimestampInSec(timestampMs) {
return Math.floor(timestampMs / 1000);
}
static convertSecondsToMilliseconds(timeInSec) {
return timeInSec * 1000;
}
}
class BacktraceReport {
data;
attributes;
attachments;
/**
* Report classifiers
*/
classifiers = [];
/**
* Report annotations
*/
annotations = {};
/**
* Report stack trace
*/
stackTrace = {};
/**
* Report message
*/
message;
/**
* Report inner errors
*/
innerReport = [];
/**
* Report timestamp in ms
*/
timestamp = TimeHelper.now();
/**
* Sets how many top frames should be skipped.
*/
skipFrames = 0;
addStackTrace(name, stack, message = '') {
if (typeof stack === 'string') {
this.stackTrace[name] = {
stack,
message,
};
}
else {
this.stackTrace[name] = stack;
}
return this;
}
constructor(data, attributes = {}, attachments = [], options = {}) {
this.data = data;
this.attributes = attributes;
this.attachments = attachments;
this.skipFrames = options?.skipFrames ?? 0;
let errorType = 'Exception';
if (data instanceof Error) {
this.message = this.generateErrorMessage(data.message);
this.annotations['error'] = {
...data,
message: this.message,
name: data.name,
stack: data.stack,
};
this.classifiers = [data.name];
this.stackTrace['main'] = {
stack: data.stack ?? '',
message: this.message,
};
// Supported in ES2022
if (data.cause) {
this.innerReport.push(data.cause);
}
}
else {
this.message = this.generateErrorMessage(data);
this.stackTrace['main'] = {
stack: new Error().stack ?? '',
message: this.message,
};
this.classifiers = ['Message'];
errorType = 'Message';
this.skipFrames += 1;
}
if (!this.attributes['error.type']) {
this.attributes['error.type'] = errorType;
}
this.attributes['error.message'] = this.message;
if (options?.timestamp) {
this.timestamp = options.timestamp;
}
if (options?.classifiers) {
this.classifiers.unshift(...options.classifiers);
}
}
generateErrorMessage(data) {
return typeof data === 'object' ? JSON.stringify(data, jsonEscaper()) : (data?.toString() ?? '');
}
}
class AttachmentManager {
attachmentEvents;
_attachmentProviders = [];
constructor() {
this.attachmentEvents = new Events();
}
/**
* Adds attachment to manager cache.
* @param attachments attachments or attachment returning functions
*/
add(...attachments) {
this.addProviders(...attachments.map((a) => typeof a === 'function'
? {
type: 'dynamic',
get: a,
}
: {
type: 'scoped',
get: () => a,
}));
}
/**
* Adds `BacktraceAttachmentProvider` to manager cache.
* @param attachmentProviders attachment providers
*/
addProviders(...attachmentProviders) {
let anyScoped = false;
for (const provider of attachmentProviders) {
if (provider.type === 'dynamic') {
this._attachmentProviders.push(provider);
}
else {
const attachment = provider.get();
this._attachmentProviders.push({
type: 'scoped',
get: () => attachment,
});
anyScoped = true;
}
}
if (anyScoped) {
this.attachmentEvents.emit('scoped-attachments-updated', this.get('scoped'));
}
}
/**
* Returns scoped, dynamic, or all attachments.
* @param type optional type to filter attachments
* @returns array of `BacktraceAttachment`
*/
get(type) {
const result = [];
for (const provider of this._attachmentProviders) {
if (type && provider.type !== type) {
continue;
}
const attachment = provider.get();
if (!attachment) {
continue;
}
if (Array.isArray(attachment)) {
result.push(...attachment);
}
else {
result.push(attachment);
}
}
return result;
}
}
class ReportDataBuilder {
static build(attributes) {
const result = { annotations: {}, attributes: {} };
if (!attributes) {
return result;
}
for (const attributeKey in attributes) {
const attribute = attributes[attributeKey];
if (attribute == null) {
result.attributes[attributeKey] = attribute;
continue;
}
switch (typeof attribute) {
case 'object': {
result.annotations[attributeKey] = attribute;
break;
}
case 'bigint': {
result.attributes[attributeKey] = attribute.toString();
break;
}
default: {
result.attributes[attributeKey] = attribute;
break;
}
}
}
return result;
}
}
class AttributeManager {
attributeEvents;
_attributeProviders = [];
constructor(providers) {
this.attributeEvents = new Events();
for (const provider of providers) {
this.addProvider(provider);
}
}
/**
* Adds attributes to manager cache
* @param attributes attributes object
*/
add(attributes) {
if (typeof attributes === 'function') {
this.addProvider({ type: 'dynamic', get: attributes });
}
else {
this.addProvider({ type: 'scoped', get: () => attributes });
}
}
/**
* Adds attribute provider to the manager
* @param attributeProvider
* @returns
*/
addProvider(attributeProvider) {
if (attributeProvider.type === 'dynamic') {
this._attributeProviders.push(attributeProvider);
return;
}
else {
const attributes = attributeProvider.get();
this._attributeProviders.push({
type: 'scoped',
get: () => attributes,
});
this.attributeEvents.emit('scoped-attributes-updated', this.get('scoped'));
}
}
/**
* Gets client attributes
* @returns Report attribute - client attributes and annotations
*/
get(attributeType) {
const result = {
annotations: {},
attributes: {},
};
for (const attributeProvider of this._attributeProviders) {
if (attributeType && attributeProvider.type != attributeType) {
continue;
}
const providerResult = ReportDataBuilder.build(attributeProvider.get());
result.attributes = {
...result.attributes,
...providerResult.attributes,
};
result.annotations = {
...result.annotations,
...providerResult.annotations,
};
}
return result;
}
}
class ClientAttributeProvider {
_sdkName;
_sdkVersion;
_sessionId;
constructor(_sdkName, _sdkVersion, _sessionId) {
this._sdkName = _sdkName;
this._sdkVersion = _sdkVersion;
this._sessionId = _sessionId;
}
get type() {
return 'scoped';
}
get() {
return {
'application.session': this._sessionId,
'backtrace.agent': this._sdkName,
'backtrace.version': this._sdkVersion,
};
}
}
class UserAttributeProvider {
type;
_source;
constructor(source) {
this._source = typeof source === 'function' ? source : () => source;
this.type = typeof source === 'function' ? 'dynamic' : 'scoped';
}
get() {
return this._source();
}
}
function stringifiedSize(value) {
return JSON.stringify(value).length;
}
function toStringSize(value) {
return value.toString().length;
}
const stringSize = (value) => stringifiedSize(value);
const numberSize = (toStringSize);
const bigintSize = (toStringSize);
const symbolSize = 0;
const functionSize = 0;
const booleanSize = (value) => (value ? 4 : 5);
const undefinedSize = 0;
const nullSize = 'null'.length;
function arraySize(array, replacer) {
const bracketLength = 2;
const commaLength = array.length - 1;
let elementsLength = 0;
for (let i = 0; i < array.length; i++) {
const element = array[i];
switch (typeof element) {
case 'function':
case 'symbol':
case 'undefined':
elementsLength += nullSize;
break;
default:
elementsLength += _jsonSize(array, i.toString(), element, replacer);
}
}
return bracketLength + commaLength + elementsLength;
}
const objectSize = (obj, replacer) => {
const entries = Object.entries(obj);
const bracketLength = 2;
let entryCount = 0;
let entriesLength = 0;
for (const [k, v] of entries) {
const valueSize = _jsonSize(obj, k, v, replacer);
if (valueSize === 0) {
continue;
}
entryCount++;
// +1 adds the comma size
entriesLength += keySize(k) + valueSize + 1;
}
// -1 removes previously added last comma size (there is no trailing comma)
const commaLength = Math.max(0, entryCount - 1);
return bracketLength + commaLength + entriesLength;
};
function keySize(key) {
const QUOTE_SIZE = 2;
if (key === null) {
return nullSize + QUOTE_SIZE;
}
else if (key === undefined) {
return '"undefined"'.length;
}
switch (typeof key) {
case 'string':
return stringSize(key);
case 'number':
return numberSize(key) + QUOTE_SIZE;
case 'boolean':
return booleanSize(key) + QUOTE_SIZE;
case 'symbol':
return symbolSize; // key not used in JSON
default:
return stringSize(key.toString());
}
}
function _jsonSize(parent, key, value, replacer) {
if (value && typeof value === 'object' && 'toJSON' in value && typeof value.toJSON === 'function') {
value = value.toJSON();
}
value = replacer ? replacer.call(parent, key, value) : value;
if (value === null) {
return nullSize;
}
else if (value === undefined) {
return undefinedSize;
}
if (Array.isArray(value)) {
return arraySize(value, replacer);
}
switch (typeof value) {
case 'bigint':
return bigintSize(value);
case 'boolean':
return booleanSize(value);
case 'function':
return functionSize;
case 'number':
return numberSize(value);
case 'object':
return objectSize(value, replacer);
case 'string':
return stringSize(value);
case 'symbol':
return symbolSize;
case 'undefined':
return undefinedSize;
}
return 0;
}
/**
* Calculates size of the object as it would be serialized into JSON.
*
* _Should_ return the same value as `JSON.stringify(value, replacer).length`.
* This may not be 100% accurate, but should work for our requirements.
* @param value Value to compute length for.
* @param replacer A function that transforms the results as in `JSON.stringify`.
* @returns Final string length.
*/
function jsonSize(value, replacer) {
return _jsonSize(undefined, '', value, replacer);
}
const REMOVED_PLACEHOLDER = '<removed>';
function limitObjectDepth(obj, depth) {
if (!(depth < Infinity)) {
return obj;
}
if (depth < 0) {
return REMOVED_PLACEHOLDER;
}
const limitIfObject = (value) => typeof value === 'object' && value ? limitObjectDepth(value, depth - 1) : value;
const result = {};
for (const key in obj) {
const value = obj[key];
if (Array.isArray(value)) {
result[key] = value.map(limitIfObject);
}
else {
result[key] = limitIfObject(value);
}
}
return result;
}
function textFormatter() {
const defaultFormatter = fallbackFormatter(jsonEscaper());
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const util = require('util');
return util.format ?? defaultFormatter;
}
catch {
return defaultFormatter;
}
}
function fallbackFormatter(jsonEscapeFunction) {
return function fallbackFormatter(...params) {
let result = '';
for (const param of params) {
result += typeof param === 'object' ? JSON.stringify(param, jsonEscapeFunction) : param?.toString();
}
return result;
};
}
exports.BreadcrumbLogLevel = void 0;
(function (BreadcrumbLogLevel) {
BreadcrumbLogLevel[BreadcrumbLogLevel["Verbose"] = 1] = "Verbose";
BreadcrumbLogLevel[BreadcrumbLogLevel["Debug"] = 2] = "Debug";
BreadcrumbLogLevel[BreadcrumbLogLevel["Info"] = 4] = "Info";
BreadcrumbLogLevel[BreadcrumbLogLevel["Warning"] = 8] = "Warning";
BreadcrumbLogLevel[BreadcrumbLogLevel["Error"] = 16] = "Error";
})(exports.BreadcrumbLogLevel || (exports.BreadcrumbLogLevel = {}));
const defaultBreadcrumbsLogLevel = (1 << 5) - 1;
exports.BreadcrumbType = void 0;
(function (BreadcrumbType) {
BreadcrumbType[BreadcrumbType["Manual"] = 1] = "Manual";
BreadcrumbType[BreadcrumbType["Log"] = 2] = "Log";
BreadcrumbType[BreadcrumbType["Navigation"] = 4] = "Navigation";
BreadcrumbType[BreadcrumbType["Http"] = 8] = "Http";
BreadcrumbType[BreadcrumbType["System"] = 16] = "System";
BreadcrumbType[BreadcrumbType["User"] = 32] = "User";
BreadcrumbType[BreadcrumbType["Configuration"] = 64] = "Configuration";
})(exports.BreadcrumbType || (exports.BreadcrumbType = {}));
const defaultBreadcurmbType = (1 << 7) - 1;
class ConsoleEventSubscriber {
/**
* All overriden console events
*/
_events = {};
_formatter;
start(backtraceBreadcrumbs) {
if ((backtraceBreadcrumbs.breadcrumbsType & exports.BreadcrumbType.Log) !== exports.BreadcrumbType.Log) {
return;
}
this._formatter = textFormatter();
this.bindToConsoleMethod('log', exports.BreadcrumbLogLevel.Info, backtraceBreadcrumbs);
this.bindToConsoleMethod('warn', exports.BreadcrumbLogLevel.Warning, backtraceBreadcrumbs);
this.bindToConsoleMethod('error', exports.BreadcrumbLogLevel.Error, backtraceBreadcrumbs);
this.bindToConsoleMethod('debug', exports.BreadcrumbLogLevel.Debug, backtraceBreadcrumbs);
this.bindToConsoleMethod('trace', exports.BreadcrumbLogLevel.Verbose, backtraceBreadcrumbs);
}
dispose() {
for (const key in this._events) {
const consoleMethod = this._events[key];
console[key] = consoleMethod;
}
}
bindToConsoleMethod(name, level, backtraceBreadcrumbs) {
const originalMethod = console[name];
console[name] = (...args) => {
originalMethod(...args);
const message = this._formatter(...args);
backtraceBreadcrumbs.addBreadcrumb(message, level, exports.BreadcrumbType.Log);
};
this._events[name] = originalMethod;
}
}
/**
* Constrains `value` to `min` and `max` values, wrapping not matching values around.
* @param min minimum value to allow
* @param max maximum value to allow
* @returns function accepting `value`
*
* @example
* const wrap = wrapped(10, 20);
* console.log(wrap(15)); // 15
* console.log(wrap(21)); // 10, wrapped around
* console.log(wrap(8)); // 18, wrapped around
*/
function wrapped(min, max) {
function wrapped(value) {
const range = max - min;
let newValue;
if (value < min) {
newValue = max - ((min - value) % range);
if (newValue === max) {
newValue = min;
}
}
else if (value >= max) {
newValue = min + ((value - max) % range);
if (newValue === max) {
newValue = min;
}
}
else {
newValue = value;
}
return newValue;
}
return wrapped;
}
/**
* Constrains `value` to `min` and `max` values.
* @param min minimum value to allow
* @param max maximum value to allow
* @returns function accepting `value`
*
* @example
* const clamp = clamped(10, 20);
* console.log(wrap(15)); // 15
* console.log(wrap(21)); // 20
* console.log(wrap(8)); // 10
*/
function clamped(min, max) {
function clamped(value) {
return Math.max(min, Math.min(value, max));
}
return clamped;
}
class OverwritingArray {
capacity;
_array;
_headConstraint;
_lengthConstraint;
_head = 0;
_length = 0;
get head() {
return this._head;
}
set head(value) {
this._head = this._headConstraint(value);
}
get length() {
return this._length;
}
set length(value) {
this._length = this._lengthConstraint(value);
}
get start() {
return this._headConstraint(this.head - this.length);
}
constructor(capacity, items) {
this.capacity = capacity;
this._array = new Array(capacity);
// Head must be always between 0 and capacity.
// If lower than 0, it needs to go from the end
// If larger than capacity, it needs to go from the start
// Wrapping solves this
this._headConstraint = wrapped(0, capacity);
// Length must be always no less than 0 and no larger than capacity
this._lengthConstraint = clamped(0, capacity);
if (items) {
this.push(...items);
}
}
add(item) {
return this.pushOne(item);
}
push(...items) {
for (const item of items) {
this.pushOne(item);
}
return this.length;
}
pop() {
this.head--;
const element = this._array[this.head];
this._array[this.head] = undefined;
this.length--;
return element;
}
shift() {
const element = this._array[this.start];
this._array[this.start] = undefined;
this.length--;
return element;
}
at(index) {
return this._array[this.index(index)];
}
*values() {
for (let i = 0; i < this.length; i++) {
const index = this.index(i);
yield this._array[index];
}
}
*keys() {
for (let i = 0; i < this.length; i++) {
yield i;
}
}
*entries() {
for (let i = 0; i < this.length; i++) {
const index = this.index(i);
yield [i, this._array[index]];
}
}
[Symbol.iterator]() {
return this.values();
}
pushOne(item) {
this._array[this.head] = item;
this.head++;
this.length++;
}
index(value) {
if (!this.length) {
return this._headConstraint(value);
}
const index = (value % this.length) + this.start;
return this._headConstraint(index);
}
}
class InMemoryBreadcrumbsStorage {
_limits;
get lastBreadcrumbId() {
return this._lastBreadcrumbId;
}
/**
* Breadcrumb name
*/
name = 'bt-breadcrumbs-0';
_lastBreadcrumbId = TimeHelper.toTimestampInSec(TimeHelper.now());
_breadcrumbs;
_breadcrumbSizes;
constructor(_limits) {
this._limits = _limits;
this._breadcrumbs = new OverwritingArray(_limits.maximumBreadcrumbs ?? 100);
this._breadcrumbSizes = new OverwritingArray(this._breadcrumbs.capacity);
}
getAttachments() {
return [this];
}
getAttachmentProviders() {
return [
{
get: () => this,
type: 'scoped',
},
];
}
static factory({ limits }) {
return new InMemoryBreadcrumbsStorage(limits);
}
/**
* Returns breadcrumbs in the JSON format
* @returns Breadcrumbs JSON
*/
get() {
return JSON.stringify([...this._breadcrumbs], jsonEscaper());
}
add(rawBreadcrumb) {
this._lastBreadcrumbId++;
const id = this._lastBreadcrumbId;
const breadcrumb = {
id,
message: rawBreadcrumb.message,
timestamp: TimeHelper.now(),
type: exports.BreadcrumbType[rawBreadcrumb.type].toLowerCase(),
level: exports.BreadcrumbLogLevel[rawBreadcrumb.level].toLowerCase(),
};
if (rawBreadcrumb.attributes) {
breadcrumb.attributes = rawBreadcrumb.attributes;
}
this._breadcrumbs.add(breadcrumb);
if (this._limits.maximumTotalBreadcrumbsSize) {
const size = jsonSize(breadcrumb, jsonEscaper());
this._breadcrumbSizes.add(size);
let totalSize = this.totalSize();
while (totalSize > this._limits.maximumTotalBreadcrumbsSize) {
this._breadcrumbs.shift();
const removedSize = this._breadcrumbSizes.shift() ?? 0;
// We subtract removedSize plus comma in JSON
totalSize -= removedSize + 1;
}
}
return id;
}
totalSize() {
let sum = 0;
for (const size of this._breadcrumbSizes) {
sum += size;
}
// Sum of:
// - all breadcrumbs
// - comma count
// - brackets
return sum + Math.max(0, this._breadcrumbSizes.length - 1) + 2;
}
}
const BREADCRUMB_ATTRIBUTE_NAME = 'breadcrumbs.lastId';
/**
* @returns `undefined` if value is `false`, else `value` if defined, else `defaultValue`
*/
const defaultIfNotFalse = (value, defaultValue) => {
return value === false ? undefined : value !== undefined ? value : defaultValue;
};
class BreadcrumbsManager {
/**
* Breadcrumbs type
*/
breadcrumbsType;
/**
* Breadcrumbs Log level
*/
logLevel;
/**
* Determines if the breadcrumb manager is enabled.
*/
_enabled = false;
_limits;
_eventSubscribers = [new ConsoleEventSubscriber()];
_interceptor;
_storage;
constructor(configuration, options) {
this._limits = {
maximumBreadcrumbs: defaultIfNotFalse(configuration?.maximumBreadcrumbs, 100),
maximumAttributesDepth: defaultIfNotFalse(configuration?.maximumAttributesDepth, 2),
maximumBreadcrumbMessageLength: defaultIfNotFalse(configuration?.maximumBreadcrumbMessageLength, 255),
maximumBreadcrumbSize: defaultIfNotFalse(configuration?.maximumBreadcrumbSize, 64 * 1024),
maximumTotalBreadcrumbsSize: defaultIfNotFalse(configuration?.maximumTotalBreadcrumbsSize, 1024 * 1024),
};
this.breadcrumbsType = configuration?.eventType ?? defaultBreadcurmbType;
this.logLevel = configuration?.logLevel ?? defaultBreadcrumbsLogLevel;
this._storage = (options?.storage ?? InMemoryBreadcrumbsStorage.factory)({ limits: this._limits });
this._interceptor = configuration?.intercept;
if (options?.subscribers) {
this._eventSubscribers.push(...options.subscribers);
}
}
addEventSubscriber(subscriber) {
if (this._enabled) {
subscriber.start(this);
}
this._eventSubscribers.push(subscriber);
}
setStorage(storage) {
if (typeof storage === 'function') {
this._storage = storage({ limits: this._limits });
}
else {
this._storage = storage;
}
}
dispose() {
this._enabled = false;
for (const subscriber of this._eventSubscribers) {
subscriber.dispose();
}
}
bind({ client, attachmentManager }) {
if (this._storage.getAttachmentProviders) {
attachmentManager.addProviders(...this._storage.getAttachmentProviders());
}
else {
attachmentManager.add(...this._storage.getAttachments());
}
client.addAttribute(() => ({
[BREADCRUMB_ATTRIBUTE_NAME]: this._storage.lastBreadcrumbId,
}));
client.on('before-skip', (report) => this.logReport(report));
}
initialize() {
if (this._enabled) {
return;
}
for (const subscriber of this._eventSubscribers) {
subscriber.start(this);
}
this._enabled = true;
}
verbose(message, attributes) {
return this.log(message, exports.BreadcrumbLogLevel.Verbose, attributes);
}
debug(message, attributes) {
return this.log(message, exports.BreadcrumbLogLevel.Debug, attributes);
}
info(message, attributes) {
return this.log(message, exports.BreadcrumbLogLevel.Info, attributes);
}
warn(message, attributes) {
return this.log(message, exports.BreadcrumbLogLevel.Warning, attributes);
}
error(message, attributes) {
return this.log(message, exports.BreadcrumbLogLevel.Error, attributes);
}
log(message, level, attributes) {
return this.addBreadcrumb(message, level, exports.BreadcrumbType.Manual, attributes);
}
logReport(report) {
const level = report.data instanceof Error ? exports.BreadcrumbLogLevel.Error : exports.BreadcrumbLogLevel.Warning;
return this.addBreadcrumb(report.message, level, exports.BreadcrumbType.System);
}
addBreadcrumb(message, level, type, attributes) {
if (!this._enabled) {
return false;
}
let rawBreadcrumb = {
message: this.prepareBreadcrumbMessage(message),
level,
type,
attributes,
};
if (this._interceptor) {
const interceptorBreadcrumb = this._interceptor(rawBreadcrumb);
if (!interceptorBreadcrumb) {
return false;
}
rawBreadcrumb = interceptorBreadcrumb;
}
if ((this.logLevel & rawBreadcrumb.level) !== level) {
return false;
}
if ((this.breadcrumbsType & rawBreadcrumb.type) !== type) {
return false;
}
if (this._limits.maximumBreadcrumbMessageLength !== undefined) {
rawBreadcrumb.message = rawBreadcrumb.message.substring(0, this._limits.maximumBreadcrumbMessageLength);
}
let limitedBreadcrumb;
if (this._limits.maximumAttributesDepth !== undefined && rawBreadcrumb.attributes) {
limitedBreadcrumb = {
...rawBreadcrumb,
attributes: limitObjectDepth(rawBreadcrumb.attributes, this._limits.maximumAttributesDepth),
};
}
else {
limitedBreadcrumb = rawBreadcrumb;
}
if (this._limits.maximumBreadcrumbSize !== undefined) {
const breadcrumbSize = jsonSize(limitedBreadcrumb, jsonEscaper());
if (breadcrumbSize > this._limits.maximumBreadcrumbSize) {
// TODO: Trim the breadcrumb
return false;
}
}
const id = this._storage.add(limitedBreadcrumb);
return id !== undefined;
}
/**
* The expectation is, message should always be defined and passed as string.
* However, logger can pass as a message an object or any other unknown type.
* To be sure the code won't break, this method ensures the message is always a string
* no matter what the logger gives us.
* @param message breadcrumb message
*/
prepareBreadcrumbMessage(message) {
if (message == null) {
return '';
}
const messageType = typeof message;
switch (messageType) {
case 'string': {
return message;
}
case 'object': {
return JSON.stringify(message, jsonEscaper());
}
default: {
return message.toString();
}
}
}
}
const UNKNOWN_FRAME = 'unknown';
const ANONYMOUS_FUNCTION = 'anonymous';
class V8StackTraceConverter {
addressSeparator;
get engine() {
return 'v8';
}
constructor(addressSeparator = '') {
this.addressSeparator = addressSeparator;
}
convert(stackTrace, message) {
const result = [];
let stackFrames = stackTrace.split('\n');
const errorHeader = message.split('\n');
// remove error header from stack trace - if the error header exists
if (stackFrames[0].indexOf(errorHeader[0]) !== -1) {
stackFrames = stackFrames.slice(errorHeader.length);
}
else {
stackFrames = stackFrames.slice(1);
}
for (const stackFrame of stackFrames) {
const normalizedStackFrame = stackFrame.trim();
if (!normalizedStackFrame) {
continue;
}
const frame = this.parseFrame(normalizedStackFrame);
result.push(frame);
}
return result;
}
parseFrame(stackFrame) {
const frameSeparator = 'at ';
if (!stackFrame.startsWith(frameSeparator)) {
return {
funcName: stackFrame,
library: UNKNOWN_FRAME,
};
}
stackFrame = stackFrame.substring(stackFrame.indexOf(frameSeparator) + frameSeparator.length);
const asyncKeyword = 'async ';
const sourceCodeSeparator = ' (';
let sourceCodeStartIndex = stackFrame.indexOf(sourceCodeSeparator);
const anonymousFunction = sourceCodeStartIndex === -1;
if (anonymousFunction) {
if (stackFrame.startsWith(asyncKeyword)) {
stackFrame = stackFrame.substring(asyncKeyword.length);
}
return {
funcName: ANONYMOUS_FUNCTION,
...this.parseSourceCodeInformation(stackFrame),
};
}
let sourceCodeInformation = stackFrame.substring(sourceCodeStartIndex + sourceCodeSeparator.length - 1, stackFrame.length);
const anonymousGenericSymbol = '(<anonymous>)';
if (sourceCodeInformation.startsWith(anonymousGenericSymbol)) {
sourceCodeStartIndex += anonymousGenericSymbol.length + 1;
sourceCodeInformation = sourceCodeInformation.substring(anonymousGenericSymbol.length);
}
if (sourceCodeInformation.startsWith(` ${frameSeparator}`)) {
sourceCodeInformation = sourceCodeInformation.substring(frameSeparator.length + 1);
}
else {
sourceCodeInformation = sourceCodeInformation.substring(1, sourceCodeInformation.length - 1);
}
let functionName = stackFrame.substring(0, sourceCodeStartIndex);
if (functionName.startsWith(asyncKeyword)) {
functionName = functionName.substring(asyncKeyword.length);
}
return {
funcName: functionName,
...this.parseSourceCodeInformation(sourceCodeInformation),
};
}
parseSourceCodeInformation(sourceCodeInformation) {
if (sourceCodeInformation.startsWith('eval')) {
return this.extractEvalInformation(sourceCodeInformation);
}
if (this.addressSeparator && sourceCodeInformation.startsWith(this.addressSeparator)) {
sourceCodeInformation = sourceCodeInformation.substring(this.addressSeparator.length).trimStart();
}
const sourceCodeParts = sourceCodeInformation.split(':');
const column = parseInt(sourceCodeParts[sourceCodeParts.length - 1]);
const lineNumber = parseInt(sourceCodeParts[sourceCodeParts.length - 2]);
const library = sourceCodeParts.slice(0, sourceCodeParts.length - 2).join(':');
return {
library,
column: isNaN(column) ? undefined : column,
line: isNaN(lineNumber) ? undefined : lineNumber,
};
}
extractEvalInformation(evalSourceCodeInformation) {
const sourceCodeStartSeparatorChar = '(';
const sourceCodeEndSeparatorChar = ')';
const sourceCodeStart = evalSourceCodeInformation.indexOf(sourceCodeStartSeparatorChar);
const sourceCodeEnd = evalSourceCodeInformation.indexOf(sourceCodeEndSeparatorChar);
if (sourceCodeStart === -1 || sourceCodeEnd === -1 || sourceCodeStart > sourceCodeEnd) {
return {
library: UNKNOWN_FRAME,
};
}
const sourceCodeInformation = evalSourceCodeInformation.substring(sourceCodeStart + sourceCodeStartSeparatorChar.length, sourceCodeEnd);
return this.parseSourceCodeInformation(sourceCodeInformation);
}
}
class IdGenerator {
static uuid() {
const bytes = [...new Array(16)].map(() => Math.floor(Math.random() * 256));
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
return (bytes
.slice(0, 4)
.map((n) => n.toString(16).padStart(2, '0'))
.join('') +
'-' +
bytes
.slice(4, 6)
.map((n) => n.toString(16).padStart(2, '0'))
.join('') +
'-' +
bytes
.slice(6, 8)
.map((n) => n.toString(16).padStart(2, '0'))
.join('') +
'-' +
bytes
.slice(8, 10)
.map((n) => n.toString(16).padStart(2, '0'))
.join('') +
'-' +
bytes
.slice(10, 16)
.map((n) => n.toString(16).padStart(2, '0'))
.join(''));
}
}
class BacktraceDataBuilder {
_sdkOptions;
_stackTraceConverter;
_attributeManager;
_debugIdProvider;
MAIN_THREAD_NAME = 'main';
constructor(_sdkOptions, _stackTraceConverter, _attributeManager, _debugIdProvider) {
this._sdkOptions = _sdkOptions;
this._stackTraceConverter = _stackTraceConverter;
this._attributeManager = _attributeManager;
this._debugIdProvider = _debugIdProvider;
}
build(report) {
const { annotations, attributes } = this._attributeManager.get();
const reportData = ReportDataBuilder.build(report.attributes);
const { threads, detectedDebugIdentifier } = this.getThreads(report);
const result = {
uuid: IdGenerator.uuid(),
timestamp: TimeHelper.toTimestampInSec(report.timestamp),
agent: this._sdkOptions.agent,
agentVersion: this._sdkOptions.agentVersion,
lang: this._sdkOptions.langName,
langVersion: this._sdkOptions.langVersion,
classifiers: report.classifiers,
mainThread: this.MAIN_THREAD_NAME,
threads,
annotations: {
...annotations,
...reportData.annotations,
...report.annotations,
},
attributes: {
...attributes,
...reportData.attributes,
},
};
if (detectedDebugIdentifier) {
result.symbolication = 'sourcemap';
}
return result;
}