UNPKG

@backtrace/sdk-core

Version:
3,439 lines • 117 kB
'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;
    }
    getThreads(report) {
        const threads = {};
        let detectedDebugIdentifier = false;
        for (const [name, traceInfo] of Object.entries(report.stackTrace)) {
            let stackFrames;
            if (Array.isArray(traceInfo)) {
                stackFrames = traceInfo;
            }
            else {
                const { message, stack } = traceInfo;
                stackFrames = this._stackTraceConverter.convert(stack, message);
            }
            if (name === this.MAIN_THREAD_NAME && report.skipFrames > 0) {
                stackFrames.splice(0, report.skipFrames);
            }
            for (const frame of stackFrames) {
                const debugIdentifier = this._debugIdProvider.getDebugId(frame.library);
                if (!debugIdentifier) {
                    continue;
                }
                detectedDebugIdentifier = true;
                frame.debug_identifier = debugIdentifier;
            }
            threads[name] = {
                fault: name === this.MAIN_THREAD_NAME,
                name,
                stack: stackFrames,
            };
        }
        return {
            threads,
            detectedDebugIdentifier,
        };
    }
}

class AbortError extends Error {
    constructor(message) {
        super(message ?? 'Operation cancelled.');
    }
}

// Required for polyfills to have the same name as the original implementations
function getGlobal(key) {
    return typeof window !== 'undefined'
        ? window[key]
        : typeof global !== 'undefined'
            ? global[key]
            : typeof self !== 'undefined'
                ? self[key]
                : undefined;
}
const OriginalAbortController = getGlobal('AbortController');

/**
 * Copied and repurposed from https://github.com/mo/abortcontroller-polyfill/blob/master/src/abortcontroller.js
 */
class Emitter {
    _listeners = {};
    _events = new Events();
    addEventListener(type, listener, options) {
        if (!(type in this._listeners)) {
            this._listeners[type] = [];
        }
        const callback = (ev) => {
            if (typeof listener === 'object') {
                return listener.handleEvent.call(this, ev);
            }
            else {
                return listener.call(this, ev);
            }
        };
        const { once, signal } = (typeof options === 'object' ? options : {});
        if (once) {
            this._events.once(type, callback);
        }
        else {
            this._events.on(type, callback);
        }
        if (signal) {
            const removeFn = () => this.removeEventListener(type, listener);
            signal.addEventListener(type, removeFn, { once: true });
        }
        this._listeners[type].push({ callback, listener, options });
    }
    removeEventListener(type, listener) {
        if (!(type in this._listeners)) {
            return;
        }
        const allListeners = this._listeners[type];
        const listeners = allListeners.filter((l) => l.listener === listener);
        for (const elem of listeners) {
            this._events.off(type, elem.callback);
            const index = allListeners.indexOf(elem);
            allListeners.splice(index, 1);
        }
    }
    dispatchEvent(event) {
        this._events.emit(event.type, event);
        return !event.defaultPrevented;
    }
}
/**
 * Copied and repurposed from https://github.com/mo/abortcontroller-polyfill/blob/master/src/abortcontroller.js
 */
class AbortSignal extends Emitter {
    aborted = false;
    onabort = null;
    reason;
    constructor() {
        super();
        // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and
        // we want Object.keys(new AbortController().signal) to be [] for compat with the native impl
        Object.defineProperty(this, 'aborted', { writable: true, configurable: true, enumerable: false });
        Object.defineProperty(this, 'onabort', { writable: true, configurable: true, enumerable: false });
        Object.defineProperty(this, 'reason', { writable: true, configurable: true, enumerable: false });
    }
    toString() {
        return '[object AbortSignal]';
    }
    throwIfAborted() {
        if (this.aborted) {
            throw this.reason;
        }
    }
    dispatchEvent(event) {
        if (event.type === 'abort') {
            this.aborted = true;
            if (typeof this.onabort === 'function') {
                this.onabort.call(this, event);
            }
        }
        return super.dispatchEvent(event);
    }
    any(signals) {
        return anySignal(...signals);
    }
}
/**
 * Copied and repurposed from https://github.com/mo/abortcontroller-polyfill/blob/master/src/abortcontroller.js
 */
class AbortController {
    signal;
    constructor() {
        // Compared to assignment, Object.defineProperty makes properties non-enumerable by default and
        // we want Object.keys(new AbortController()) to be [] for compat with the native impl
        this.signal = new AbortSignal();
        Object.defineProperty(this, 'signal', { configurable: true, enumerable: false });
    }
    abort(reason) {
        let event;
        try {
            event = new Event('abort');
        }
        catch (e) {
            if (typeof document !== 'undefined') {
                const ie8Document = document;
                if (!ie8Document.createEvent && ie8Document.createEventObject) {
                    // For Internet Explorer 8:
                    event = ie8Document.createEventObject();
                    event.type = 'abort';
                }
                else {
                    // For Internet Explorer 11:
                    event = document.createEvent('Event');
                    event.initEvent('abort', false, false);
                }
            }
            else {
                // Fallback where document isn't available:
                event = {
                    type: 'abort',
                    bubbles: false,
                    cancelable: false,
                };
            }
        }
        let signalReason = reason;
        if (signalReason === undefined) {
            if (typeof document === 'undefined') {
                signalReason = new AbortError('This operation was aborted');
            }
            else {
                try {
                    signalReason = new DOMException('signal is aborted without reason');
                }
                catch (err) {
                    // IE 11 does not support calling the DOMException constructor, use a
                    // regular error object on it instead.
                    signalReason = new AbortError('This operation was aborted');
                }
            }
        }
        this.signal.reason = signalReason;
        this.signal.dispatchEvent(event);
    }
    toString() {
        return '[object AbortController]';
    }
}
/**
 * Copied and repurposed from https://github.com/mo/abortcontroller-polyfill/blob/master/src/abortcontroller.js
 */
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    // These are necessary to make sure that we get correct output for:
    // Object.prototype.toString.call(new AbortController())
    AbortController.prototype[Symbol.toStringTag] = 'AbortController';
    AbortSignal.prototype[Symbol.toStringTag] = 'AbortSignal';
}
/**
 * Creates a new abort controller.
 *
 * If the AbortController is not available, a polyfill is used.
 * @returns
 */
function createAbortController() {
    if (OriginalAbortController) {
        return new OriginalAbortController();
    }
    else {
        return new AbortController();
    }
}
function anySignal(...signals) {
    const controller = createAbortController();
    function cleanup() {
        // Cleanup
        for (const signal of signals) {
            if (signal) {
                signal.removeEventListener('abort', onAbort);
            }
        }
    }
    function onAbort() {
        controller.abort();
        cleanup();
    }
    for (const signal of signals) {
        if (!signal) {
            continue;
        }
        if (signal.aborted) {
            onAbort();
            break;
        }
        signal.addEventListener('abort', onAbort);
    }
    controller.signal.dispose = cleanup;
    return controller.signal;
}

/**
 * Due to the event loop, NodeJS application might not exit
 * due to some background work that the SDK delegates to setIntervals.
 * Thanks to this helper we can cancell interval in NodeJS process.
 * @param interval setInterval interval
 */
function unrefInterval(interval) {
    if (!interval || typeof interval === 'number') {
        return;
    }
    interval.unref();
}

