Skip to content
This repository has been archived by the owner on Dec 13, 2023. It is now read-only.

Commit

Permalink
Add metrics submission support (#41)
Browse files Browse the repository at this point in the history
* Add metrics submission support.

* Bump version number in reporting.

* Add and adjust existing unit tests. Fix version number. Rework how sessions are handled in this package.

* Use axios for sending session metric events.

* correct application.version

* fix app version and name in metrics

* simplify application name and version passing

* add unknown string if application name or version are not found

* Provide default unkown value for app name and version if it cannot be found.

* Update token in tests

* Metrics client linter + support for full URLs to Backtrace instances

* Use NODE_ENV to pass console or no to metrics instance

Co-authored-by: Stone <[email protected]>
Co-authored-by: kdysput <[email protected]>
  • Loading branch information
3 people authored Feb 3, 2022
1 parent 5fd4773 commit ba69393
Show file tree
Hide file tree
Showing 17 changed files with 1,907 additions and 51 deletions.
1,452 changes: 1,450 additions & 2 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "backtrace-node",
"version": "1.1.1",
"version": "1.2.0",
"description": "Backtrace error reporting tool",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand All @@ -24,7 +24,7 @@
"url": "[email protected]:backtrace-labs/backtrace-node.git"
},
"scripts": {
"test": "mocha --require ts-node/register -r ./tsconfig.json --project tsconfig.json test/**/*.ts",
"test": "NODE_ENV=test mocha --require ts-node/register -r ./tsconfig.json --project tsconfig.json test/**/*.ts",
"lint": "tslint -p ./tsconfig.json",
"format": "prettier --write \"source/**/*.ts\" \"source/**/*.js\"",
"build": "tsc"
Expand Down
2 changes: 1 addition & 1 deletion source/backtraceApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export class BacktraceApi extends EventEmitter {
}
return BacktraceResult.Ok(report, result.data);
} catch (err) {
return BacktraceResult.OnError(report, err);
return BacktraceResult.OnError(report, err as Error);
}
}

Expand Down
38 changes: 35 additions & 3 deletions source/backtraceClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import fs from 'fs';
import path from 'path';
import { BacktraceApi } from './backtraceApi';
import { ClientRateLimit } from './clientRateLimit';
import { readSystemAttributes } from './helpers/moduleResolver';
import { BacktraceClientOptions, IBacktraceClientOptions } from './model/backtraceClientOptions';
import { IBacktraceData } from './model/backtraceData';
import { BacktraceMetrics } from './model/backtraceMetrics';
import { BacktraceReport } from './model/backtraceReport';
import { BacktraceResult } from './model/backtraceResult';

