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

Commit ba69393

Browse files
stonefollariStonekonraddysput
authored
Add metrics submission support (#41)
* 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]>
1 parent 5fd4773 commit ba69393

17 files changed

+1907
-51
lines changed

package-lock.json

Lines changed: 1450 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "backtrace-node",
3-
"version": "1.1.1",
3+
"version": "1.2.0",
44
"description": "Backtrace error reporting tool",
55
"main": "./lib/index.js",
66
"types": "./lib/index.d.ts",
@@ -24,7 +24,7 @@
2424
"url": "[email protected]:backtrace-labs/backtrace-node.git"
2525
},
2626
"scripts": {
27-
"test": "mocha --require ts-node/register -r ./tsconfig.json --project tsconfig.json test/**/*.ts",
27+
"test": "NODE_ENV=test mocha --require ts-node/register -r ./tsconfig.json --project tsconfig.json test/**/*.ts",
2828
"lint": "tslint -p ./tsconfig.json",
2929
"format": "prettier --write \"source/**/*.ts\" \"source/**/*.js\"",
3030
"build": "tsc"

source/backtraceApi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export class BacktraceApi extends EventEmitter {
3030
}
3131
return BacktraceResult.Ok(report, result.data);
3232
} catch (err) {
33-
return BacktraceResult.OnError(report, err);
33+
return BacktraceResult.OnError(report, err as Error);
3434
}
3535
}
3636

source/backtraceClient.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import fs from 'fs';
33
import path from 'path';
44
import { BacktraceApi } from './backtraceApi';
55
import { ClientRateLimit } from './clientRateLimit';
6+
import { readSystemAttributes } from './helpers/moduleResolver';
67
import { BacktraceClientOptions, IBacktraceClientOptions } from './model/backtraceClientOptions';
78
import { IBacktraceData } from './model/backtraceData';
9+
import { BacktraceMetrics } from './model/backtraceMetrics';
810
import { BacktraceReport } from './model/backtraceReport';
911
import { BacktraceResult } from './model/backtraceResult';
1012

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