class BacktraceDatabaseContext {
    bucketCount;
    _records;
    _recordBuckets = {};
    constructor(bucketCount = 3) {
        this.bucketCount = bucketCount;
        this._records = [];
        this._recordBuckets = {};
    }
    find(predicate) {
        return this._records.find(predicate);
    }
    add(record) {
        if (this._recordBuckets[record.id] !== undefined) {
            this._recordBuckets[record.id] = 0;
            return;
        }
        this._records.push(record);
        this._recordBuckets[record.id] = 0;
    }
    get() {
        return this._records.map((r) => ({ ...r }));
    }
    getBucket(index) {
        const bucket = [];
        for (const record of this._records) {
            if (this._recordBuckets[record.id] === index) {
                bucket.push(record);
            }
        }
        return bucket;
    }
    count() {
        return this._records.length;
    }
    countByType() {
        return this._records.reduce((total, current) => {
            total[current.type]++;
            return total;
        }, {});
    }
    remove(recordOrId) {
        const id = typeof recordOrId === 'string' ? recordOrId : recordOrId.id;
        const index = this._records.findIndex((r) => r.id === id);
        if (index === -1) {
            return;
        }
        this._records.splice(index, 1);
        delete this._recordBuckets[id];
    }
    increaseBucket(bucketStart) {
        for (const [id, bucket] of Object.entries(this._recordBuckets)) {
            if (bucket < bucketStart) {
                continue;
            }
            const newBucket = bucket + 1;
            if (newBucket >= this.bucketCount) {
                this.remove(id);
            }
            this._recordBuckets[id] = newBucket;
        }
    }
    load(records) {
        for (const record of records) {
            this.add(record);
        }
    }
    dropOverLimits(limits) {
        const remaining = { ...limits };
        const dropped = [];
        for (let i = this._records.length - 1; i >= 0; i--) {
            const record = this._records[i];
            if (remaining[record.type] === 0) {
                this.remove(record);
                dropped.push(record);
            }
            else {
                remaining[record.type]--;
            }
        }
        return dropped;
    }
}

class BacktraceDatabase extends Events {
    _options;
    _storageProvider;
    _requestHandler;
    _sessionFiles;
    /**
     * Determines if the database is enabled.
     */
    get enabled() {
        return this._enabled;
    }
    /**
     * Abort controller to cancel asynchronous database operations when
     * the database is being disabled by the user.
     */
    _abortController = createAbortController();
    _databaseRecordContext;
    _storageProviders = [];
    _recordLimits;
    _retryInterval;
    _intervalId;
    _enabled = false;
    constructor(_options, _storageProvider, _requestHandler, _sessionFiles) {
        super();
        this._options = _options;
        this._storageProvider = _storageProvider;
        this._requestHandler = _requestHandler;
        this._sessionFiles = _sessionFiles;
        this._databaseRecordContext = new BacktraceDatabaseContext(this._options?.maximumRetries);
        this._recordLimits = {
            report: this._options?.maximumNumberOfRecords ?? 8,
            attachment: this._options?.maximumNumberOfAttachmentRecords ?? 10,
        };
        this._retryInterval = this._options?.retryInterval ?? 60_000;
    }
    /**
     * Starts database integration.
     * @returns true if the database started successfully. Otherwise false.
     */
    initialize() {
        if (this._enabled) {
            return this._enabled;
        }
        if (this._options?.enable === false) {
            return false;
        }
        const startResult = this._storageProvider.start();
        if (!startResult) {
            return false;
        }
        const lockId = this._sessionFiles?.lockPreviousSessions();
        this.loadReports()
            .then(() => {
            this.setupDatabaseAutoSend();
        })
            .finally(() => lockId && this._sessionFiles?.unlockPreviousSessions(lockId));
        this._enabled = true;
        return true;
    }
    bind({ client }) {
        if (this._enabled) {
            return;
        }
        if (this._options?.enable === false) {
            return;
        }
        client.on('before-send', (_, data, attachments) => {
            const record = this.add(data, attachments);
            if (!record || record.locked) {
                return undefined;
            }
            record.locked = true;
        });
        client.on('after-send', (_, data, __, submissionResult) => {
            const record = this._databaseRecordContext.find((record) => record.type === 'report' && record.data.uuid === data.uuid);
            if (!record) {
                return;
            }
            record.locked = false;
            if (submissionResult.status === 'Ok') {
                this.remove(record);
                this._sessionFiles?.unlockPreviousSessions(record.id);
            }
        });
    }
    /**
     * Adds backtrace data to the database
     * @param backtraceData diagnostic data object
     * @param attachments attachments
     * @returns record if database is enabled. Otherwise undefined.
     */
    add(backtraceData, attachments) {
        if (!this._enabled) {
            return undefined;
        }
        const sessionId = backtraceData.attributes?.['application.session'];
        const record = {
            type: 'report',
            data: backtraceData,
            timestamp: TimeHelper.now(),
            id: IdGenerator.uuid(),
            locked: false,
            attachments: attachments,
            sessionId: typeof sessionId === 'string' ? sessionId : undefined,
        };
        this.prepareDatabase([record]);
        const saveResult = this._storageProvider.add(record);
        if (!saveResult) {
            return undefined;
        }
        this._databaseRecordContext.add(record);
        this.lockSessionWithRecord(record);
        this.emit('added', record);
        return record;
    }
    /**
     * Adds Bactrace attachment to the database
     * @param backtraceData diagnostic data object
     * @param attachment the attachment to add
     * @param sessionId session ID to use
     * @returns record if database is enabled. Otherwise undefined.
     */
    addAttachment(rxid, attachment, sessionId) {
        if (!this._enabled) {
            return undefined;
        }
        const record = {
            type: 'attachment',
            timestamp: TimeHelper.now(),
            id: IdGenerator.uuid(),
            rxid,
            locked: false,
            attachment,
            sessionId,
        };
        this.prepareDatabase([record]);
        const saveResult = this._storageProvider.add(record);
        if (!saveResult) {
            return undefined;
        }
        this._databaseRecordContext.add(record);
        this.lockSessionWithRecord(record);
        this.emit('added', record);
        return record;
    }
    /**
     * Returns stored references to Backtrace records
     * @returns available records in the database
     */
    get() {
        return this._databaseRecordContext.get();
    }
    /**
     * @returns Returns number of records stored in the Database
     */
    count() {
        return this._databaseRecordContext.count();
    }
    /**
     * @returns Returns number of records by type stored in the Database
     */
    countByType() {
        return this._databaseRecordContext.countByType();
    }
    /**
     * Disables database integration. After running the dispose method, you cannot
     * execute any database operations.
     */
    dispose() {
        this._enabled = false;
        clearInterval(this._intervalId);
        this._abortController.abort();
    }
    /**
     * Removes the database record/records
     * @param recordOrRecords database records
     */
    remove(recordOrRecords) {
        if (!this._enabled) {
            return;
        }
        const records = Array.isArray(recordOrRecords) ? recordOrRecords : [recordOrRecords];
        for (const record of records) {
            this._databaseRecordContext.remove(record);
            this._storageProvider.delete(record);
            this._sessionFiles?.unlockPreviousSessions(record.id);
            this.emit('removed', record);
        }
    }
    addStorageProvider(storageProvider) {
        if (this._enabled) {
            throw new Error('Cannot add storage provider after the database has been enabled.');
        }
        this._storageProviders.push(storageProvider);
    }
    /**
     * Sends all records available in the database to Backtrace and removes them
     * no matter if the submission process was successful or not.
     * @param abortSignal optional abort signal to cancel sending requests
     */
    async flush(abortSignal) {
        const start = TimeHelper.now();
        await this.send(abortSignal);
        const records = this.get().filter((n) => n.timestamp <= start);
        for (const record of records) {
            if (abortSignal?.aborted) {
                return;
            }
            this.remove(record);
        }
    }
    /**
     * Sends all records available in the database to Backtrace.
     * @param abortSignal optional abort signal to cancel sending requests
     */
    async send(abortSignal) {
        for (let bucketIndex = 0; bucketIndex < this._databaseRecordContext.bucketCount; bucketIndex++) {
            // make a copy of records to not update the array after each remove
            const records = [...this._databaseRecordContext.getBucket(bucketIndex)];
            const signal = anySignal(abortSignal, this._abortController.signal);
            try {
                for (const record of records) {
                    if (!this.enabled) {
                        return;
                    }
                    if (record.locked) {
                        continue;
                    }
                    try {
                        record.locked = true;
                        this.emit('before-send', record);
                        const result = record.type === 'report'
                            ? await this._requestHandler.send(record.data, record.attachments, signal)
                            : await this._requestHandler.sendAttachment(record.rxid, record.attachment, signal);
                        this.emit('after-send', record, result);
                        if (result.status === 'Ok' ||
                            result.status === 'Unsupported' ||
                            result.status === 'Report skipped') {
                            this.remove(record);
                            continue;
                        }
                        this._databaseRecordContext.increaseBucket(bucketIndex);
                        return;
                    }
                    finally {
                        record.locked = false;
                    }
                }
            }
            finally {
                signal.dispose();
            }
        }
    }
    /**
     * Prepare database to insert records
     * @param forRecords records for which to update the database
     */
    prepareDatabase(forRecords) {
        const dropLimits = { ...this._recordLimits };
        for (const record of forRecords) {
            dropLimits[record.type]--;
        }
        const dropped = this._databaseRecordContext.dropOverLimits(dropLimits);
        this.remove(dropped);
    }
    async loadReports() {
        const records = await this._storageProvider.get();
        // limit only non-attachment records
        const countByType = {
            attachment: 0,
            report: 0,
        };
        const recordsToAdd = [];
        const recordsToRemove = [];
        for (const record of records) {
            if (countByType[record.type] >= this._recordLimits[record.type]) {
                recordsToRemove.push(record);
            }
            else {
                recordsToAdd.push(record);
                countByType[record.type]++;
            }
        }
        // delete old records before adding them to the database
        if (recordsToRemove.length) {
            this.remove(recordsToRemove);
        }
        this.prepareDatabase(recordsToAdd);
        this._databaseRecordContext.load(recordsToAdd);
        for (const record of recordsToAdd) {
            this.lockSessionWithRecord(record);
            this.emit('added', record);
        }
    }
    async setupDatabaseAutoSend() {
        if (!this._enabled) {
            return;
        }
        if (this._options?.autoSend === false) {
            return;
        }
        const sendDatabaseReports = async () => {
            await this.send();
        };
        this._intervalId = setInterval(sendDatabaseReports, this._retryInterval);
        unrefInterval(this._intervalId);
        await this.send();
    }
    lockSessionWithRecord(record) {
        if (!this._sessionFiles) {
            return;
        }
        const sessionId = record.sessionId;
        if (typeof sessionId !== 'string') {
            this._sessionFiles.lockPreviousSessions(record.id);
            return;
        }
        const session = this._sessionFiles.getSessionWithId(sessionId);
        session?.lock(record.id);
    }
}

