UNPKG

@backtrace/node

Version:
1,287 lines (1,261 loc) 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 : '') : ''; pro