UNPKG

@backtrace/node

Version:
1,389 lines 54.7 kB
import { DEFAULT_TIMEOUT, BacktraceReportSubmissionResult, ConnectionError, BacktraceCoreApi, TimeHelper, BreadcrumbType, BreadcrumbLogLevel, jsonEscaper, IdGenerator, BacktraceCoreClientBuilder, BacktraceReport, BacktraceCoreClient, VariableDebugIdMapProvider, BreadcrumbsManager, FileAttributeManager } from '@backtrace/sdk-core';
export { BacktraceReport, BacktraceStringAttachment, BacktraceUint8ArrayAttachment, BreadcrumbLogLevel, BreadcrumbType, BreadcrumbsManager } from '@backtrace/sdk-core';
import fs from 'fs';
import path from 'path';
import FormData from 'form-data';
import http from 'http';
import https from 'https';
import { Readable, Writable } from 'stream';
import EventEmitter from 'events';
import process$1 from 'process';
import { fileURLToPath } from 'url';
import os from 'os';
import { execSync } from 'child_process';

class BacktraceBufferAttachment {
    constructor(name, buffer) {
        this.name = name;
        this.buffer = buffer;
    }
    get() {
        return this.buffer;
    }
}

class BacktraceFileAttachment {
    constructor(filePath, name, _fileSystem) {
        this.filePath = filePath;
        this._fileSystem = _fileSystem;
        this.name = name !== null && name !== void 0 ? name : path.basename(this.filePath);
    }
    get() {
        var _a, _b;
        if (!((_a = this._fileSystem) !== null && _a !== void 0 ? _a : fs).existsSync(this.filePath)) {
            return undefined;
        }
        return ((_b = this._fileSystem) !== null && _b !== void 0 ? _b : fs).createReadStream(this.filePath);
    }
}

const ATTACHMENT_FILE_NAME = 'bt-attachments';
class FileAttachmentsManager {
    constructor(_fileSystem, _fileName) {
        this._fileSystem = _fileSystem;
        this._fileName = _fileName;
    }
    static create(fileSystem) {
        return new FileAttachmentsManager(fileSystem);
    }
    static createFromSession(sessionFiles, fileSystem) {
        const fileName = sessionFiles.getFileName(ATTACHMENT_FILE_NAME);
        return new FileAttachmentsManager(fileSystem, fileName);
    }
    initialize() {
        this.saveAttachments();
    }
    bind({ attachmentManager, sessionFiles }) {
        if (this._fileName) {
            throw new Error('This instance is already bound.');
        }
        if (!sessionFiles) {
            return;
        }
        this._fileName = sessionFiles.getFileName(ATTACHMENT_FILE_NAME);
        this._attachmentsManager = attachmentManager;
        attachmentManager.attachmentEvents.on('scoped-attachments-updated', () => this.saveAttachments());
    }
    dispose() {
        this._fileName = undefined;
    }
    async get() {
        if (!this._fileName) {
            return [];
        }
        try {
            const content = await this._fileSystem.readFile(this._fileName);
            const attachments = JSON.parse(content);
            return attachments.map(([path, name]) => new BacktraceFileAttachment(path, name));
        }
        catch (_a) {
            return [];
        }
    }
    async saveAttachments() {
        if (!this._fileName || !this._attachmentsManager) {
            return;
        }
        const fileAttachments = this._attachmentsManager
            .get('scoped')
            .filter((f) => f instanceof BacktraceFileAttachment)
            .map((f) => [f.filePath, f.name]);
        await this._fileSystem.writeFile(this._fileName, JSON.stringify(fileAttachments));
    }
}