function isFileAttachment(attachment) {
    return 'filePath' in attachment && typeof attachment.filePath === 'string';
}

class AttachmentBacktraceDatabaseFileRecord {
    type = 'attachment';
    id;
    rxid;
    timestamp;
    attachment;
    sessionId;
    locked;
    constructor(record) {
        this.attachment = record.attachment;
        this.id = record.id;
        this.timestamp = record.timestamp;
        this.rxid = record.rxid;
        this.sessionId = record.sessionId;
        // make sure the database record stored in the database directory
        // is never locked. By doing this, we want to be sure once we load
        // the record once again, the record will be available for future usage
        this.locked = false;
    }
    static fromRecord(record) {
        return new AttachmentBacktraceDatabaseFileRecord(record);
    }
    static fromJson(json, fileSystem) {
        try {
            const record = typeof json === 'string' ? JSON.parse(json) : json;
            if (!isFileAttachment(record.attachment)) {
                return undefined;
            }
            if (!fileSystem.existsSync(record.attachment.filePath)) {
                return undefined;
            }
            const attachment = fileSystem.createAttachment(record.attachment.filePath, record.attachment.name);
            return new AttachmentBacktraceDatabaseFileRecord({
                ...record,
                attachment,
            });
        }
        catch {
            return undefined;
        }
    }
}

class ReportBacktraceDatabaseFileRecord {
    attachments;
    type = 'report';
    data;
    id;
    timestamp;
    sessionId;
    locked;
    constructor(record, attachments) {
        this.attachments = attachments;
        this.data = record.data;
        this.id = record.id;
        this.timestamp = record.timestamp;
        this.sessionId = record.sessionId;
        // make sure the database record stored in the database directory
        // is never locked. By doing this, we want to be sure once we load
        // the record once again, the record will be available for future usage
        this.locked = false;
    }
    static fromRecord(record) {
        return new ReportBacktraceDatabaseFileRecord(record, record.attachments.filter(isFileAttachment));
    }
    static fromJson(json, fileSystem) {
        try {
            const record = typeof json === 'string' ? JSON.parse(json) : json;
            const attachments = record.attachments
                ? record.attachments
                    .filter(isFileAttachment)
                    .map((n) => fileSystem.createAttachment(n.filePath, n.name))
                : [];
            return new ReportBacktraceDatabaseFileRecord(record, attachments);
        }
        catch {
            return undefined;
        }
    }
}

const BacktraceDatabaseFileRecord = {
    fromRecord(record) {
        switch (record.type) {
            case 'attachment':
                return AttachmentBacktraceDatabaseFileRecord.fromRecord(record);
            case 'report':
                return ReportBacktraceDatabaseFileRecord.fromRecord(record);
            default:
                throw new Error('unknown record type');
        }
    },
    fromJson(json, fileSystem) {
        try {
            const record = JSON.parse(json);
            switch (record.type) {
                case 'attachment':
                    return AttachmentBacktraceDatabaseFileRecord.fromJson(record, fileSystem);
                case 'report':
                    return ReportBacktraceDatabaseFileRecord.fromJson(record, fileSystem);
                default:
                    return undefined;
            }
        }
        catch {
            return undefined;
        }
    },
};

