@backtrace/node
Version:
Backtrace-JavaScript Node.JS integration
1,288 lines (1,261 loc) • 56 kB
JavaScript
'use strict';
var sdkCore = require('@backtrace/sdk-core');
var fs = require('fs');
var path = require('path');
var FormData = require('form-data');
var http = require('http');
var https = require('https');
var stream = require('stream');
var EventEmitter = require('events');
var process$1 = require('process');
var url = require('url');
var os = require('os');
var child_process = require('child_process');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
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 : sdkCore.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 sdkCore.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 stream.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 sdkCore.BacktraceReportSubmissionResult.Ok(JSON.parse(result));
}
case 401:
case 403: {
return sdkCore.BacktraceReportSubmissionResult.OnInvalidToken();
}
case 429: {
return sdkCore.BacktraceReportSubmissionResult.OnLimitReached();
}
default: {
return sdkCore.BacktraceReportSubmissionResult.OnInternalServerError(result);
}
}
}
handleRequestError(err) {
if (sdkCore.ConnectionError.isConnectionError(err)) {
return sdkCore.BacktraceReportSubmissionResult.OnNetworkingError(err.message);
}
return sdkCore.BacktraceReportSubmissionResult.OnInternalServerError(err.message);
}
handleError(err) {
if (sdkCore.ConnectionError.isConnectionError(err)) {
return sdkCore.BacktraceReportSubmissionResult.OnNetworkingError(err.message);
}
const errorMessage = err instanceof Error ? err.message : err;
return sdkCore.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 sdkCore.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 stream.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 = sdkCore.TimeHelper.toTimestampInSec(sdkCore.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: sdkCore.TimeHelper.now(),
type: sdkCore.BreadcrumbType[rawBreadcrumb.type].toLowerCase(),
level: sdkCore.BreadcrumbLogLevel[rawBreadcrumb.level].toLowerCase(),
attributes: rawBreadcrumb.attributes,
};
const breadcrumbJson = JSON.stringify(breadcrumb, sdkCore.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 = url.fileURLToPath(path.dirname((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('bundle.cjs', document.baseURI).href))));
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 : sdkCore.IdGenerator.uuid();
return {
[this.MACHINE_ID_ATTRIBUTE]: guid,
};
}
generateGuid() {
var _a;
switch (process.platform) {
case 'win32': {
return (_a = child_process.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 child_process.execSync(this.COMMANDS[process.platform])
.toString()
.split('IOPlatformUUID')[1]
.split('\n')[0]
.replace(/=|\s+|"/gi, '')
.toLowerCase();
}
case 'linux':
case 'freebsd': {
return child_process.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 sdkCore.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 sdkCore.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 sdkCore.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 sdkCore.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(sdkCore.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(sdkCore.FileAttributeManager, sdkCore.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 sdkCore.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 sdkCore.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,
});
warnin