class BacktraceNodeRequestHandler {
    constructor(options) {
        var _a;
        this.UPLOAD_FILE_NAME = 'upload_file';
        this.JSON_HEADERS = {
            'Content-type': 'application/json',
            'Transfer-Encoding': 'chunked',
        };
        this.MULTIPART_HEADERS = {
            'Transfer-Encoding': 'chunked',
        };
        this._timeout = (_a = options === null || options === void 0 ? void 0 : options.timeout) !== null && _a !== void 0 ? _a : DEFAULT_TIMEOUT;
        this._ignoreSslCertificate = options === null || options === void 0 ? void 0 : options.ignoreSslCertificate;
    }
    async postError(submissionUrl, dataJson, attachments, abortSignal) {
        const formData = attachments.length === 0 ? dataJson : this.createFormData(dataJson, attachments);
        return this.send(submissionUrl, formData, abortSignal);
    }
    async post(submissionUrl, payload, abortSignal) {
        return this.send(submissionUrl, payload, abortSignal);
    }
    async postAttachment(submissionUrl, attachment, abortSignal) {
        try {
            const attachmentData = attachment.get();
            if (!attachmentData) {
                return BacktraceReportSubmissionResult.ReportSkipped();
            }
            const url = new URL(submissionUrl);
            const httpClient = this.getHttpClient(url);
            return new Promise((res) => {
                const request = httpClient.request(url, {
                    rejectUnauthorized: this._ignoreSslCertificate === true,
                    timeout: this._timeout,
                    method: 'POST',
                }, (response) => {
                    let result = '';
                    response.on('data', (d) => {
                        result += d.toString();
                    });
                    response.on('end', () => {
                        cleanup();
                        return res(this.handleResponse(response, result));
                    });
                    response.on('error', () => {
                        cleanup();
                    });
                });
                abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', () => BacktraceNodeRequestHandler.abortFn(abortSignal, request), { once: true });
                function cleanup() {
                    abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cleanup);
                }
                request.on('error', (err) => {
                    cleanup();
                    return res(this.handleRequestError(err));
                });
                if (attachmentData instanceof Readable) {
                    attachmentData.pipe(request);
                }
                else {
                    request.write(attachmentData);
                }
            });
        }
        catch (err) {
            return this.handleError(err);
        }
    }
    async send(submissionUrl, payload, abortSignal) {
        try {
            const url = new URL(submissionUrl);
            const httpClient = this.getHttpClient(url);
            return new Promise((res) => {
                const request = httpClient.request(url, {
                    rejectUnauthorized: this._ignoreSslCertificate === true,
                    timeout: this._timeout,
                    method: 'POST',
                    headers: typeof payload === 'string'
                        ? this.JSON_HEADERS
                        : { ...payload.getHeaders(), ...this.MULTIPART_HEADERS },
                }, (response) => {
                    let result = '';
                    response.on('data', (d) => {
                        result += d.toString();
                    });
                    response.on('end', () => {
                        cleanup();
                        return res(this.handleResponse(response, result));
                    });
                    response.on('error', () => {
                        cleanup();
                    });
                });
                abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.addEventListener('abort', () => BacktraceNodeRequestHandler.abortFn(abortSignal, request), { once: true });
                function cleanup() {
                    abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.removeEventListener('abort', cleanup);
                }
                request.on('error', (err) => {
                    cleanup();
                    return res(this.handleRequestError(err));
                });
                if (typeof payload === 'string') {
                    request.write(payload);
                    request.end();
                }
                else {
                    payload.pipe(request);
                }
            });
        }
        catch (err) {
            return this.handleError(err);
        }
    }
    getHttpClient(submissionUrl) {
        return submissionUrl.protocol === 'https:' ? https : http;
    }
    handleResponse(response, result) {
        switch (response.statusCode) {
            case 200: {
                return BacktraceReportSubmissionResult.Ok(JSON.parse(result));
            }
            case 401:
            case 403: {
                return BacktraceReportSubmissionResult.OnInvalidToken();
            }
            case 429: {
                return BacktraceReportSubmissionResult.OnLimitReached();
            }
            default: {
                return BacktraceReportSubmissionResult.OnInternalServerError(result);
            }
        }
    }
    handleRequestError(err) {
        if (ConnectionError.isConnectionError(err)) {
            return BacktraceReportSubmissionResult.OnNetworkingError(err.message);
        }
        return BacktraceReportSubmissionResult.OnInternalServerError(err.message);
    }
    handleError(err) {
        if (ConnectionError.isConnectionError(err)) {
            return BacktraceReportSubmissionResult.OnNetworkingError(err.message);
        }
        const errorMessage = err instanceof Error ? err.message : err;
        return BacktraceReportSubmissionResult.OnUnknownError(errorMessage);
    }
    static abortFn(signal, request) {
        const reason = signal.reason instanceof Error
            ? signal.reason
            : typeof signal.reason === 'string'
                ? new Error(signal.reason)
                : new Error('Operation cancelled.');
        request.destroy(reason);
    }
    createFormData(json, attachments) {
        const formData = new FormData();
        formData.append(this.UPLOAD_FILE_NAME, json, `${this.UPLOAD_FILE_NAME}.json`);
        if (!attachments || attachments.length === 0) {
            return formData;
        }
        for (const attachment of attachments) {
            const data = attachment.get();
            if (!data) {
                continue;
            }
            formData.append(`attachment_${attachment.name}`, data, attachment.name);
        }
        return formData;
    }
}

class BacktraceApi extends BacktraceCoreApi {
    constructor(options) {
        var _a;
        super(options, (_a = options.requestHandler) !== null && _a !== void 0 ? _a : new BacktraceNodeRequestHandler(options.requestHandlerOptions));
    }
}

const AGENT = {
    langName: 'nodejs',
    langVersion: process.version,
    /**
     * To do - in the build stage, we can inject information
     * about our package name and agent version. Since we don't have
     * it now, I'm leaving it hardcoded, but in the future we want
     * to change it and use webpack to generate it
     */
    agent: '@backtrace/node',
    agentVersion: '0.7.0',
};

/**
 * Transform a client attachment into the attachment model.
 */
function transformAttachment(attachment) {
    return typeof attachment === 'string'
        ? new BacktraceFileAttachment(attachment)
        : attachment;
}

/**
 * Splits incoming data into chunks, writing them to the sink.
 */
function chunkifier({ sink: streamFactory, ...options }) {
    let chunkCount = 0;
    function createStreamContext() {
        const stream = streamFactory(chunkCount++);
        // We need to forward the 'drain' event, in case the sink stream resumes writing.
        const disposeEvents = forwardEvents(stream, writable, 'drain');
        return { stream, disposeEvents, isEmptyChunk: true };
    }
    let context;
    let splitter;
    const writable = new Writable({
        ...options,
        write(data, encoding, callback) {
            // If data is empty from the start, forward the write directly to current stream
            if (!data.length) {
                return (context !== null && context !== void 0 ? context : (context = createStreamContext())).stream.write(data, encoding, callback);
            }
            while (data) {
                if (!data.length) {
                    break;
                }
                splitter !== null && splitter !== void 0 ? splitter : (splitter = options.splitter());
                const [currentChunk, nextChunk] = splitter(data, encoding);
                if (!nextChunk) {
                    const current = (context !== null && context !== void 0 ? context : (context = createStreamContext()));
                    if (currentChunk.length) {
                        current.isEmptyChunk = false;
                    }
                    return current.stream.write(currentChunk, encoding, callback);
                }
                data = nextChunk;
                if (context ? context.isEmptyChunk : !currentChunk.length && !options.allowEmptyChunks) {
                    continue;
                }
                const current = (context !== null && context !== void 0 ? context : (context = createStreamContext()));
                current.disposeEvents();
                current.stream.write(currentChunk, encoding, (err) => {
                    current.stream.destroy(err !== null && err !== void 0 ? err : undefined);
                });
                // On next loop iteration, or write, create new stream again
                context = undefined;
                splitter = undefined;
            }
            callback();
            return true;
        },
    });
    return writable;
}
function forwardEvents(from, to, ...events) {
    const fwd = (event) => (...args) => to.emit(event, ...args, to);
    const forwards = [];
    for (const event of events) {
        const fn = fwd(event);
        from.on(event, fn);
        forwards.push([event, fn]);
    }
    // Return a dispose function - when called, event callbacks will be detached
    const off = () => forwards.forEach(([event, fn]) => from.off(event, fn));
    return off;
}

/**
 * Combines several splitters into one.
 *
 * Each splitter is checked, in order that they are passed.
 * Splitters receive always the first chunk.
 *
 * If more than one splitter returns splitted chunks, the second
 * chunks are concatenated and treated as one chunk.
 * @param splitters
 * @returns
 */