class BacktraceDatabaseFileStorageProvider {
    _fileSystem;
    _path;
    _enabled = true;
    RECORD_SUFFIX = '-record.json';
    constructor(_fileSystem, _path) {
        this._fileSystem = _fileSystem;
        this._path = _path;
    }
    /**
     * Create a provider if provided options are valid
     * @param options database configuration
     * @returns database file storage provider
     */
    static createIfValid(fileSystem, options) {
        if (!options) {
            return undefined;
        }
        if (!options.enable) {
            return undefined;
        }
        if (options.enable && !options.path) {
            throw new Error('Missing mandatory path to the database. Please define the database.path option in the configuration.');
        }
        return new BacktraceDatabaseFileStorageProvider(fileSystem, options.path);
    }
    start() {
        // make sure by mistake we don't create anything or start any operation
        if (this._enabled === false) {
            return false;
        }
        return true;
    }
    delete(record) {
        const recordPath = this.getRecordPath(record.id);
        return this.unlinkRecord(recordPath);
    }
    add(record) {
        const recordPath = this.getRecordPath(record.id);
        try {
            this._fileSystem.writeFileSync(recordPath, JSON.stringify(BacktraceDatabaseFileRecord.fromRecord(record), jsonEscaper()));
            return true;
        }
        catch {
            return false;
        }
    }
    async get() {
        const databaseFiles = await this._fileSystem.readDir(this._path);
        const recordNames = databaseFiles
            .filter((file) => file.endsWith(this.RECORD_SUFFIX))
            .map((f) => this._path + '/' + f);
        const records = [];
        for (const recordName of recordNames) {
            try {
                const recordJson = await this._fileSystem.readFile(recordName);
                const record = BacktraceDatabaseFileRecord.fromJson(recordJson, this._fileSystem);
                if (!record) {
                    await this._fileSystem.unlink(recordName);
                    continue;
                }
                records.push(record);
            }
            catch {
                await this._fileSystem.unlink(recordName);
            }
        }
        return records;
    }
    unlinkRecord(recordPath) {
        if (!this._fileSystem.existsSync(recordPath)) {
            return false;
        }
        try {
            this._fileSystem.unlinkSync(recordPath);
            return true;
        }
        catch {
            return false;
        }
    }
    getRecordPath(id) {
        return this._path + '/' + `${id}${this.RECORD_SUFFIX}`;
    }
}

class MetricsEvent {
    metricGroupName;
    metricGroupValue;
    attributes;
    id = IdGenerator.uuid();
    constructor(metricGroupName, metricGroupValue, attributes = {}) {
        this.metricGroupName = metricGroupName;
        this.metricGroupValue = metricGroupValue;
        this.attributes = attributes;
    }
    toJSON() {
        return {
            timestamp: TimeHelper.toTimestampInSec(TimeHelper.now()),
            attributes: this.attributes,
            [this.metricGroupName]: this.metricGroupValue,
        };
    }
}

class SummedEvent extends MetricsEvent {
    constructor(metricGroupName, attributes = {}) {
        super('metric_group', metricGroupName, attributes);
    }
}

class UniqueEvent extends MetricsEvent {
    constructor(attributes) {
        super('unique', ['guid'], attributes);
    }
}

class BacktraceMetrics {
    _options;
    _sessionProvider;
    _attributeManager;
    _summedEventsSubmissionQueue;
    _uniqueEventsSubmissionQueue;
    /**
     * Returns current session id.
     */
    get sessionId() {
        return this._sessionProvider.sessionId;
    }
    /**
     * Default metrics submission interval. The variable defines how often metrics will be sent to metrics system.
     */
    DEFAULT_UPDATE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60);
    DEFAULT_SERVER_URL = 'https://events.backtrace.io';
    metricsHost;
    _updateInterval;
    _updateIntervalId;
    _abortController;
    constructor(_options, _sessionProvider, _attributeManager, _summedEventsSubmissionQueue, _uniqueEventsSubmissionQueue) {
        this._options = _options;
        this._sessionProvider = _sessionProvider;
        this._attributeManager = _attributeManager;
        this._summedEventsSubmissionQueue = _summedEventsSubmissionQueue;
        this._uniqueEventsSubmissionQueue = _uniqueEventsSubmissionQueue;
        this.metricsHost = this._options.metricsSubmissionUrl ?? this.DEFAULT_SERVER_URL;
        this._updateInterval = this._options.autoSendInterval ?? this.DEFAULT_UPDATE_INTERVAL;
        this._abortController = createAbortController();
    }
    /**
     * Starts metrics submission.
     */
    initialize() {
        if (!this._sessionProvider.newSession) {
            return;
        }
        this.addSummedEvent('Application Launches');
        this.handleAbort(() => this.send(this._abortController.signal));
        if (this._updateInterval === 0) {
            return;
        }
        this._updateIntervalId = setInterval(() => {
            this.handleAbort(() => this.send(this._abortController.signal));
        }, this._updateInterval);
        unrefInterval(this._updateIntervalId);
    }
    /**
     * Returns total number of events in the submission queue.
     */
    count() {
        return this._summedEventsSubmissionQueue?.total ?? 0 + this._uniqueEventsSubmissionQueue?.total ?? 0;
    }
    /**
     * Add summed event to next Backtrace Metrics request.
     * @param metricName Summed event name.
     * @param eventAttributes event attributes.
     */
    addSummedEvent(metricName, eventAttributes = {}) {
        if (!metricName) {
            return false;
        }
        const attributes = this.convertAttributes({
            ...this._attributeManager.get().attributes,
            ...ReportDataBuilder.build(eventAttributes ?? {}).attributes,
        });
        this._summedEventsSubmissionQueue.add(new SummedEvent(metricName, attributes));
        return true;
    }
    /**
     * Sends event to the metrics system.
     */
    async send(abortSignal) {
        if (!this._sessionProvider.shouldSend()) {
            return false;
        }
        await Promise.all([this.sendUniqueEvent(abortSignal), this._summedEventsSubmissionQueue.send(abortSignal)]);
        this._sessionProvider.afterMetricsSubmission();
        return true;
    }
    /**
     * Cleans up metrics interface.
     */
    dispose() {
        this._abortController.abort();
        if (this._updateIntervalId) {
            clearInterval(this._updateIntervalId);
        }
        this._uniqueEventsSubmissionQueue.dispose && this._uniqueEventsSubmissionQueue.dispose();
        this._summedEventsSubmissionQueue.dispose && this._summedEventsSubmissionQueue.dispose();
    }
    async sendUniqueEvent(abortSignal) {
        // always add the same unique event before send.
        const { attributes } = this._attributeManager.get();
        this._uniqueEventsSubmissionQueue.add(new UniqueEvent(this.convertAttributes(attributes)));
        await this._uniqueEventsSubmissionQueue.send(abortSignal);
    }
    /**
     * Event aggregators expecting to retrieve attributes in a string format. They also
     * don't expect to retrieve null/undefined as attribute values.
     */
    convertAttributes(attributes) {
        return Object.keys(attributes)
            .filter((n) => attributes[n] != null)
            .reduce((acc, n) => {
            acc[n] = attributes[n]?.toString();
            return acc;
        }, {});
    }
    async handleAbort(fn) {
        try {
            await fn();
            return true;
        }
        catch (err) {
            if (err instanceof AbortError) {
                return false;
            }
            throw err;
        }
    }
}

class Delay {
    /**
     * Promise set timeout wrapper.
     * @param timeout timeout in ms
     * @param signal abort signal
     */
    static wait(timeout, signal) {
        return new Promise((resolve, reject) => {
            // intervalId has to be defined here,
            // as abortCallback can execute before setTimeout is executed
            // eslint-disable-next-line prefer-const
            let intervalId;
            function abortCallback() {
                clearTimeout(intervalId);
                reject(new AbortError());
            }
            if (signal?.aborted) {
                return abortCallback();
            }
            intervalId = setTimeout(() => {
                signal?.removeEventListener('abort', abortCallback);
                resolve();
            }, timeout);
            signal?.addEventListener('abort', abortCallback);
        });
    }
}