2327
constructor(clientOptions: IBacktraceClientOptions | BacktraceClientOptions) {
2428
super();
@@ -33,6 +37,25 @@ export class BacktraceClient extends EventEmitter {
3337
this._clientRateLimit = new ClientRateLimit(this.options.rateLimit);
3438
this.registerHandlers();
3539
this.setupScopedAttributes();
40+
41+
this.attributes = this.getClientAttributes();
42+
if (this.options.enableMetricsSupport) {
43+
this._backtraceMetrics = new BacktraceMetrics(
44+
clientOptions as BacktraceClientOptions,
45+
() => {
46+
return this.getClientAttributes();
47+
},
48+
process.env.NODE_ENV === 'test' ? undefined : console,
49+
);
50+
}
51+
}
52+
53+
private getClientAttributes() {
54+
return {
55+
...readSystemAttributes(),
56+
...this._scopedAttributes,
57+
...this.options.attributes,
58+
};
3659
}
3760

3861
/**
@@ -194,8 +217,15 @@ export class BacktraceClient extends EventEmitter {
194217
if (url.includes('submit.backtrace.io')) {
195218
return url;
196219
}
220+
// allow user to define full URL to Backtrace without defining a token if the token is already available
221+
// in the backtrace endpoint.
222+
if (url.includes('token=')) {
223+
return url;
224+
}
197225
if (!this.options.token) {
198-
throw new Error('Token is required if Backtrace-node have to build url to Backtrace');
226+
throw new Error(
227+
'Token option is required if endpoint is not provided in `https://submit.backtrace.io/<universe>/<token>/json` format.',
228+
);
199229
}
200230
const uriSeparator = url.endsWith('/') ? '' : '/';
201231
return `${this.options.endpoint}${uriSeparator}post?format=json&token=${this.options.token}`;
@@ -207,6 +237,7 @@ export class BacktraceClient extends EventEmitter {
207237
}
208238
return {
209239
...attributes,
240+
...this.attributes,
210241
...this.options.attributes,
211242
...this.getMemorizedAttributes(),
212243
...this._scopedAttributes,
@@ -246,8 +277,9 @@ export class BacktraceClient extends EventEmitter {
246277
}
247278
const json = JSON.parse(fs.readFileSync(applicationPackageJsonPath, 'utf8'));
248279
this._scopedAttributes = {
249-
'application.version': json.version,
250-
application: json.name,
280+
// a value for application name and version are required. If none are found, use unknown string.
281+
'application.version': json.version || 'unknown',
282+
application: json.name || 'unknown',
251283
main: json.main,
252284
description: json.description,
253285
author: typeof json.author === 'object' && json.author.name ? json.author.name : json.author,

source/const/application.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export const APP_NAME = 'backtrace-node';
2+
export const VERSION = '1.2.0';
3+
export const LANG = 'nodejs';
4+
export const THREAD = 'main';

source/helpers/moduleResolver.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3+
import * as os from 'os';
4+
import { BacktraceReport } from '../model/backtraceReport';
5+
import { VERSION } from '../const/application';
36

47
/**
58
* Read module dependencies
@@ -55,3 +58,30 @@ function readParentDir(root: string, depth: number) {
5558
const parent = path.join(root, '..');
5659
return readModule(parent, --depth);
5760
}
61+
62+
63+
export function readSystemAttributes(): {[index: string]: any} {
64+
const mem = process.memoryUsage();
65+
const result = {
66+
'process.age': Math.floor(process.uptime()),
67+
'uname.uptime': os.uptime(),
68+
'uname.machine': process.arch,
69+
'uname.version': os.release(),
70+
'uname.sysname': process.platform,
71+
'vm.rss.size': mem.rss,
72+
'gc.heap.total': mem.heapTotal,
73+
'gc.heap.used': mem.heapUsed,
74+
'node.env': process.env.NODE_ENV,
75+
'debug.port': process.debugPort,
76+
'backtrace.version': VERSION,
77+
guid: BacktraceReport.machineId,
78+
hostname: os.hostname(),
79+
} as any;
80+
81+
const cpus = os.cpus();
82+
if (cpus && cpus.length > 0) {
83+
result['cpu.count'] = cpus.length;
84+
result['cpu.brand'] = cpus[0].model;
85+
}
86+
return result;
87+
}

source/model/backtraceClientOptions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export class BacktraceClientOptions implements IBacktraceClientOptions {
1414
public sampling: number | undefined = undefined;
1515
public rateLimit: number = 0;
1616
public debugBacktrace: boolean = false;
17+
18+
public enableMetricsSupport: boolean = true;
19+
public metricsSubmissionUrl?: string;
1720
}
1821

1922
export interface IBacktraceClientOptions {

source/model/backtraceMetrics.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { BacktraceClientOptions } from '..';
2+
import { currentTimestamp, getEndpointParams, post, uuid } from '../utils';
3+
4+
/**
5+
* Handles Backtrace Metrics.
6+
*/
7+
export class BacktraceMetrics {
8+
private readonly universe: string;
9+
private readonly token: string;
10+
private readonly hostname: string;
11+
12+
private summedEndpoint: string;
13+
private uniqueEndpoint: string;
14+
15+
private sessionId: string = uuid();
16+
17+
constructor(
18+
configuration: BacktraceClientOptions,
19+
private readonly attributeProvider: () => object,
20+
private readonly _logger: { error(...data: any[]): void } | undefined,
21+
) {
22+
if (!configuration.endpoint) {
23+
throw new Error(`Backtrace: missing 'endpoint' option.`);
24+
}
25+
const endpointParameters = getEndpointParams(configuration.endpoint, configuration.token);
26+
if (!endpointParameters) {
27+
throw new Error(`Invalid Backtrace submission parameters. Cannot create a submission URL to metrics support`);
28+
}
29+
const { universe, token } = endpointParameters;
30+
31+
if (!universe) {
32+
throw new Error(`Backtrace: 'universe' could not be parsed from the endpoint.`);
33+
}
34+
35+
if (!token) {
36+
throw new Error(`Backtrace: missing 'token' option or it could not be parsed from the endpoint.`);
37+
}
38+
39+
this.universe = universe;
40+
this.token = token;
41+
this.hostname = configuration.metricsSubmissionUrl ?? 'https://events.backtrace.io';
42+
43+
this.summedEndpoint = `${this.hostname}/api/summed-events/submit?universe=${this.universe}&token=${this.token}`;
44+
this.uniqueEndpoint = `${this.hostname}/api/unique-events/submit?universe=${this.universe}&token=${this.token}`;
45+
46+
this.handleSession();
47+
}
48+
49+
/**
50+
* Handle persisting of session. When called, will create a new session.
51+
*/
52+
private handleSession(): void {
53+
// If sessionId is not set, create new session. Send unique and app launch events.
54+
this.sendUniqueEvent();
55+
this.sendSummedEvent('Application Launches');
56+
}
57+
58+
/**
59+
* Send POST to unique-events API endpoint
60+
*/
61+
public async sendUniqueEvent(): Promise<void> {
62+
const attributes = this.getEventAttributes();
63+
const payload = {
64+
application: attributes.application,
65+
appversion: attributes['application.version'],
66+
metadata: {
67+
dropped_events: 0,
68+
},
69+
unique_events: [
70+
{
71+
timestamp: currentTimestamp(),
72+
unique: ['guid'],
73+
attributes: this.getEventAttributes(),
74+
},
75+
],
76+
};
77+
78+
try {
79+
await post(this.uniqueEndpoint, payload);
80+
} catch (e) {
81+
this._logger?.error(`Encountered error sending unique event: ${e?.message}`);
82+
}
83+
}
84+
85+
/**
86+
* Send POST to summed-events API endpoint
87+
*/
88+
public async sendSummedEvent(metricGroup: string): Promise<void> {
89+
const attributes = this.getEventAttributes();
90+
91+
const payload = {
92+
application: attributes.application,
93+
appversion: attributes['application.version'],
94+
metadata: {
95+
dropped_events: 0,
96+
},
97+
summed_events: [
98+
{
99+
timestamp: currentTimestamp(),
100+
metric_group: metricGroup,
101+
attributes: this.getEventAttributes(),
102+
},
103+
],
104+
};
105+
106+
try {
107+
await post(this.summedEndpoint, payload);
108+
} catch (e) {
109+
this._logger?.error(`Encountered error sending summed event: ${e?.message}`);
110+
}
111+
}
112+
113+
private getEventAttributes(): { [index: string]: any } {
114+
const clientAttributes = this.attributeProvider() as {
115+
[index: string]: any;
116+
};
117+
const result: { [index: string]: string } = {
118+
'application.session': this.sessionId,
119+
};
120+
121+
for (const attributeName in clientAttributes) {
122+
if (Object.prototype.hasOwnProperty.call(clientAttributes, attributeName)) {
123+
const element = clientAttributes[attributeName];
124+
const elementType = typeof element;
125+
126+
if (elementType === 'string' || elementType === 'boolean' || elementType === 'number') {
127+
const attributeValue = element.toString();
128+
if (attributeValue) {
129+
result[attributeName] = attributeValue;
130+
}
131+
}
132+
}
133+
}
134+
return result;
135+
}
136+
}

source/model/backtraceReport.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { pseudoRandomBytes } from 'crypto';
2-
import * as os from 'os';
2+
import { APP_NAME, LANG, THREAD, VERSION } from '../const/application';
33
import { machineIdSync } from '../helpers/machineId';
4-
import { readModule } from '../helpers/moduleResolver';
4+
import { readModule, readSystemAttributes } from '../helpers/moduleResolver';
55
import { readMemoryInformation, readProcessStatus } from '../helpers/processHelper';
6+
import { currentTimestamp } from '../utils';
67
import { IBacktraceData } from './backtraceData';
78
import { BacktraceStackTrace } from './backtraceStackTrace';
89

910
/**
1011
* BacktraceReport describe current exception/message payload message to Backtrace
1112
*/
1213
export class BacktraceReport {
13-
private static machineId = machineIdSync(true);
14+
public static machineId = machineIdSync(true);
1415

1516
public set symbolication(symbolication: boolean) {
1617
this._symbolication = symbolication;
@@ -34,17 +35,17 @@ export class BacktraceReport {
3435
// reprot id
3536
public readonly uuid: string = this.generateUuid();
3637
// timestamp
37-
public readonly timestamp: number = Math.floor(new Date().getTime() / 1000);
38+
public readonly timestamp: number = currentTimestamp();
3839
// lang
39-
public readonly lang = 'nodejs';
40+
public readonly lang = LANG;
4041
// environment version
4142
public readonly langVersion = process.version;
4243
// Backtrace-ndoe name
43-
public readonly agent = 'backtrace-node';
44+
public readonly agent = APP_NAME;
4445
// Backtrace-node version
45-
public readonly agentVersion = '1.1.1';
46+
public readonly agentVersion= VERSION;
4647
// main thread name
47-
public readonly mainThread = 'main';
48+
public readonly mainThread = THREAD;
4849

4950
public classifiers: string[] = [];
5051

@@ -288,24 +289,8 @@ export class BacktraceReport {
288289
}
289290

290291
private readAttributes(): object {
291-
const mem = process.memoryUsage();
292-
const result = {
293-
'process.age': Math.floor(process.uptime()),
294-
'uname.uptime': os.uptime(),
295-
'uname.machine': process.arch,
296-
'uname.version': os.release(),
297-
'uname.sysname': process.platform,
298-
'vm.rss.size': mem.rss,
299-
'gc.heap.total': mem.heapTotal,
300-
'gc.heap.used': mem.heapUsed,
301-
'node.env': process.env.NODE_ENV,
302-
'debug.port': process.debugPort,
303-
'backtrace.version': this.agentVersion,
304-
guid: BacktraceReport.machineId,
305-
hostname: os.hostname(),
306-
} as any;
292+
const result = readSystemAttributes();
307293

308-
const cpus = os.cpus();
309294
if (this._callingModule) {
310295
const { name, version, main, description, author } = (this._callingModule || {}) as any;
311296
result['name'] = name;
@@ -314,11 +299,6 @@ export class BacktraceReport {
314299
result['description'] = description;
315300
result['author'] = typeof author === 'object' && author.name ? author.name : author;
316301
}
317-
318-
if (cpus && cpus.length > 0) {
319-
result['cpu.count'] = cpus.length;
320-
result['cpu.brand'] = cpus[0].model;
321-
}
322302
return result;
323303
}
324304

0 commit comments

Comments
 (0)