function combinedChunkSplitter(...splitters) {
    return (chunk, encoding) => {
        const rest = [];
        for (const splitter of splitters) {
            const [c1, c2] = splitter(chunk, encoding);
            chunk = c1;
            if (c2) {
                // Prepend second chunk to the rest
                rest.unshift(c2);
            }
        }
        // If any chunks are in rest, concatenate them and pass as the second chunk
        return [chunk, rest.length ? Buffer.concat(rest) : undefined];
    };
}

/**
 * Chunk sink which writes data to disk.
 *
 * Each time a new chunk is created, a new stream is created with path provided from options.
 */
class FileChunkSink extends EventEmitter {
    /**
     * Returns all files that have been written to and are not deleted.
     */
    get files() {
        return this._streamTracker.elements;
    }
    constructor(_options) {
        super();
        this._options = _options;
        // Track files using a FIFO queue
        this._streamTracker = limitedFifo(_options.maxFiles, (file) => {
            // On file removal, emit delete or delete the file
            // If file is not yet destroyed (pending writes), wait on 'close'
            if (file.destroyed) {
                this.emitDeleteOrDelete(file);
            }
            else {
                file.once('close', () => this.emitDeleteOrDelete(file));
            }
        });
    }
    /**
     * Returns `ChunkSink`. Pass this to `chunkifier`.
     */
    getSink() {
        return (n) => {
            const stream = this.createStream(n);
            this._streamTracker.push(stream);
            this.emit('create', stream);
            return stream;
        };
    }
    createStream(n) {
        var _a;
        const path = this._options.file(n);
        return ((_a = this._options.fs) !== null && _a !== void 0 ? _a : fs).createWriteStream(path);
    }
    emitDeleteOrDelete(file) {
        // If 'delete' event is not handled, delete the file
        if (!this.emit('delete', file)) {
            this._options.fs.unlink(file.path.toString('utf-8')).catch(() => {
                // Do nothing on error
            });
        }
    }
}
/**
 * Limited FIFO queue. Each time the capacity is exceeded, the first element is removed
 * and `onShift` is called with the removed element.
 * @param capacity Maximum capacity.
 */
function limitedFifo(capacity, onShift) {
    const elements = [];
    function push(element) {
        elements.push(element);
        if (elements.length > capacity) {
            const first = elements.shift();
            if (first) {
                onShift(first);
            }
        }
    }
    return { elements: elements, push };
}

/**
 * Splits data into chunks with maximum length.
 * @param maxLength Maximum length of one chunk.
 * @param wholeLines Can be one of:
 * * `"skip"` - if last line does not fit in the chunk, it will be skipped entirely
 * * `"break"` - if last line does not fit in the chunk, it will be broken into two new chunks
 * * `false` - last line will be always broken into old and new chunk
 */
function lengthChunkSplitter(maxLength, wholeLines = false) {
    let seen = 0;
    const emptyBuffer = Buffer.of();
    return function lengthChunkSplitter(data) {
        const remainingLength = maxLength - seen;
        if (data.length <= remainingLength) {
            seen += data.length;
            return [data];
        }
        seen = 0;
        if (!wholeLines) {
            return [data.subarray(0, remainingLength), data.subarray(remainingLength)];
        }
        // Check last newline before first chunk end
        const lastLineIndex = data.subarray(0, remainingLength).lastIndexOf('\n');
        // If there is no newline, pass empty buffer as the first chunk
        // and write all data into the second
        if (lastLineIndex === -1) {
            if (remainingLength !== maxLength) {
                return [emptyBuffer, data];
            }
            if (wholeLines === 'break') {
                // Break the line into two chunks
                return [data.subarray(0, remainingLength), data.subarray(remainingLength)];
            }
            else {
                const firstNewLine = data.indexOf('\n', remainingLength);
                if (firstNewLine === -1) {
                    return [emptyBuffer];
                }
                return [emptyBuffer, data.subarray(firstNewLine + 1)];
            }
        }
        // +1 - include trailing newline in first chunk, skip in second
        return [data.subarray(0, lastLineIndex + 1), data.subarray(lastLineIndex + 1)];
    };
}

/**
 * Splits data into chunks with maximum lines.
 * @param maxLines Maximum lines in one chunk.
 */
function lineChunkSplitter(maxLines) {
    let seen = 0;
    function findNthLine(data, remaining) {
        let lastIndex = -1;
        let count = 0;
        // eslint-disable-next-line no-constant-condition
        while (true) {
            lastIndex = data.indexOf('\n', lastIndex + 1);
            if (lastIndex === -1) {
                return [-1, count];
            }
            if (remaining === ++count) {
                return [lastIndex + 1, count];
            }
        }
    }
    return function lineChunkSplitter(data) {
        const remainingLines = maxLines - seen;
        const [index, count] = findNthLine(data, remainingLines);
        if (index === -1) {
            seen += count;
            return [data];
        }
        seen = 0;
        return [data.subarray(0, index), data.subarray(index)];
    };
}