class MetricsSubmissionQueue {
    _submissionUrl;
    _eventName;
    _requestHandler;
    _metricMetadata;
    maximumEvents;
    get total() {
        return this._events.length;
    }
    get submissionUrl() {
        return this._submissionUrl;
    }
    DELAY_BETWEEN_REQUESTS = TimeHelper.convertSecondsToMilliseconds(10);
    _events = [];
    _numberOfDroppedRequests = 0;
    _abortController;
    MAXIMUM_NUMBER_OF_ATTEMPTS = 3;
    constructor(_submissionUrl, _eventName, _requestHandler, _metricMetadata, maximumEvents = 50) {
        this._submissionUrl = _submissionUrl;
        this._eventName = _eventName;
        this._requestHandler = _requestHandler;
        this._metricMetadata = _metricMetadata;
        this.maximumEvents = maximumEvents;
        this._abortController = createAbortController();
    }
    add(event) {
        this._events.push(event);
        if (this.reachedLimit()) {
            this.send();
        }
    }
    async send(abortSignal) {
        const eventsToProcess = this._events.splice(0);
        const signal = anySignal(abortSignal, this._abortController.signal);
        try {
            return await this.submit(eventsToProcess, signal);
        }
        finally {
            signal.dispose();
        }
    }
    dispose() {
        this._abortController.abort();
    }
    async submit(events, abortSignal) {
        for (let attempts = 0; attempts < this.MAXIMUM_NUMBER_OF_ATTEMPTS; attempts++) {
            const response = await this._requestHandler.post(this._submissionUrl, JSON.stringify({
                ...this._metricMetadata,
                [this._eventName]: events,
                metadata: {
                    dropped_events: this._numberOfDroppedRequests,
                },
            }, jsonEscaper()), abortSignal);
            if (response.status === 'Ok') {
                this._numberOfDroppedRequests = 0;
                return;
            }
            this._numberOfDroppedRequests++;
            await Delay.wait(2 ** attempts * this.DELAY_BETWEEN_REQUESTS, abortSignal);
        }
        // if the code reached this line, it means, we couldn't send data to server
        // we need to try to return events to the queue and try to send it once again later.
        this.returnEventsIfPossible(events);
    }
    returnEventsIfPossible(events) {
        if (this.maximumEvents < this._events.length + events.length) {
            return;
        }
        // push events to the beginning of the queue
        this._events.unshift(...events);
    }
    reachedLimit() {
        return this.maximumEvents === this._events.length && this.maximumEvents !== 0;
    }
}

class MetricsBuilder {
    _options;
    _sessionProvider;
    _attributeManager;
    _requestHandler;
    /**
     * Default metrics submission interval. The variable defines how often metrics will be sent to metrics system
     * By default 30 mins.
     */
    DEFAULT_UPDATE_INTERVAL = TimeHelper.convertSecondsToMilliseconds(30 * 60);
    APPLICATION_VERSION_ATTRIBUTE = 'application.version';
    APPLICATION_ATTRIBUTE = 'application';
    constructor(_options, _sessionProvider, _attributeManager, _requestHandler) {
        this._options = _options;
        this._sessionProvider = _sessionProvider;
        this._attributeManager = _attributeManager;
        this._requestHandler = _requestHandler;
    }
    build(uniqueEventsSubmissionQueue, summedEventsSubmissionQueue) {
        const metricsOptions = {
            ...this.optionsWithDefaults(),
            ...(this._options.metrics ?? {}),
        };
        if (!metricsOptions.enable) {
            return undefined;
        }
        const applicationInfo = this.verifyAttributeSetup();
        if (!applicationInfo) {
            return undefined;
        }
        uniqueEventsSubmissionQueue =
            uniqueEventsSubmissionQueue ??
                this.createUniqueEventSubmissionQueue(metricsOptions.metricsSubmissionUrl, applicationInfo);
        if (!uniqueEventsSubmissionQueue) {
            return undefined;
        }
        summedEventsSubmissionQueue =
            summedEventsSubmissionQueue ??
                this.createSummedEventSubmissionQueue(metricsOptions.metricsSubmissionUrl, applicationInfo);
        if (!summedEventsSubmissionQueue) {
            return undefined;
        }
        return new BacktraceMetrics(metricsOptions, this._sessionProvider, this._attributeManager, summedEventsSubmissionQueue, uniqueEventsSubmissionQueue);
    }
    verifyAttributeSetup() {
        const { attributes } = this._attributeManager.get();
        const application = attributes[this.APPLICATION_ATTRIBUTE];
        const applicationVersion = attributes[this.APPLICATION_VERSION_ATTRIBUTE];
        if (!application || !applicationVersion) {
            return undefined;
        }
        return { application, applicationVersion };
    }
    createUniqueEventSubmissionQueue(metricsHost, applicationInfo) {
        const uniqueEventsSubmissionUrl = MetricsUrlInformation.generateUniqueEventsUrl(metricsHost, this._options.url, this._options.token);
        if (!uniqueEventsSubmissionUrl) {
            return undefined;
        }
        return new MetricsSubmissionQueue(uniqueEventsSubmissionUrl, 'unique_events', this._requestHandler, {
            [this.APPLICATION_ATTRIBUTE]: applicationInfo.application,
            appversion: applicationInfo.applicationVersion,
        }, this._options?.metrics?.size);
    }
    createSummedEventSubmissionQueue(metricsHost, applicationInfo) {
        const summedEventsSubmissionUrl = MetricsUrlInformation.generateSummedEventsUrl(metricsHost, this._options.url, this._options.token);
        if (!summedEventsSubmissionUrl) {
            return undefined;
        }
        return new MetricsSubmissionQueue(summedEventsSubmissionUrl, 'summed_events', this._requestHandler, {
            [this.APPLICATION_ATTRIBUTE]: applicationInfo.application,
            appversion: applicationInfo.applicationVersion,
        }, this._options?.metrics?.size);
    }
    optionsWithDefaults() {
        return {
            enable: true,
            autoSendInterval: this.DEFAULT_UPDATE_INTERVAL,
            metricsSubmissionUrl: 'https://events.backtrace.io',
            size: 50,
        };
    }
}

class SingleSessionProvider {
    newSession = true;
    sessionId = IdGenerator.uuid();
    _lastActive = 0;
    get lastActive() {
        return this._lastActive;
    }
    afterMetricsSubmission() {
        this._lastActive = TimeHelper.now();
    }
    /**
     * Allow to alway send metrics - in the single session there is no reason
     * to skip sending metrics.
     */
    shouldSend() {
        return true;
    }
}

class RateLimitWatcher {
    /**
     * Time the single report can stay in the queue in ms.
     */
    MAXIMUM_TIME_IN_QUEUE = 60 * 1000;
    _reportPerMin;
    _watcherEnable;
    _reportQueue = [];
    get enabled() {
        return this._watcherEnable;
    }
    constructor(reportPerMin) {
        if (reportPerMin == null) {
            reportPerMin = 0;
        }
        if (reportPerMin < 0) {
            throw new Error('ReportPerMinute argument must be greater or equal to zero');
        }
        this._reportPerMin = reportPerMin;
        this._watcherEnable = reportPerMin > 0;
    }
    skipReport() {
        const time = TimeHelper.now();
        if (!this._watcherEnable) {
            return false;
        }
        this.clear(time);
        if (this._reportQueue.length >= this._reportPerMin) {
            return true;
        }
        this._reportQueue.push(time);
        return false;
    }
    clear(time) {
        if (this._reportQueue.length === 0) {
            return;
        }
        // we don't have anything to remove
        if (time - this._reportQueue[0] < this.MAXIMUM_TIME_IN_QUEUE) {
            return;
        }
        for (let queueIndex = this._reportQueue.length; queueIndex != 0; queueIndex--) {
            if (time - this._reportQueue[queueIndex - 1] > this.MAXIMUM_TIME_IN_QUEUE) {
                this._reportQueue = this._reportQueue.slice(queueIndex);
                return;
            }
        }
    }
}

