backtrace-node
Version:
Backtrace error reporting tool
333 lines (292 loc) • 10.1 kB
text/typescript
import { pseudoRandomBytes } from 'crypto';
import { APP_NAME, LANG, THREAD, VERSION } from '../const/application';
import { machineIdSync } from '../helpers/machineId';
import { readModule, readSystemAttributes } from '../helpers/moduleResolver';
import { readMemoryInformation, readProcessStatus } from '../helpers/processHelper';
import { currentTimestamp } from '../utils';
import { IBacktraceData } from './backtraceData';
import { BacktraceStackTrace } from './backtraceStackTrace';
/**
* BacktraceReport describe current exception/message payload message to Backtrace
*/
export class BacktraceReport {
public static machineId = machineIdSync(true);
public set symbolication(symbolication: boolean) {
this._symbolication = symbolication;
}
public set symbolicationMap(symbolMap: Array<{ file: string; uuid: string }>) {
if (!symbolMap) {
throw new Error('Symbolication map is undefined');
}
if (!Array.isArray(symbolMap)) {
throw new TypeError('Invalid type of symbolication map');
}
const invalidValues = symbolMap.some((n) => !n.file || !n.uuid);
if (invalidValues) {
throw new TypeError('Symbolication map contains invalid values - missing file or uuid value');
}
this._symbolicationMap = symbolMap;
}
// reprot id
public readonly uuid: string = this.generateUuid();
// timestamp
public readonly timestamp: number = currentTimestamp();
// lang
public readonly lang = LANG;
// environment version
public readonly langVersion = process.version;
// Backtrace-ndoe name
public readonly agent = APP_NAME;
// Backtrace-node version
public readonly agentVersion= VERSION;
// main thread name
public readonly mainThread = THREAD;
public classifiers: string[] = [];
/**
* @deprecated
* Please use client.sendReport instead
* BacktraceReport generated by library allows you to
* automatically send reports to Backtrace via send method
*/
public send?: (callback: (err?: Error) => void) => void;
/**
* @deprecated
* Please use client.sendReport instead
* BacktraceReport generated by library allows you to
* automatically send reports to Backtrace via send method
*/
public sendSync?: (callback: (err?: Error) => void) => void;
/**
* Thread information about current application
*/
public stackTrace!: BacktraceStackTrace;
private _symbolicationMap?: Array<{ file: string; uuid: string }>;
private _symbolication = false;
/**
* Current report attributes
*/
private attributes: { [index: string]: any } = {};
/**
* Backtrace complex objet
*/
private annotations: { [index: string]: any } = {};
/**
* Calling module information
*/
private _callingModule!: NodeRequire;
private _callingModulePath!: string;
private tabWidth: number = 8;
private contextLineCount: number = 200;
private err!: Error;
/**
* Create new BacktraceReport - report information that will collect information
* for Backtrace.
*
* Possible existing scenarios:
* arg1: error + arg2: attributes = all required
* arg1: object, arg2: nothing
*
* @param err Error or message - content to report
* @param attributes Report attributes dictionary
* @param attachments Report attachments that Backtrace will send to API
*/
constructor(
private data: Error | string = '',
private clientAttributes: { [index: string]: any } = {},
private attachments: string[] = [],
) {
if (!clientAttributes) {
clientAttributes = {};
}
this.splitAttributesFromAnnotations(clientAttributes);
if (!attachments) {
attachments = [];
}
this.setError(data);
}
/**
* Check if report contains exception information
*/
public isExceptionTypeReport(): boolean {
return this.detectReportType(this.data);
}
public getPayload(): Error | string {
return this.data;
}
/**
* Set error or message in BacktraceReport object
* @param err Current error
*/
public setError(err: Error | string): void {
this.data = err;
if (this.detectReportType(err)) {
this.err = err;
this.classifiers = [err.name];
} else {
this.err = new Error(err);
this.classifiers = [];
}
}
/**
* @deprecated
* Please don't use log method in new BacktraceReport object.
*/
public log() {
console.warn('log method is deprecated.');
}
/**
* @deprecated
* Please don't use trace method in new BacktraceReport object
*/
public trace() {
console.warn('trace method is deprecated.');
}
/**
* Add new attributes to existing report attributes
* @param attributes new report attributes object
*/
public addObjectAttributes(attributes: { [index: string]: any }): void {
this.clientAttributes = {
...this.clientAttributes,
...this.attributes,
...attributes,
};
}
public addAttribute(key: string, value: any): void {
this.clientAttributes[key] = value;
}
public addAnnotation(key: string, value: object): void {
this.annotations[key] = value;
}
public getAttachments(): string[] {
return this.attachments;
}
public async toJson(): Promise<IBacktraceData> {
// why library should wait to retrieve source code data?
// architecture decision require to pass additional parameters
// not in constructor, but in additional method.
await this.collectReportInformation();
const result = {
uuid: this.uuid,
timestamp: this.timestamp,
lang: this.lang,
langVersion: this.langVersion,
mainThread: this.mainThread,
classifiers: this.classifiers,
threads: { main: this.stackTrace.toJson() },
agent: this.agent,
agentVersion: this.agentVersion,
annotations: this.annotations,
attributes: this.attributes,
sourceCode: this.stackTrace.getSourceCode(),
symbolication_maps: this._symbolicationMap || this.stackTrace.symbolicationMaps,
} as IBacktraceData;
// when symbolication information exists, set symbolication to sourcemap.
// we should check symbolicationMap and _symbolication boolean value and symbolication id from attributes
// if any value exists, we should extend report object with 'sourcemap' property.
if (this._symbolication || this.attributes['symbolication_id'] || this._symbolicationMap) {
result.symbolication = 'sourcemap';
}
return result;
}
public setSourceCodeOptions(tabWidth: number, contextLineCount: number) {
this.tabWidth = tabWidth;
this.contextLineCount = contextLineCount;
}
/**
* Include symbolication information based on stack trace analysis
*/
private includeSymbolication(): boolean {
return this._symbolication && !this.attributes['symbolication_id'] && !this._symbolicationMap;
}
private async collectReportInformation(): Promise<void> {
// get stack trace to retrieve calling module information
this.stackTrace = new BacktraceStackTrace(this.err as Error);
this.stackTrace.setSourceCodeOptions(this.tabWidth, this.contextLineCount);
await this.stackTrace.parseStackFrames(this.includeSymbolication());
// retrieve calling module object
if (!this.attributes.hasOwnProperty('application')) {
[this._callingModule, this._callingModulePath] = readModule(this.stackTrace.getCallingModulePath());
}
// combine attributes
this.attributes = {
...this.readBuiltInAttributes(),
...this.clientAttributes,
};
// combine annotations
this.annotations = this.readAnnotation();
}
private readBuiltInAttributes(): object {
return {
...readMemoryInformation(),
...readProcessStatus(),
...this.readAttributes(),
...this.readErrorAttributes(),
};
}
private detectReportType(err: Error | string): err is Error {
return err instanceof Error;
}
private generateUuid(): string {
const bytes = pseudoRandomBytes(16);
return (
bytes.slice(0, 4).toString('hex') +
'-' +
bytes.slice(4, 6).toString('hex') +
'-' +
bytes.slice(6, 8).toString('hex') +
'-' +
bytes.slice(8, 10).toString('hex') +
'-' +
bytes.slice(10, 16).toString('hex')
);
}
private readErrorAttributes(): object {
if (!this.detectReportType(this.err)) {
return {
'error.message': this.err,
};
}
this.classifiers = [this.err.name];
return {
'error.message': this.err.message,
};
}
private readAttributes(): object {
const result = readSystemAttributes();
if (this._callingModule) {
const { name, version, main, description, author } = (this._callingModule || {}) as any;
result['name'] = name;
result['version'] = version;
result['main'] = main;
result['description'] = description;
result['author'] = typeof author === 'object' && author.name ? author.name : author;
}
return result;
}
private readAnnotation(): object {
const result = {
'Environment Variables': process.env,
'Exec Arguments': process.execArgv,
} as any;
if (this.detectReportType(this.err)) {
result['Exception'] = this.err;
}
return { ...result, ...this.annotations };
}
private splitAttributesFromAnnotations(clientAttributes: { [index: string]: any }) {
for (const key in clientAttributes) {
if (clientAttributes.hasOwnProperty(key)) {
const element = this.clientAttributes[key];
if (!element) {
continue;
}
if (typeof element === 'object') {
this.annotations[key] = element;
} else {
this.attributes[key] = element;
}
}
}
}
}