Expand All @@ -19,6 +21,8 @@ export class BacktraceClient extends EventEmitter {
private _clientRateLimit: ClientRateLimit;
private _symbolication = false;
private _symbolicationMap?: Array<{ file: string; uuid: string }>;
private attributes: object = {};
private readonly _backtraceMetrics: BacktraceMetrics | undefined;

constructor(clientOptions: IBacktraceClientOptions | BacktraceClientOptions) {
super();
Expand All @@ -33,6 +37,25 @@ export class BacktraceClient extends EventEmitter {
this._clientRateLimit = new ClientRateLimit(this.options.rateLimit);
this.registerHandlers();
this.setupScopedAttributes();

this.attributes = this.getClientAttributes();
if (this.options.enableMetricsSupport) {
this._backtraceMetrics = new BacktraceMetrics(
clientOptions as BacktraceClientOptions,
() => {
return this.getClientAttributes();
},
process.env.NODE_ENV === 'test' ? undefined : console,
);
}
}

private getClientAttributes() {
return {
...readSystemAttributes(),
...this._scopedAttributes,
...this.options.attributes,
};
}

/**
Expand Down Expand Up @@ -194,8 +217,15 @@ export class BacktraceClient extends EventEmitter {
if (url.includes('submit.backtrace.io')) {
return url;
}
// allow user to define full URL to Backtrace without defining a token if the token is already available
// in the backtrace endpoint.
if (url.includes('token=')) {
return url;
}
if (!this.options.token) {
throw new Error('Token is required if Backtrace-node have to build url to Backtrace');
throw new Error(
'Token option is required if endpoint is not provided in `https://submit.backtrace.io/<universe>/<token>/json` format.',
);
}
const uriSeparator = url.endsWith('/') ? '' : '/';
return `${this.options.endpoint}${uriSeparator}post?format=json&token=${this.options.token}`;
Expand All @@ -207,6 +237,7 @@ export class BacktraceClient extends EventEmitter {
}
return {
...attributes,
...this.attributes,
...this.options.attributes,
...this.getMemorizedAttributes(),
...this._scopedAttributes,
Expand Down Expand Up @@ -246,8 +277,9 @@ export class BacktraceClient extends EventEmitter {
}
const json = JSON.parse(fs.readFileSync(applicationPackageJsonPath, 'utf8'));
this._scopedAttributes = {
'application.version': json.version,
application: json.name,
// a value for application name and version are required. If none are found, use unknown string.
'application.version': json.version || 'unknown',
application: json.name || 'unknown',
main: json.main,
description: json.description,
author: typeof json.author === 'object' && json.author.name ? json.author.name : json.author,
Expand Down
4 changes: 4 additions & 0 deletions source/const/application.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const APP_NAME = 'backtrace-node';
export const VERSION = '1.2.0';
export const LANG = 'nodejs';
export const THREAD = 'main';
30 changes: 30 additions & 0 deletions source/helpers/moduleResolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { BacktraceReport } from '../model/backtraceReport';
import { VERSION } from '../const/application';

/**
* Read module dependencies
Expand Down Expand Up @@ -55,3 +58,30 @@ function readParentDir(root: string, depth: number) {
const parent = path.join(root, '..');
return readModule(parent, --depth);
}


export function readSystemAttributes(): {[index: string]: any} {
const mem = process.memoryUsage();
const result = {
'process.age': Math.floor(process.uptime()),
'uname.uptime': os.uptime(),
'uname.machine': process.arch,
'uname.version': os.release(),
'uname.sysname': process.platform,
'vm.rss.size': mem.rss,
'gc.heap.total': mem.heapTotal,
'gc.heap.used': mem.heapUsed,
'node.env': process.env.NODE_ENV,
'debug.port': process.debugPort,
'backtrace.version': VERSION,
guid: BacktraceReport.machineId,
hostname: os.hostname(),
} as any;

const cpus = os.cpus();
if (cpus && cpus.length > 0) {
result['cpu.count'] = cpus.length;
result['cpu.brand'] = cpus[0].model;
}
return result;
}
3 changes: 3 additions & 0 deletions source/model/backtraceClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export class BacktraceClientOptions implements IBacktraceClientOptions {
public sampling: number | undefined = undefined;
public rateLimit: number = 0;
public debugBacktrace: boolean = false;

public enableMetricsSupport: boolean = true;
public metricsSubmissionUrl?: string;
}

export interface IBacktraceClientOptions {
Expand Down
136 changes: 136 additions & 0 deletions source/model/backtraceMetrics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { BacktraceClientOptions } from '..';
import { currentTimestamp, getEndpointParams, post, uuid } from '../utils';

/**
* Handles Backtrace Metrics.
*/
export class BacktraceMetrics {
private readonly universe: string;
private readonly token: string;
private readonly hostname: string;

private summedEndpoint: string;
private uniqueEndpoint: string;

private sessionId: string = uuid();

constructor(
configuration: BacktraceClientOptions,
private readonly attributeProvider: () => object,
private readonly _logger: { error(...data: any[]): void } | undefined,
) {
if (!configuration.endpoint) {
throw new Error(`Backtrace: missing 'endpoint' option.`);
}
const endpointParameters = getEndpointParams(configuration.endpoint, configuration.token);
if (!endpointParameters) {
throw new Error(`Invalid Backtrace submission parameters. Cannot create a submission URL to metrics support`);
}
const { universe, token } = endpointParameters;

if (!universe) {
throw new Error(`Backtrace: 'universe' could not be parsed from the endpoint.`);
}

if (!token) {
throw new Error(`Backtrace: missing 'token' option or it could not be parsed from the endpoint.`);
}

this.universe = universe;
this.token = token;
this.hostname = configuration.metricsSubmissionUrl ?? 'https://events.backtrace.io';

this.summedEndpoint = `${this.hostname}/api/summed-events/submit?universe=${this.universe}&token=${this.token}`;
this.uniqueEndpoint = `${this.hostname}/api/unique-events/submit?universe=${this.universe}&token=${this.token}`;

this.handleSession();
}

/**
* Handle persisting of session. When called, will create a new session.
*/
private handleSession(): void {
// If sessionId is not set, create new session. Send unique and app launch events.
this.sendUniqueEvent();
this.sendSummedEvent('Application Launches');
}

/**
* Send POST to unique-events API endpoint
*/
public async sendUniqueEvent(): Promise<void> {
const attributes = this.getEventAttributes();
const payload = {
application: attributes.application,
appversion: attributes['application.version'],
metadata: {
dropped_events: 0,
},
unique_events: [
{
timestamp: currentTimestamp(),
unique: ['guid'],
attributes: this.getEventAttributes(),
},
],
};

try {
await post(this.uniqueEndpoint, payload);
} catch (e) {
this._logger?.error(`Encountered error sending unique event: ${e?.message}`);
}
}

/**
* Send POST to summed-events API endpoint
*/
public async sendSummedEvent(metricGroup: string): Promise<void> {
const attributes = this.getEventAttributes();

const payload = {
application: attributes.application,
appversion: attributes['application.version'],
metadata: {
dropped_events: 0,
},
summed_events: [
{
timestamp: currentTimestamp(),
metric_group: metricGroup,
attributes: this.getEventAttributes(),
},
],
};

try {
await post(this.summedEndpoint, payload);
} catch (e) {
this._logger?.error(`Encountered error sending summed event: ${e?.message}`);
}
}

private getEventAttributes(): { [index: string]: any } {
const clientAttributes = this.attributeProvider() as {
[index: string]: any;
};
const result: { [index: string]: string } = {
'application.session': this.sessionId,
};

for (const attributeName in clientAttributes) {
if (Object.prototype.hasOwnProperty.call(clientAttributes, attributeName)) {
const element = clientAttributes[attributeName];
const elementType = typeof element;

if (elementType === 'string' || elementType === 'boolean' || elementType === 'number') {
const attributeValue = element.toString();
if (attributeValue) {
result[attributeName] = attributeValue;
}
}
}
}
return result;
}
}
40 changes: 10 additions & 30 deletions source/model/backtraceReport.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { pseudoRandomBytes } from 'crypto';
import * as os from 'os';
import { APP_NAME, LANG, THREAD, VERSION } from '../const/application';
import { machineIdSync } from '../helpers/machineId';
import { readModule } from '../helpers/moduleResolver';
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 {
private static machineId = machineIdSync(true);
public static machineId = machineIdSync(true);

public set symbolication(symbolication: boolean) {
this._symbolication = symbolication;
Expand All @@ -34,17 +35,17 @@ export class BacktraceReport {
// reprot id
public readonly uuid: string = this.generateUuid();
// timestamp
public readonly timestamp: number = Math.floor(new Date().getTime() / 1000);
public readonly timestamp: number = currentTimestamp();
// lang
public readonly lang = 'nodejs';
public readonly lang = LANG;
// environment version
public readonly langVersion = process.version;
// Backtrace-ndoe name
public readonly agent = 'backtrace-node';
public readonly agent = APP_NAME;
// Backtrace-node version
public readonly agentVersion = '1.1.1';
public readonly agentVersion= VERSION;
// main thread name
public readonly mainThread = 'main';
public readonly mainThread = THREAD;

public classifiers: string[] = [];

Expand Down Expand Up @@ -288,24 +289,8 @@ export class BacktraceReport {
}

private readAttributes(): object {
const mem = process.memoryUsage();
const result = {
'process.age': Math.floor(process.uptime()),
'uname.uptime': os.uptime(),
'uname.machine': process.arch,
'uname.version': os.release(),
'uname.sysname': process.platform,
'vm.rss.size': mem.rss,
'gc.heap.total': mem.heapTotal,
'gc.heap.used': mem.heapUsed,
'node.env': process.env.NODE_ENV,
'debug.port': process.debugPort,
'backtrace.version': this.agentVersion,
guid: BacktraceReport.machineId,
hostname: os.hostname(),
} as any;
const result = readSystemAttributes();

const cpus = os.cpus();
if (this._callingModule) {
const { name, version, main, description, author } = (this._callingModule || {}) as any;
result['name'] = name;
Expand All @@ -314,11 +299,6 @@ export class BacktraceReport {
result['description'] = description;
result['author'] = typeof author === 'object' && author.name ? author.name : author;
}

if (cpus && cpus.length > 0) {
result['cpu.count'] = cpus.length;
result['cpu.brand'] = cpus[0].model;
}
return result;
}

Expand Down
Loading

0 comments on commit ba69393

Please sign in to comment.