class BacktraceCoreClient extends Events {
    /**
     * Backtrace client instance
     */
    static _instance;
    /**
     * Determines if the client is enabled.
     */
    get enabled() {
        return this._enabled;
    }
    /**
     * Current session id
     */
    get sessionId() {
        return this._sessionProvider.sessionId;
    }
    /**
     * Backtrace SDK name
     */
    get agent() {
        return this._sdkOptions.agent;
    }
    /**
     * Backtrace SDK version
     */
    get agentVersion() {
        return this._sdkOptions.agentVersion;
    }
    /**
     * Available cached client attributes
     */
    get attributes() {
        return this.attributeManager.get().attributes;
    }
    /**
     * Available cached client annotatations
     */
    get annotations() {
        return this.attributeManager.get().annotations;
    }
    get metrics() {
        return this._modules.get(BacktraceMetrics);
    }
    get breadcrumbs() {
        return this._modules.get(BreadcrumbsManager);
    }
    get database() {
        return this._modules.get(BacktraceDatabase);
    }
    /**
     * Client cached attachments
     */
    get attachments() {
        return this.attachmentManager.get();
    }
    /**
     * Modules used by the client
     */
    get modules() {
        return this._modules;
    }
    get sessionFiles() {
        return this._modules.get(SessionFiles);
    }
    options;
    attributeManager;
    attachmentManager;
    fileSystem;
    _modules = new Map();
    _dataBuilder;
    _reportSubmission;
    _rateLimitWatcher;
    _sessionProvider;
    _sdkOptions;
    _requestHandler;
    _enabled = false;
    constructor(setup) {
        super();
        this.options = setup.options;
        this.fileSystem = setup.fileSystem;
        this._sdkOptions = setup.sdkOptions;
        this._sessionProvider = setup.sessionProvider ?? new SingleSessionProvider();
        this._reportSubmission =
            setup.reportSubmission ?? new RequestBacktraceReportSubmission(this.options, setup.requestHandler);
        this._rateLimitWatcher = new RateLimitWatcher(this.options.rateLimit);
        this._requestHandler = setup.requestHandler;
        const attributeProviders = [
            new ClientAttributeProvider(this.agent, this.agentVersion, this._sessionProvider.sessionId),
        ];
        if (setup.attributeProviders) {
            attributeProviders.push(...setup.attributeProviders);
        }
        if (this.options.userAttributes) {
            attributeProviders.push(new UserAttributeProvider(this.options.userAttributes));
        }
        this.attributeManager = new AttributeManager(attributeProviders);
        this.attachmentManager = new AttachmentManager();
        if (this.options.attachments) {
            this.attachmentManager.add(...this.options.attachments);
        }
        const stackTraceConverter = setup.stackTraceConverter ?? new V8StackTraceConverter();
        this._dataBuilder = new BacktraceDataBuilder(this._sdkOptions, stackTraceConverter, this.attributeManager, new DebugIdProvider(stackTraceConverter, setup.debugIdMapProvider));
        if (this.options?.database?.enable === true && setup.fileSystem) {
            const provider = BacktraceDatabaseFileStorageProvider.createIfValid(setup.fileSystem, this.options.database);
            if (this.fileSystem) {
                const sessionFiles = new SessionFiles(this.fileSystem, this.options.database.path, this.sessionId, this.options.database.maximumOldSessions ?? 1);
                this._modules.set(SessionFiles, sessionFiles);
            }
            if (provider) {
                const database = new BacktraceDatabase(this.options.database, provider, this._reportSubmission, this.sessionFiles);
                this._modules.set(BacktraceDatabase, database);
            }
        }
        const metrics = new MetricsBuilder(this.options, this._sessionProvider, this.attributeManager, setup.requestHandler).build(setup.uniqueMetricsQueue, setup.summedMetricsQueue);
        if (metrics) {
            this._modules.set(BacktraceMetrics, metrics);
        }
        if (this.options.breadcrumbs?.enable !== false) {
            const breadcrumbsManager = new BreadcrumbsManager(this.options?.breadcrumbs, setup.breadcrumbsSetup);
            this._modules.set(BreadcrumbsManager, breadcrumbsManager);
        }
        if (setup.modules) {
            for (const module of setup.modules) {
                this.addModule(module);
            }
        }
    }
    initialize() {
        this.validateAttributes();
        if (this.fileSystem && this.options.database?.createDatabaseDirectory) {
            if (!this.options.database.path) {
                throw new Error('Missing mandatory path to the database. Please define the database.path option in the configuration.');
            }
            this.fileSystem.createDirSync(this.options.database?.path);
        }
        for (const module of this._modules.values()) {
            if (module.bind) {
                module.bind(this.getModuleBindData());
            }
            if (module.initialize) {
                module.initialize();
            }
        }
        this.sessionFiles?.clearPreviousSessions();
        this._enabled = true;
        BacktraceCoreClient._instance = this;
    }
    addAttribute(attributes) {
        this.attributeManager.add(attributes);
    }
    addAttachment(...attachments) {
        this.attachmentManager.add(...attachments);
    }
    // This function CANNOT be an async function due to possible async state machine stack frame inclusion, which breaks the skip stacks
    send(data, reportAttributesOrAbortSignal, reportAttachments = [], abortSignal) {
        if (!this._enabled) {
            return Promise.resolve(BacktraceReportSubmissionResult.SdkDisabled());
        }
        if (this._rateLimitWatcher.skipReport()) {
            return Promise.resolve(BacktraceReportSubmissionResult.OnLimitReached('Client'));
        }
        // If data is BacktraceReport, we know that the second argument should be only AbortSignal
        const reportAttributes = !this.isReport(data)
            ? reportAttributesOrAbortSignal
            : undefined;
        // If data is BacktraceReport, we know that the second argument should be only AbortSignal
        abortSignal = !this.isReport(data) ? abortSignal : reportAttributesOrAbortSignal;
        const report = this.isReport(data)
            ? data
            : new BacktraceReport(data, reportAttributes, [], {
                skipFrames: this.skipFrameOnMessage(data),
            });
        this.emit('before-skip', report);
        if (this.options.skipReport && this.options.skipReport(report)) {
            return Promise.resolve(BacktraceReportSubmissionResult.ReportSkipped());
        }
        const backtraceData = this.generateSubmissionData(report);
        if (!backtraceData) {
            return Promise.resolve(BacktraceReportSubmissionResult.ReportSkipped());
        }
        const submissionAttachments = this.generateSubmissionAttachments(report, reportAttachments);
        this.emit('before-send', report, backtraceData, submissionAttachments);
        return this._reportSubmission
            .send(backtraceData, submissionAttachments, abortSignal)
            .then((submissionResult) => {
            this.emit('after-send', report, backtraceData, submissionAttachments, submissionResult);
            return submissionResult;
        });
    }
    /**
     * Disposes the client and all client callbacks
     */
    dispose() {
        this._enabled = false;
        for (const module of this._modules.values()) {
            if (module.dispose) {
                module.dispose();
            }
        }
        BacktraceCoreClient.destroy();
    }
    addModule(typeOrModule, module) {
        let type;
        if (typeof typeOrModule === 'function') {
            type = typeOrModule;
        }
        else {
            module = typeOrModule;
            type = Object.getPrototypeOf(module);
        }
        if (!module) {
            throw new Error('Module implementation is required.');
        }
        this._modules.set(type, module);
        if (this._enabled) {
            module.bind && module.bind(this.getModuleBindData());
            module.initialize && module.initialize();
        }
    }
    generateSubmissionData(report) {
        const backtraceData = this._dataBuilder.build(report);
        if (!this.options.beforeSend) {
            return backtraceData;
        }
        return this.options.beforeSend(backtraceData);
    }
    generateSubmissionAttachments(report, reportAttachments) {
        return [...this.attachmentManager.get(), ...(report.attachments ?? []), ...(reportAttachments ?? [])];
    }
    skipFrameOnMessage(data) {
        return typeof data === 'string' ? 1 : 0;
    }
    isReport(data) {
        return data instanceof BacktraceReport;
    }
    getModuleBindData() {
        return {
            client: this,
            options: this.options,
            attributeManager: this.attributeManager,
            attachmentManager: this.attachmentManager,
            reportSubmission: this._reportSubmission,
            requestHandler: this._requestHandler,
            database: this.database,
            sessionFiles: this.sessionFiles,
            fileSystem: this.fileSystem,
        };
    }
    static destroy() {
        this._instance = undefined;
    }
    validateAttributes() {
        function validateApplicationAndVersion(attributes) {
            if (!attributes['application'] || !attributes['application.version']) {
                return 'application and application.version attributes must be defined.';
            }
        }
        // Validate scoped attributes first. If they return no errors, there is no point
        // in checking all attributes, which resolving of may be slower.
        const scoped = this.attributeManager.get('scoped');
        const scopedError = validateApplicationAndVersion(scoped.attributes);
        if (!scopedError) {
            return;
        }
        const allAttributes = this.attributeManager.get();
        const error = validateApplicationAndVersion(allAttributes.attributes);
        if (!error) {
            return;
        }
        throw new Error(error);
    }
}