const FILE_PREFIX = 'bt-breadcrumbs';
class FileBreadcrumbsStorage {
    get lastBreadcrumbId() {
        return this._lastBreadcrumbId;
    }
    constructor(session, _fileSystem, _limits) {
        this._fileSystem = _fileSystem;
        this._limits = _limits;
        this._lastBreadcrumbId = TimeHelper.toTimestampInSec(TimeHelper.now());
        const splitters = [];
        const maximumBreadcrumbs = this._limits.maximumBreadcrumbs;
        if (maximumBreadcrumbs !== undefined) {
            splitters.push(() => lineChunkSplitter(Math.ceil(maximumBreadcrumbs / 2)));
        }
        const maximumTotalBreadcrumbsSize = this._limits.maximumTotalBreadcrumbsSize;
        if (maximumTotalBreadcrumbsSize !== undefined) {
            splitters.push(() => lengthChunkSplitter(Math.ceil(maximumTotalBreadcrumbsSize), 'skip'));
        }
        this._sink = new FileChunkSink({
            maxFiles: 2,
            fs: this._fileSystem,
            file: (n) => session.getFileName(FileBreadcrumbsStorage.getFileName(n)),
        });
        if (!splitters.length) {
            this._dest = this._sink.getSink()(0);
        }
        else {
            this._dest = chunkifier({
                sink: this._sink.getSink(),
                splitter: splitters.length === 1 ? splitters[0] : () => combinedChunkSplitter(...splitters.map((s) => s())),
            });
        }
        this._dest.on('error', () => {
            // Do nothing on error
        });
    }
    static getSessionAttachments(session, fileSystem) {
        const files = session
            .getSessionFiles()
            .filter((f) => path.basename(f).startsWith(FILE_PREFIX))
            .slice(0, 2);
        return files.map((file) => new BacktraceFileAttachment(file, path.basename(file), fileSystem));
    }
    static factory(session, fileSystem) {
        return ({ limits }) => new FileBreadcrumbsStorage(session, fileSystem, limits);
    }
    getAttachments() {
        const files = [...this._sink.files].map((f) => f.path.toString('utf-8'));
        return files.map((f) => new BacktraceFileAttachment(f, path.basename(f), this._fileSystem));
    }
    getAttachmentProviders() {
        return [
            {
                get: () => this.getAttachments(),
                type: 'dynamic',
            },
        ];
    }
    add(rawBreadcrumb) {
        this._lastBreadcrumbId++;
        const id = this._lastBreadcrumbId;
        const breadcrumb = {
            id,
            message: rawBreadcrumb.message,
            timestamp: TimeHelper.now(),
            type: BreadcrumbType[rawBreadcrumb.type].toLowerCase(),
            level: BreadcrumbLogLevel[rawBreadcrumb.level].toLowerCase(),
            attributes: rawBreadcrumb.attributes,
        };
        const breadcrumbJson = JSON.stringify(breadcrumb, jsonEscaper());
        const jsonLength = breadcrumbJson.length + 1; // newline
        const sizeLimit = this._limits.maximumTotalBreadcrumbsSize;
        if (sizeLimit !== undefined) {
            if (jsonLength > sizeLimit) {
                return undefined;
            }
        }
        this._dest.write(breadcrumbJson + '\n');
        return id;
    }
    static getFileName(index) {
        return `${FILE_PREFIX}-${index}`;
    }
}

class ApplicationInformationAttributeProvider {
    get type() {
        return 'scoped';
    }
    constructor(applicationSearchPaths, nodeConfiguration) {
        var _a, _b;
        if (nodeConfiguration === void 0) { nodeConfiguration = {
            application: (_a = process$1.env) === null || _a === void 0 ? void 0 : _a.npm_package_name,
            version: (_b = process$1.env) === null || _b === void 0 ? void 0 : _b.npm_package_version,
        }; }
        this.APPLICATION_ATTRIBUTE = 'application';
        this.APPLICATION_VERSION_ATTRIBUTE = 'application.version';
        this._application = nodeConfiguration === null || nodeConfiguration === void 0 ? void 0 : nodeConfiguration.application;
        this._applicationVersion = nodeConfiguration === null || nodeConfiguration === void 0 ? void 0 : nodeConfiguration.version;
        this.applicationSearchPaths = applicationSearchPaths !== null && applicationSearchPaths !== void 0 ? applicationSearchPaths : this.generateDefaultApplicationSearchPaths();
    }
    get() {
        var _a, _b;
        const applicationData = this.readApplicationInformation();
        if (applicationData) {
            this._application = (_a = this._application) !== null && _a !== void 0 ? _a : applicationData['name'];
            this._applicationVersion = (_b = this._applicationVersion) !== null && _b !== void 0 ? _b : applicationData['version'];
        }
        return {
            package: applicationData,
            [this.APPLICATION_ATTRIBUTE]: this._application,
            [this.APPLICATION_VERSION_ATTRIBUTE]: this._applicationVersion,
        };
    }
    generateDefaultApplicationSearchPaths() {
        var _a;
        const possibleSourcePaths = [process$1.cwd(), this.generatePathBasedOnTheDirName()];
        const potentialCommandLineStartupFile = process$1.argv[1];
        if (potentialCommandLineStartupFile) {
            const potentialCommandLineStartupFilePath = path.resolve(potentialCommandLineStartupFile);
            if (fs.existsSync(potentialCommandLineStartupFilePath)) {
                possibleSourcePaths.unshift(potentialCommandLineStartupFilePath);
            }
        }
        if (typeof require !== 'undefined' && ((_a = require.main) === null || _a === void 0 ? void 0 : _a.path)) {
            possibleSourcePaths.unshift(path.dirname(require.main.path));
        }
        return possibleSourcePaths;
    }
    generatePathBasedOnTheDirName() {
        const dirname = fileURLToPath(path.dirname(import.meta.url));
        const nodeModulesIndex = dirname.lastIndexOf('node_modules');
        if (nodeModulesIndex === -1) {
            return dirname;
        }
        return dirname.substring(0, nodeModulesIndex);
    }
    readApplicationInformation() {
        for (let possibleSourcePath of this.applicationSearchPaths) {
            // to make sure we check all directories including `/` we want to assign the parent
            // directory to the current path after checking if the parent directory is equal the current
            // directory. Because of that in the while condition, there is an assignement to the
            // current directory to check next dir in the dir strucutre.
            do {
                const packageJson = this.readPackageFromDir(possibleSourcePath);
                if (packageJson) {
                    return packageJson;
                }
            } while (possibleSourcePath !== path.dirname(possibleSourcePath) &&
                (possibleSourcePath = path.dirname(possibleSourcePath)));
        }
    }
    readPackageFromDir(dirPath) {
        const packagePath = path.join(dirPath, 'package.json');
        if (!fs.existsSync(packagePath)) {
            return undefined;
        }
        return JSON.parse(fs.readFileSync(packagePath, 'utf8'));
    }
}

class UnitConverter {
    static parseKb(str) {
        return parseInt(str, 10) * 1024;
    }
}

const MEMORY_INFORMATION_REGEX = /^(.+):\s+(\d+)\s*(.+)?$/;
const MEMORY_ATTRIBUTE_MAP = {
    MemTotal: 'system.memory.total',
    MemFree: 'system.memory.free',
    MemAvailable: 'system.memory.available',
    Buffers: 'system.memory.buffers',
    Cached: 'system.memory.cached',
    SwapCached: 'system.memory.swap.cached',
    Active: 'system.memory.active',
    Inactive: 'system.memory.inactive',
    SwapTotal: 'system.memory.swap.total',
    SwapFree: 'system.memory.swap.free',
    Dirty: 'system.memory.dirty',
    Writeback: 'system.memory.writeback',
    Slab: 'system.memory.slab',
    VmallocTotal: 'system.memory.vmalloc.total',
    VmallocUsed: 'system.memory.vmalloc.used',
    VmallocChunk: 'system.memory.vmalloc.chunk',
};
const PROCESS_STATUS_MAP = [
    {
        re: /^nonvoluntary_ctxt_switches:\s+(\d+)$/m,
        parse: parseInt,
        attr: 'sched.cs.involuntary',
    },
    {
        re: /^voluntary_ctxt_switches:\s+(\d+)$/m,
        parse: parseInt,
        attr: 'sched.cs.voluntary',
    },
    { re: /^FDSize:\s+(\d+)$/m, parse: parseInt, attr: 'descriptor.count' },
    { re: /^FDSize:\s+(\d+)$/m, parse: parseInt, attr: 'descriptor.count' },
    { re: /^VmData:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.data.size' },
    { re: /^VmLck:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.locked.size' },
    { re: /^VmPTE:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.pte.size' },
    { re: /^VmHWM:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.rss.peak' },
    { re: /^VmRSS:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.rss.size' },
    { re: /^VmLib:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.shared.size' },
    { re: /^VmStk:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.stack.size' },
    { re: /^VmSwap:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.swap.size' },
    { re: /^VmPeak:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.vma.peak' },
    { re: /^VmSize:\s+(\d+)\s+kB$/m, parse: UnitConverter.parseKb, attr: 'vm.vma.size' },
];

class LinuxProcessStatusAttributeProvider {
    constructor() {
        this._isLinux = process.platform === 'linux';
    }
    get type() {
        return this._isLinux ? 'dynamic' : 'scoped';
    }
    get() {
        if (!this._isLinux) {
            return {};
        }
        const memoryInformation = this.getMemoryInformation();
        const processInformation = this.getProcessStatus();
        return {
            ...memoryInformation,
            ...processInformation,
        };
    }
    getMemoryInformation() {
        const result = {};
        let file = '';
        try {
            file = fs.readFileSync('/proc/meminfo', { encoding: 'utf8' });
        }
        catch (err) {
            return {};
        }
        const lines = file.trim().split('\n');
        for (const line of lines) {
            const match = line.match(MEMORY_INFORMATION_REGEX);
            if (!match) {
                continue;
            }
            const name = match[1];
            const attrName = MEMORY_ATTRIBUTE_MAP[name];
            if (!attrName) {
                continue;
            }
            let number = parseInt(match[2], 10);
            const units = match[3];
            if (units === 'kB') {
                number *= 1024;
            }
            result[attrName] = number;
        }
        return result;
    }
    getProcessStatus() {
        // Justification for doing this synchronously:
        // * We need to collect this information in the process uncaughtException handler, in which the
        //   event loop is not safe to use.
        // * We are collecting a snapshot of virtual memory used. If this is done asynchronously, then
        //   we may pick up virtual memory information for a time different than the moment we are
        //   interested in.
        // * procfs is a virtual filesystem; there is no disk I/O to block on. It's synchronous anyway.
        let contents;
        try {
            contents = fs.readFileSync('/proc/self/status', { encoding: 'utf8' });
        }
        catch (err) {
            return {};
        }
        const result = {};
        for (let i = 0; i < PROCESS_STATUS_MAP.length; i += 1) {
            const item = PROCESS_STATUS_MAP[i];
            const match = contents.match(item.re);
            if (!match) {
                continue;
            }
            result[item.attr] = item.parse(match[1], 10);
        }
        return result;
    }
}

class MachineAttributeProvider {
    get type() {
        return 'scoped';
    }
    get() {
        const cpus = os.cpus();
        return {
            'cpu.arch': os.arch(),
            'cpu.boottime': Math.floor(Date.now() / 1000) - os.uptime(),
            'cpu.count': cpus.length,
            'cpu.brand': cpus[0].model,
            'cpu.frequency': cpus[0].speed,
            'system.memory.total': os.totalmem(),
            hostname: os.hostname(),
            'uname.version': os.release(),
            'Environment Variables': process.env,
        };
    }
}

class MachineIdentitfierAttributeProvider {
    constructor() {
        this.MACHINE_ID_ATTRIBUTE = 'guid';
        this.COMMANDS = {
            win32: 'reg query "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Cryptography" /v MachineGuid',
            darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
            linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :',
            freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid',
        };
    }
    get type() {
        return 'scoped';
    }
    get() {
        var _a;
        const guid = (_a = this.generateGuid()) !== null && _a !== void 0 ? _a : IdGenerator.uuid();
        return {
            [this.MACHINE_ID_ATTRIBUTE]: guid,
        };
    }
    generateGuid() {
        var _a;
        switch (process.platform) {
            case 'win32': {
                return (_a = execSync(this.COMMANDS['win32'])
                    .toString()
                    .match(/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/i)) === null || _a === void 0 ? void 0 : _a[0].toLowerCase();
            }
            case 'darwin': {
                return execSync(this.COMMANDS[process.platform])
                    .toString()
                    .split('IOPlatformUUID')[1]
                    .split('\n')[0]
                    .replace(/=|\s+|"/gi, '')
                    .toLowerCase();
            }
            case 'linux':
            case 'freebsd': {
                return execSync(this.COMMANDS[process.platform])
                    .toString()
                    .replace(/\r+|\n+|\s+/gi, '')
                    .toLowerCase();
            }
            default: {
                return null;
            }
        }
    }
}
MachineIdentitfierAttributeProvider.SUPPORTED_PLATFORMS = ['win32', 'darwin', 'linux', 'freebsd'];

class ProcessInformationAttributeProvider {
    get type() {
        return 'scoped';
    }
    get() {
        return {
            'process.thread.count': 1,
            'process.cwd': process.cwd(),
            pid: process.pid,
            'uname.machine': process.arch,
            'uname.sysname': this.convertPlatformToAttribute(process.platform),
            environment: process.env.NODE_ENV,
            'debug.port': process.debugPort,
            'Exec Arguments': process.execArgv,
        };
    }
    convertPlatformToAttribute(platform) {
        switch (platform) {
            case 'win32': {
                return 'Windows';
            }
            case 'darwin': {
                return 'Mac OS';
            }
            default: {
                return platform.charAt(0).toUpperCase() + platform.slice(1);
            }
        }
    }
}

class ProcessStatusAttributeProvider {
    get type() {
        return 'dynamic';
    }
    get() {
        const processMemoryUsage = process.memoryUsage();
        const result = {
            'vm.rss.size': processMemoryUsage.rss,
            'gc.heap.total': processMemoryUsage.heapTotal,
            'gc.heap.used': processMemoryUsage.heapUsed,
            'process.age': Math.floor(process.uptime()),
            'system.memory.free': os.freemem(),
        };
        return result;
    }
}

class BacktraceClientBuilder extends BacktraceCoreClientBuilder {
    constructor(clientSetup) {
        var _a;
        super({
            ...clientSetup,
            options: { ...clientSetup.options, attachments: (_a = clientSetup.options.attachments) === null || _a === void 0 ? void 0 : _a.map(transformAttachment) },
        });
        this.addAttributeProvider(new ApplicationInformationAttributeProvider());
        this.addAttributeProvider(new ProcessStatusAttributeProvider());
        this.addAttributeProvider(new MachineAttributeProvider());
        this.addAttributeProvider(new ProcessInformationAttributeProvider());
        this.addAttributeProvider(new LinuxProcessStatusAttributeProvider());
        this.addAttributeProvider(new MachineIdentitfierAttributeProvider());
    }
    build() {
        const instance = new BacktraceClient(this.clientSetup);
        instance.initialize();
        return instance;
    }
}

class NodeOptionReader {
    /**
     * Read option based on the option name. If the option doesn't start with `--`
     * additional prefix will be added.
     * @param optionName option name
     * @returns option value
     */
    static read(optionName, argv = process.execArgv, nodeOptions = process.env['NODE_OPTIONS']) {
        /**
         * exec argv overrides NODE_OPTIONS.
         * for example:
         * yarn start = NODEW_ENV=production node --unhandled-rejections=none ./lib/index.js
         * NODE_OPTIONS='--unhandled-rejections=throw' yarn start
         *
         * Even if NODE_OPTIONS have unhandled rejections set to throw, the value passed in argv
         * will be used.
         */
        if (!optionName.startsWith('--')) {
            optionName = '--' + optionName;
        }
        const commandOption = argv.find((n) => n.startsWith(optionName));
        function readOptionValue(optionName, commandOption) {
            let result = commandOption.substring(optionName.length);
            if (!result) {
                return true;
            }
            if (result.startsWith('=')) {
                result = result.substring(1);
            }
            return result;
        }
        if (commandOption) {
            return readOptionValue(optionName, commandOption);
        }
        if (!nodeOptions) {
            return undefined;
        }
        const nodeOption = nodeOptions.split(' ').find((n) => n.startsWith(optionName));
        if (!nodeOption) {
            return undefined;
        }
        return readOptionValue(optionName, nodeOption);
    }
}

class NodeDiagnosticReportConverter {
    convert(report) {
        const jsStack = report.javascriptStack.stack;
        const validJsStack = jsStack && jsStack[0] !== 'Unavailable.';
        const message = validJsStack ? report.javascriptStack.message : report.header.event;
        const btReport = new BacktraceReport(message, {
            hostname: report.header.host,
            ...this.getUnameData(report),
            ...this.getCpuData(report),
            ...this.getMemoryData(report),
            // Annotations
            'Environment Variables': report.environmentVariables,
            'Exec Arguments': report.header.commandLine,
            Error: report,
        }, [], {
            timestamp: parseInt(report.header.dumpEventTimeStamp),
            classifiers: [report.header.trigger],
        });
        if (validJsStack) {
            btReport.addStackTrace('main', jsStack.join('\n'));
        }
        const nativeStack = report.nativeStack;
        if (nativeStack) {
            const nativeFrames = nativeStack.map((frame) => ({
                funcName: frame.symbol,
                library: 'v8',
            }));
            // If the JS stack is valid, add as 'native', otherwise 'main'
            const threadName = validJsStack ? 'native' : 'main';
            btReport.addStackTrace(threadName, nativeFrames);
        }
        const isOom = report.header.event === 'Allocation failed - JavaScript heap out of memory';
        const errorType = isOom ? 'OOMException' : 'Crash';
        btReport.attributes['error.type'] = errorType;
        return btReport;
    }
    getUnameData(report) {
        return {
            'uname.sysname': report.header.osName,
            'uname.release': report.header.osRelease,
            'uname.version': report.header.osVersion,
            'uname.machine': report.header.osMachine,
        };
    }
    getCpuData(report) {
        const cpu = report.header.cpus[0];
        return {
            'cpu.arch': report.header.arch,
            'cpu.brand': cpu.model,
            'cpu.frequency': cpu.speed,
            'cpu.user': cpu.user,
            'cpu.nice': cpu.nice,
            'cpu.sys': cpu.sys,
            'cpu.idle': cpu.idle,
            'cpu.irq': cpu.irq,
            'cpu.count': report.header.cpus.length,
        };
    }
    getMemoryData(report) {
        return {
            'vm.rss.size': Math.round(report.javascriptHeap.usedMemory / 1024),
            'vm.rss.peak': Math.round(report.javascriptHeap.totalMemory / 1024),
            'vm.rss.available': report.javascriptHeap.availableMemory,
        };
    }
}

class FsNodeFileSystem {
    readDir(dir) {
        return fs.promises.readdir(dir);
    }
    readDirSync(dir) {
        return fs.readdirSync(dir);
    }
    createDir(dir) {
        return fs.promises.mkdir(dir, { recursive: true });
    }
    createDirSync(dir) {
        fs.mkdirSync(dir, { recursive: true });
    }
    readFile(path) {
        return fs.promises.readFile(path, 'utf-8');
    }
    readFileSync(path) {
        return fs.readFileSync(path, 'utf-8');
    }
    writeFile(path, content) {
        return fs.promises.writeFile(path, content);
    }
    writeFileSync(path, content) {
        fs.writeFileSync(path, content);
    }
    unlink(path) {
        return fs.promises.unlink(path);
    }
    unlinkSync(path) {
        fs.unlinkSync(path);
    }
    rename(oldPath, newPath) {
        return fs.promises.rename(oldPath, newPath);
    }
    renameSync(oldPath, newPath) {
        fs.renameSync(oldPath, newPath);
    }
    createWriteStream(path) {
        return fs.createWriteStream(path, 'utf-8');
    }
    createReadStream(path) {
        return fs.createReadStream(path, 'utf-8');
    }
    async exists(path) {
        try {
            await fs.promises.stat(path);
            return true;
        }
        catch (_a) {
            return false;
        }
    }
    existsSync(path) {
        return fs.existsSync(path);
    }
    createAttachment(path, name) {
        return new BacktraceFileAttachment(path, name);
    }
}

class BacktraceClient extends BacktraceCoreClient {
    get nodeFileSystem() {
        return this.fileSystem;
    }
    constructor(clientSetup) {
        var _a, _b, _c;
        const fileSystem = (_a = clientSetup.fileSystem) !== null && _a !== void 0 ? _a : new FsNodeFileSystem();
        super({
            sdkOptions: AGENT,
            requestHandler: new BacktraceNodeRequestHandler(clientSetup.options),
            debugIdMapProvider: new VariableDebugIdMapProvider(global),
            ...clientSetup,
            fileSystem,
            options: {
                ...clientSetup.options,
                attachments: (_b = clientSetup.options.attachments) === null || _b === void 0 ? void 0 : _b.map(transformAttachment),
            },
        });
        this._listeners = {};
        const breadcrumbsManager = this.modules.get(BreadcrumbsManager);
        if (breadcrumbsManager && this.sessionFiles) {
            breadcrumbsManager.setStorage(FileBreadcrumbsStorage.factory(this.sessionFiles, fileSystem));
        }
        if (this.sessionFiles && ((_c = clientSetup.options.database) === null || _c === void 0 ? void 0 : _c.captureNativeCrashes)) {
            this.addModule(FileAttributeManager, FileAttributeManager.create(fileSystem));
            this.addModule(FileAttachmentsManager, FileAttachmentsManager.create(fileSystem));
        }
    }
    initialize() {
        var _a, _b;
        const lockId = (_a = this.sessionFiles) === null || _a === void 0 ? void 0 : _a.lockPreviousSessions();
        try {
            super.initialize();
            this.captureUnhandledErrors(this.options.captureUnhandledErrors, this.options.captureUnhandledPromiseRejections);
            this.captureNodeCrashes();
        }
        catch (err) {
            lockId && ((_b = this.sessionFiles) === null || _b === void 0 ? void 0 : _b.unlockPreviousSessions(lockId));
            throw err;
        }
        this.loadNodeCrashes().finally(() => { var _a; return lockId && ((_a = this.sessionFiles) === null || _a === void 0 ? void 0 : _a.unlockPreviousSessions(lockId)); });
    }
    static builder(options) {
        return new BacktraceClientBuilder({ options });
    }
    /**
     * Initializes the client. If the client already exists, the available instance
     * will be returned and all other options will be ignored.
     * @param options client configuration
     * @param build builder
     * @returns backtrace client
     */
    static initialize(options, build) {
        if (this.instance) {
            return this.instance;
        }
        const builder = this.builder(options);
        build && build(builder);
        this._instance = builder.build();
        return this._instance;
    }
    /**
     * Returns created BacktraceClient instance if the instance exists.
     * Otherwise undefined.
     */
    static get instance() {
        return this._instance;
    }
    /**
     * Disposes the client and all client callbacks
     */
    dispose() {
        for (const [name, listener] of Object.entries(this._listeners)) {
            process.removeListener(name, listener);
        }
        super.dispose();
        BacktraceClient._instance = undefined;
    }
    captureUnhandledErrors(captureUnhandledExceptions = true, captureUnhandledRejections = true) {
        if (!captureUnhandledExceptions && !captureUnhandledRejections) {
            return;
        }
        const captureUncaughtException = async (error, origin) => {
            if (origin === 'unhandledRejection' && !captureUnhandledRejections) {
                return;
            }
            if (origin === 'uncaughtException' && !captureUnhandledExceptions) {
                return;
            }
            await this.send(new BacktraceReport(error, { 'error.type': 'Unhandled exception', errorOrigin: origin }, [], {
                classifiers: origin === 'unhandledRejection' ? ['UnhandledPromiseRejection'] : undefined,
            }));
        };
        process.prependListener('uncaughtExceptionMonitor', captureUncaughtException);
        this._listeners['uncaughtExceptionMonitor'] = captureUncaughtException;
        if (!captureUnhandledRejections) {
            return;
        }
        // Node 15+ has changed the default unhandled promise rejection behavior.
        // In node 14 - the default behavior is to warn about unhandled promise rejections. In newer version
        // the default mode is throw.
        const nodeMajorVersion = process.version.split('.')[0];
        const unhandledRejectionMode = NodeOptionReader.read('unhandled-rejections');
        const traceWarnings = NodeOptionReader.read('trace-warnings');
        /**
         * Node JS allows to use only uncaughtExceptionMonitor only when:
         * - we're in the throw/strict error mode
         * - the node version 15+
         *
         * In other scenarios we need to capture unhandledRejections via other event.
         */
        const ignoreUnhandledRejectionHandler = unhandledRejectionMode === 'strict' ||
            unhandledRejectionMode === 'throw' ||
            (nodeMajorVersion !== 'v14' && !unhandledRejectionMode);
        if (ignoreUnhandledRejectionHandler) {
            return;
        }
        const captureUnhandledRejectionsCallback = async (reason) => {
            var _a, _b, _c, _d;
            const isErrorTypeReason = reason instanceof Error;
            await this.send(new BacktraceReport(isErrorTypeReason ? reason : ((_a = reason === null || reason === void 0 ? void 0 : reason.toString()) !== null && _a !== void 0 ? _a : 'Unhandled rejection'), {
                'error.type': 'Unhandled exception',
            }, [], {
                classifiers: ['UnhandledPromiseRejection'],
                skipFrames: isErrorTypeReason ? 0 : 1,
            }));
            const error = isErrorTypeReason ? reason : new Error((_b = reason === null || reason === void 0 ? void 0 : reason.toString()) !== null && _b !== void 0 ? _b : 'Unhandled rejection');
            // if there is any other unhandled rejection handler, reproduce default node behavior
            // and let other handlers to capture the event
            if (process.listenerCount('unhandledRejection') !== 1) {
                return;
            }
            // everything else will be handled by node
            if (unhandledRejectionMode === 'none' || unhandledRejectionMode === 'warn') {
                return;
            }
            // handle last status: warn-with-error-code
            process.exitCode = 1;
            const unhandledRejectionErrName = 'UnhandledPromiseRejectionWarning';
            process.emitWarning((_c = (isErrorTypeReason ? error.stack : reason === null || reason === void 0 ? void 0 : reason.toString())) !== null && _c !== void 0 ? _c : '', unhandledRejectionErrName);
            const warning = new Error(`Unhandled promise rejection. This error originated either by ` +
                `throwing inside of an async function without a catch block, ` +
                `or by rejecting a promise which was not handled with .catch(). ` +
                `To terminate the node process on unhandled promise ` +
                'rejection, use the CLI flag `--unhandled-rejections=strict` (see ' +
                'https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). ');
            Object.defineProperty(warning, 'name', {
                value: 'UnhandledPromiseRejectionWarning',
                enumerable: false,
                writable: true,
                configurable: true,
            });
            warning.stack = traceWarnings && isErrorTypeReason ? ((_d = error.stack) !== null && _d !== void 0 ? _d : '') : '';
            process.emitWarning(warning);
        };
        process.prependListener('unhandledRejection', captureUnhandledRejectionsCallback);
        this._listeners['unhandledRejection'] = captureUnhandledRejectionsCallback;
    }
    captureNodeCrashes() {
        var _a, _b;
        if (!process.report) {
            return;
        }
        if (!((_a = this.options.database) === null || _a === void 0 ? void 0 : _a.enable)) {
            return;
        }
        if (!((_b = this.options.database) === null || _b === void 0 ? void 0 : _b.captureNativeCrashes)) {
            return;
        }
        process.report.reportOnFatalError = true;
        if (!process.report.directory) {
            process.report.directory = this.options.database.path;
        }
    }
    async loadNodeCrashes() {
        var _a, _b, _c, _d, _e, _f;
        if (!this.database || !this.nodeFileSystem || !((_a = this.options.database) === null || _a === void 0 ? void 0 : _a.captureNativeCrashes)) {
            return;
        }
        const reportName = (_b = process.report) === null || _b === void 0 ? void 0 : _b.filename;
        const databasePath = ((_c = process.report) === null || _c === void 0 ? void 0 : _c.directory)
            ? process.report.directory
            : ((_e = (_d = this.options.database) === null || _d === void 0 ? void 0 : _d.path) !== null && _e !== void 0 ? _e : process.cwd());
        let databaseFiles;
        try {
            databaseFiles = await this.nodeFileSystem.readDir(databasePath);
        }
        catch (_g) {
            return;
        }
        const converter = new NodeDiagnosticReportConverter();
        const recordNames = databaseFiles.filter((file) => 
        // If the user specifies a preset name for reports, we should compare it directly
        // Otherwise, match the default name
        reportName ? file === reportName : file.startsWith('report.') && file.endsWith('.json'));
        if (!recordNames.length) {
            return;
        }
        const reports = [];
        for (const recordName of recordNames) {
            const recordPath = path.join(databasePath, recordName);
            try {
                const recordJson = await this.nodeFileSystem.readFile(recordPath);
                const report = converter.convert(JSON.parse(recordJson));
                reports.push([recordPath, report]);
            }
            catch (_h) {
                // Do nothing, skip the report
            }
        }
        // Sort reports by timestamp descending
        reports.sort((a, b) => b[1].timestamp - a[1].timestamp);
        // Map reports to sessions
        // When the sessions are sorted by timestamp, we can assume that each previous session maps to the next report
        let currentSession = (_f = this.sessionFiles) === null || _f === void 0 ? void 0 : _f.getPreviousSession();
        for (const tuple of reports) {
            tuple[2] = currentSession;
            currentSession = currentSession === null || currentSession === void 0 ? void 0 : currentSession.getPreviousSession();
        }
        for (const [recordPath, report, session] of reports) {
            try {
                if (session) {
                    report.attachments.push(...FileBreadcrumbsStorage.getSessionAttachments(session, this.nodeFileSystem));
                    const fileAttributes = FileAttributeManager.createFromSession(session, this.nodeFileSystem);
                    Object.assign(report.attributes, await fileAttributes.get());
                    const fileAttachments = FileAttachmentsManager.createFromSession(session, this.nodeFileSystem);
                    report.attachments.push(...(await fileAttachments.get()));
                    report.attributes['application.session'] = session.sessionId;
                }
                else {
                    report.attributes['application.session'] = null;
                }
                const data = this.generateSubmissionData(report);
                if (data) {
                    this.database.add(data, report.attachments);
                }
            }
            catch (_j) {
                // Do nothing, skip the report
            }
            finally {
                try {
                    await this.nodeFileSystem.unlink(recordPath);
                }
                catch (_k) {
                    // Do nothing
                }
            }
        }
        await this.database.send();
    }
}

export { BacktraceApi, BacktraceBufferAttachment, BacktraceClient, BacktraceClientBuilder, BacktraceFileAttachment, BacktraceNodeRequestHandler, FileAttachmentsManager, FileBreadcrumbsStorage, FsNodeFileSystem };
//# sourceMappingURL=bundle.mjs.map