class BacktraceCoreClientBuilder {
    clientSetup;
    constructor(clientSetup) {
        this.clientSetup = clientSetup;
    }
    addAttributeProvider(provider) {
        if (!this.clientSetup.attributeProviders) {
            this.clientSetup.attributeProviders = [provider];
        }
        else {
            this.clientSetup.attributeProviders.push(provider);
        }
        return this;
    }
    useBreadcrumbSubscriber(breadcrumbSubscriber) {
        if (!this.clientSetup.breadcrumbsSetup) {
            this.clientSetup.breadcrumbsSetup = {};
        }
        if (!this.clientSetup.breadcrumbsSetup.subscribers) {
            this.clientSetup.breadcrumbsSetup.subscribers = [breadcrumbSubscriber];
        }
        else {
            this.clientSetup.breadcrumbsSetup.subscribers.push(breadcrumbSubscriber);
        }
        return this;
    }
    useBreadcrumbsStorage(storage) {
        if (!this.clientSetup.breadcrumbsSetup) {
            this.clientSetup.breadcrumbsSetup = {};
        }
        if (typeof storage === 'function') {
            this.clientSetup.breadcrumbsSetup.storage = storage;
        }
        else {
            this.clientSetup.breadcrumbsSetup.storage = () => storage;
        }
        return this;
    }
    useSessionProvider(sessionProvider) {
        this.clientSetup.sessionProvider = sessionProvider;
        return this;
    }
    useRequestHandler(handler) {
        this.clientSetup.requestHandler = handler;
        return this;
    }
    useStackTraceConverter(stackTraceConverter) {
        this.clientSetup.stackTraceConverter = stackTraceConverter;
        return this;
    }
    useFileSystem(fileSystem) {
        this.clientSetup.fileSystem = fileSystem;
        return this;
    }
    useReportSubmission(reportSubmission) {
        this.clientSetup.reportSubmission = reportSubmission;
        return this;
    }
    useSummedMetricsQueue(queue) {
        this.clientSetup.summedMetricsQueue = queue;
        return this;
    }
    useUniqueMetricsQueue(queue) {
        this.clientSetup.uniqueMetricsQueue = queue;
        return this;
    }
    useModule(module) {
        if (!this.clientSetup.modules) {
            this.clientSetup.modules = [module];
        }
        else {
            this.clientSetup.modules.push(module);
        }
        return this;
    }
}

class BacktraceStringAttachment {
    name;
    data;
    constructor(name, data) {
        this.name = name;
        this.data = data;
    }
    get() {
        return this.data;
    }
}

class BacktraceUint8ArrayAttachment {
    name;
    data;
    constructor(name, data) {
        this.name = name;
        this.data = data;
    }
    get() {
        return new Blob([this.data.buffer]);
    }
}

const ATTRIBUTE_FILE_NAME = 'bt-attributes';
class FileAttributeManager {
    _fileSystem;
    _fileName;
    _attributeManager;
    constructor(_fileSystem, _fileName) {
        this._fileSystem = _fileSystem;
        this._fileName = _fileName;
    }
    static create(fileSystem) {
        return new FileAttributeManager(fileSystem);
    }
    static createFromSession(sessionFiles, fileSystem) {
        const fileName = sessionFiles.getFileName(ATTRIBUTE_FILE_NAME);
        return new FileAttributeManager(fileSystem, fileName);
    }
    initialize() {
        this.saveAttributes();
    }
    bind({ attributeManager, sessionFiles }) {
        if (this._fileName) {
            throw new Error('This instance is already bound.');
        }
        if (!sessionFiles) {
            return;
        }
        this._fileName = sessionFiles.getFileName(ATTRIBUTE_FILE_NAME);
        this._attributeManager = attributeManager;
        attributeManager.attributeEvents.on('scoped-attributes-updated', () => this.saveAttributes());
    }
    dispose() {
        this._fileName = undefined;
    }
    async get() {
        if (!this._fileName) {
            return {};
        }
        try {
            const content = await this._fileSystem.readFile(this._fileName);
            return JSON.parse(content);
        }
        catch {
            return {};
        }
    }
    async saveAttributes() {
        if (!this._fileName || !this._attributeManager) {
            return;
        }
        const reportData = this._attributeManager.get('scoped');
        await this._fileSystem.writeFile(this._fileName, JSON.stringify(reportData.attributes, jsonEscaper()));
    }
}

const SESSION_MARKER_PREFIX = 'bt-session';
const isDefined = (t) => !!t;
class SessionFiles {
    _fileSystem;
    _directory;
    sessionId;
    _maxPreviousLockedSessions;
    timestamp;
    _lockable;
    marker;
    _events = new Events();
    _escapedSessionId;
    _locks = new Set();
    _previousSession;
    _cleared = false;
    constructor(_fileSystem, _directory, sessionId, _maxPreviousLockedSessions = 1, timestamp = Date.now(), _lockable = true) {
        this._fileSystem = _fileSystem;
        this._directory = _directory;
        this.sessionId = sessionId;
        this._maxPreviousLockedSessions = _maxPreviousLockedSessions;
        this.timestamp = timestamp;
        this._lockable = _lockable;
        this._escapedSessionId = this.escapeFileName(sessionId);
        this.marker = this.getFileName(SESSION_MARKER_PREFIX);
    }
    initialize() {
        this.createSessionMarker();
    }
    getPreviousSession() {
        if (this._previousSession) {
            return this._previousSession;
        }
        const directoryFiles = this.readDirectoryFiles();
        if (!directoryFiles.length) {
            return undefined;
        }
        const sessionMarkers = directoryFiles
            .filter((f) => f.startsWith(SESSION_MARKER_PREFIX))
            .map((f) => this.getFileSession(f))
            .filter(isDefined)
            .sort((a, b) => b.timestamp - a.timestamp);
        const currentSessionMarker = sessionMarkers.find((s) => s.sessionId === this.sessionId);
        const lastSessionMarker = currentSessionMarker
            ? sessionMarkers.filter(({ timestamp }) => currentSessionMarker.timestamp > timestamp)[0]
            : sessionMarkers[0];
        if (!lastSessionMarker) {
            return undefined;
        }
        return (this._previousSession = new SessionFiles(this._fileSystem, this._directory, lastSessionMarker.sessionId, this._maxPreviousLockedSessions - 1, lastSessionMarker.timestamp, this._maxPreviousLockedSessions > 0));
    }
    getSessionWithId(sessionId) {
        // eslint-disable-next-line @typescript-eslint/no-this-alias
        let session = this;
        while (session && session.sessionId !== sessionId) {
            session = session.getPreviousSession();
        }
        return session;
    }
    getPreviousSessions(count = Infinity) {
        const result = [];
        let current = this.getPreviousSession();
        while (current && count > 0) {
            result.push(current);
            count--;
            current = current.getPreviousSession();
        }
        return result;
    }
    lockPreviousSessions(lockId) {
        lockId = lockId ?? IdGenerator.uuid();
        for (const session of this.getPreviousSessions(this._maxPreviousLockedSessions)) {
            session.lock(lockId);
        }
        return lockId;
    }
    unlockPreviousSessions(lockId) {
        for (const session of this.getPreviousSessions(this._maxPreviousLockedSessions)) {
            session.unlock(lockId);
        }
    }
    clearPreviousSessions() {
        for (const session of this.getPreviousSessions()) {
            session.clearSession();
        }
    }
    getFileName(prefix) {
        this.throwIfCleared();
        return this._directory + '/' + `${this.escapeFileName(prefix)}_${this._escapedSessionId}_${this.timestamp}`;
    }
    getSessionFiles() {
        this.throwIfCleared();
        const files = this.readDirectoryFiles();
        return files
            .map((file) => this.getFileSession(file))
            .filter(isDefined)
            .filter(({ sessionId }) => sessionId === this.sessionId)
            .map(({ file }) => this._directory + '/' + file);
    }
    clearSession() {
        if (this._locks.size > 0) {
            this._events.once('unlocked', () => this.clearSession());
            return;
        }
        if (this._cleared) {
            return;
        }
        try {
            const sessionFiles = this.getSessionFiles();
            for (const file of sessionFiles) {
                this._fileSystem.unlinkSync(file);
            }
        }
        catch {
            // Don't propagate errors
        }
        finally {
            this._cleared = true;
        }
    }
    lock(lockId) {
        if (this._cleared || !this._lockable) {
            return;
        }
        lockId = lockId ?? IdGenerator.uuid();
        this._locks.add(lockId);
        return lockId;
    }
    unlock(lockId) {
        this._locks.delete(lockId);
        if (this._locks.size === 0) {
            this._events.emit('unlocked');
        }
    }
    getFileSession(file) {
        const [escapedSessionId, rawTimestamp] = this.splitByOneChar(file, '_').slice(-2);
        const timestamp = parseInt(rawTimestamp);
        if (isNaN(timestamp)) {
            return undefined;
        }
        return { file, escapedSessionId, timestamp, sessionId: this.unescapeFileName(escapedSessionId) };
    }
    readDirectoryFiles() {
        try {
            return this._fileSystem.readDirSync(this._directory);
        }
        catch {
            return [];
        }
    }
    createSessionMarker() {
        this._fileSystem.writeFileSync(this.marker, '');
    }
    escapeFileName(name) {
        return name.replace(/_/g, '__');
    }
    unescapeFileName(name) {
        return name.replace(/__/g, '_');
    }
    splitByOneChar(str, char) {
        const result = [];
        let start = 0;
        let index = str.indexOf(char);
        while (index !== -1) {
            if (str[index + 1] === char) {
                index = str.indexOf(char, index + 2);
            }
            else {
                result.push(str.substring(start, index));
                start = index + 1;
                index = str.indexOf(char, start);
            }
        }
        result.push(str.substring(start));
        return result;
    }
    throwIfCleared() {
        if (this._cleared) {
            throw new Error('This session files are cleared.');
        }
    }
}

const SOURCE_DEBUG_ID_VARIABLE = '_btDebugIds';
class DebugIdProvider {
    _stackTraceConverter;
    _debugIdMapProvider;
    _fileDebugIds;
    constructor(_stackTraceConverter, _debugIdMapProvider) {
        this._stackTraceConverter = _stackTraceConverter;
        this._debugIdMapProvider = _debugIdMapProvider;
        this._fileDebugIds = this.loadDebugIds();
    }
    getDebugId(file) {
        const debugId = this._fileDebugIds[file];
        if (debugId) {
            return debugId;
        }
        // in case of dynamic require - lazy load dynamically debug ids
        this._fileDebugIds = this.loadDebugIds();
        return this._fileDebugIds[file];
    }
    loadDebugIds() {
        const debugIdMap = this._debugIdMapProvider?.getDebugIdMap();
        if (!debugIdMap) {
            return {};
        }
        const message = new Error().message;
        const result = {};
        for (const [rawStack, debugId] of Object.entries(debugIdMap)) {
            const frames = this._stackTraceConverter.convert(rawStack, message);
            if (!frames.length) {
                continue;
            }
            // The first frame will have the file's path
            const frame = frames[0];
            result[frame.library] = debugId;
        }
        return result;
    }
}

class VariableDebugIdMapProvider {
    _variable;
    constructor(_variable) {
        this._variable = _variable;
    }
    getDebugIdMap() {
        return this._variable[SOURCE_DEBUG_ID_VARIABLE] ?? {};
    }
}

exports.ANONYMOUS_FUNCTION = ANONYMOUS_FUNCTION;
exports.AttachmentManager = AttachmentManager;
exports.BacktraceCoreApi = BacktraceCoreApi;
exports.BacktraceCoreClient = BacktraceCoreClient;
exports.BacktraceCoreClientBuilder = BacktraceCoreClientBuilder;
exports.BacktraceDatabase = BacktraceDatabase;
exports.BacktraceReport = BacktraceReport;
exports.BacktraceReportSubmissionResult = BacktraceReportSubmissionResult;
exports.BacktraceStringAttachment = BacktraceStringAttachment;
exports.BacktraceUint8ArrayAttachment = BacktraceUint8ArrayAttachment;
exports.BreadcrumbsManager = BreadcrumbsManager;
exports.ConnectionError = ConnectionError;
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
exports.DebugIdProvider = DebugIdProvider;
exports.FileAttributeManager = FileAttributeManager;
exports.IdGenerator = IdGenerator;
exports.OverwritingArray = OverwritingArray;
exports.RequestBacktraceReportSubmission = RequestBacktraceReportSubmission;
exports.SOURCE_DEBUG_ID_VARIABLE = SOURCE_DEBUG_ID_VARIABLE;
exports.SessionFiles = SessionFiles;
exports.SingleSessionProvider = SingleSessionProvider;
exports.SubmissionUrlInformation = SubmissionUrlInformation;
exports.SummedEvent = SummedEvent;
exports.TimeHelper = TimeHelper;
exports.UNKNOWN_FRAME = UNKNOWN_FRAME;
exports.V8StackTraceConverter = V8StackTraceConverter;
exports.VariableDebugIdMapProvider = VariableDebugIdMapProvider;
exports.anySignal = anySignal;
exports.defaultBreadcrumbsLogLevel = defaultBreadcrumbsLogLevel;
exports.defaultBreadcurmbType = defaultBreadcurmbType;
exports.jsonEscaper = jsonEscaper;
//# sourceMappingURL=bundle.cjs.map