@orbitai/orbitrace 1.1.12 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -0
- package/dist/index.js +282 -1
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -77,12 +77,66 @@ In short-lived processes (scripts, CI jobs, serverless functions), call `flush()
|
|
|
77
77
|
await orbitrace.flush();
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
+
### 5. Automatic Capture
|
|
81
|
+
|
|
82
|
+
Pass `autoCapture: true` to `getInstance` (SDK 1.2.0 or newer) to capture what nobody handled and record breadcrumbs automatically. It is off unless you turn it on, so upgrading never changes how an application crashes or logs.
|
|
83
|
+
|
|
84
|
+
```javascript
|
|
85
|
+
orbitrace.getInstance({ ...config, autoCapture: true });
|
|
86
|
+
// or choose parts:
|
|
87
|
+
orbitrace.getInstance({ ...config, autoCapture: { exceptions: true, console: true, fetch: false } });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- **Uncaught exceptions and unhandled rejections** are captured. The process still crashes as Node would: the SDK waits up to 2 seconds for the event to send, prints the error and exits with code 1, and respects `--unhandled-rejections`. If the application registers its own `uncaughtException` or `unhandledRejection` handlers, they decide instead.
|
|
91
|
+
- **Breadcrumbs** are recorded from `console` calls and `fetch` requests (URLs without credentials, query string or fragment). The SDK's own logs and requests are skipped.
|
|
92
|
+
- `orbitrace.close()` removes everything automatic capture installed.
|
|
93
|
+
|
|
94
|
+
### 6. Breadcrumbs
|
|
95
|
+
|
|
96
|
+
Breadcrumbs are the last 30 things that happened before an event, sent with it. Add your own at important steps:
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
orbitrace.addBreadcrumb({
|
|
100
|
+
category: "checkout",
|
|
101
|
+
message: "Payment authorized",
|
|
102
|
+
level: "info", // debug, info, warning or error
|
|
103
|
+
data: { orderId: "ord_1" },
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Breadcrumb data is filtered like metadata. Work that runs concurrently should have its own breadcrumbs: use the Express handlers or `wrap()` below, or `orbitrace.withScope(() => work())`.
|
|
108
|
+
|
|
109
|
+
### 7. Express
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
app.use(orbitrace.expressRequestHandler()); // before the routes: breadcrumbs per request
|
|
113
|
+
app.post("/checkout", async (req, res, next) => {
|
|
114
|
+
try {
|
|
115
|
+
res.json(await checkout(req.body));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
next(error); // Express 4 does not pass errors from async routes on by itself
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
app.use(orbitrace.expressErrorHandler()); // after the routes: captures with method, path and status, then calls next(error)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### 8. Cloud Functions and Other Serverless Handlers
|
|
124
|
+
|
|
125
|
+
The runtime may freeze as soon as a handler returns. `wrap()` gives each call its own breadcrumbs, captures a thrown error (and rethrows it), and sends pending events before returning:
|
|
126
|
+
|
|
127
|
+
```javascript
|
|
128
|
+
exports.checkout = onRequest(orbitrace.wrap(async (req, res) => {
|
|
129
|
+
res.json(await checkout(req.body));
|
|
130
|
+
}));
|
|
131
|
+
```
|
|
132
|
+
|
|
80
133
|
## Data Collected
|
|
81
134
|
|
|
82
135
|
Each event stores:
|
|
83
136
|
|
|
84
137
|
- The error message (truncated to 200 characters) and stack trace, or the message text.
|
|
85
138
|
- The `metadata` object you pass, plus `env`, `version` and `service` from your configuration.
|
|
139
|
+
- Breadcrumbs, when there are any: the last 30 recorded before the event.
|
|
86
140
|
- The IP address and user agent of the server that sent the request, with an approximate location derived from that IP.
|
|
87
141
|
|
|
88
142
|
Before sending, metadata values whose key contains `password`, `passwd`, `secret`, `token`, `authorization`, `cookie`, `apikey`, `privatekey`, `credential` or `session` (ignoring case, `-` and `_`) are replaced with `[Filtered]`, at any depth. Error messages and stack traces are sent as they are.
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require("node:async_hooks");
|
|
2
|
+
const util = require("node:util");
|
|
3
|
+
|
|
1
4
|
// Reporting must never slow down or break the host application, so every failure is logged and swallowed.
|
|
2
5
|
const REQUEST_TIMEOUT_MS = 5000;
|
|
3
6
|
// Repeats of the same event inside this window are counted locally and sent once as a single request.
|
|
@@ -5,6 +8,13 @@ const DEDUPE_WINDOW_MS = 10000;
|
|
|
5
8
|
// Caps concurrent requests so a loop of distinct errors cannot exhaust the host's sockets or memory.
|
|
6
9
|
const MAX_IN_FLIGHT = 10;
|
|
7
10
|
const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
11
|
+
// Breadcrumbs kept per scope; the oldest is dropped when a new one arrives.
|
|
12
|
+
const MAX_BREADCRUMBS = 30;
|
|
13
|
+
const MAX_BREADCRUMB_MESSAGE_LENGTH = 200;
|
|
14
|
+
const BREADCRUMB_LEVELS = ["debug", "info", "warning", "error"];
|
|
15
|
+
// How long a crashing process waits for its last event before exiting.
|
|
16
|
+
const CRASH_FLUSH_TIMEOUT_MS = 2000;
|
|
17
|
+
const CONSOLE_LEVELS = { debug: "debug", log: "info", info: "info", warn: "warning", error: "error" };
|
|
8
18
|
// Metadata keys containing any of these (ignoring case, "-" and "_") have their values replaced before sending.
|
|
9
19
|
const SENSITIVE_KEY_PARTS = [
|
|
10
20
|
"password",
|
|
@@ -20,6 +30,9 @@ const SENSITIVE_KEY_PARTS = [
|
|
|
20
30
|
];
|
|
21
31
|
const FILTERED_VALUE = "[Filtered]";
|
|
22
32
|
|
|
33
|
+
// Each request or wrapped invocation gets its own breadcrumbs, so concurrent work never mixes histories.
|
|
34
|
+
const scopes = new AsyncLocalStorage();
|
|
35
|
+
|
|
23
36
|
function isSensitiveKey(key) {
|
|
24
37
|
const normalized = key.toLowerCase().replace(/[-_]/g, "");
|
|
25
38
|
return SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
|
|
@@ -57,15 +70,41 @@ function filterSensitive(value, ancestors = new WeakSet()) {
|
|
|
57
70
|
return filtered;
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
function truncate(value, maxLength) {
|
|
74
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// URLs are recorded without credentials, query string or fragment, which often carry tokens or personal data.
|
|
78
|
+
function cleanUrl(url) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = new URL(url);
|
|
81
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
82
|
+
} catch {
|
|
83
|
+
return String(url).split(/[?#]/)[0];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// What Node does with an unhandled rejection when nothing else handles it (--unhandled-rejections).
|
|
88
|
+
function unhandledRejectionMode() {
|
|
89
|
+
const flags = [...process.execArgv, ...(process.env.NODE_OPTIONS || "").split(/\s+/)].join(" ");
|
|
90
|
+
return flags.match(/--unhandled-rejections[= ](\S+)/)?.[1] || "throw";
|
|
91
|
+
}
|
|
92
|
+
|
|
60
93
|
class Orbitrace {
|
|
61
94
|
static #config = null;
|
|
62
95
|
static #pending = new Map();
|
|
63
96
|
static #inFlight = new Set();
|
|
64
97
|
static #pausedUntil = 0;
|
|
98
|
+
static #breadcrumbs = [];
|
|
99
|
+
// Undo functions for everything automatic capture installed; close() runs them.
|
|
100
|
+
static #restore = [];
|
|
101
|
+
static #fetch = null;
|
|
102
|
+
static #handlingCrash = false;
|
|
65
103
|
|
|
66
104
|
static getInstance(config) {
|
|
67
105
|
this.assertServerRuntime();
|
|
68
106
|
this.validateConfig(config);
|
|
107
|
+
this.close();
|
|
69
108
|
|
|
70
109
|
this.#config = {
|
|
71
110
|
apiKey: config.apiKey,
|
|
@@ -77,6 +116,12 @@ class Orbitrace {
|
|
|
77
116
|
service: config.service || "not-specified",
|
|
78
117
|
disabled: config.disabled || false,
|
|
79
118
|
};
|
|
119
|
+
|
|
120
|
+
const autoCapture = this.#autoCaptureOptions(config.autoCapture);
|
|
121
|
+
|
|
122
|
+
if (autoCapture && !this.#config.disabled) {
|
|
123
|
+
this.#installAutoCapture(autoCapture);
|
|
124
|
+
}
|
|
80
125
|
}
|
|
81
126
|
|
|
82
127
|
static validateConfig(config) {
|
|
@@ -112,6 +157,71 @@ class Orbitrace {
|
|
|
112
157
|
return this.#capture("capture_message", { message, metadata });
|
|
113
158
|
}
|
|
114
159
|
|
|
160
|
+
// Records something that happened, sent with the next event of the current scope.
|
|
161
|
+
static addBreadcrumb({ category = "manual", message = "", level = "info", data } = {}) {
|
|
162
|
+
try {
|
|
163
|
+
const breadcrumbs = scopes.getStore()?.breadcrumbs ?? this.#breadcrumbs;
|
|
164
|
+
|
|
165
|
+
breadcrumbs.push({
|
|
166
|
+
timestamp: Date.now(),
|
|
167
|
+
category: String(category).slice(0, 64),
|
|
168
|
+
level: BREADCRUMB_LEVELS.includes(level) ? level : "info",
|
|
169
|
+
message: truncate(String(message), MAX_BREADCRUMB_MESSAGE_LENGTH),
|
|
170
|
+
...(data !== undefined && { data: filterSensitive(data) }),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
174
|
+
breadcrumbs.shift();
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// A breadcrumb that cannot be recorded is skipped.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Runs callback with its own breadcrumbs, for work that is not an Express request or wrapped function.
|
|
182
|
+
static withScope(callback) {
|
|
183
|
+
return scopes.run({ breadcrumbs: [] }, callback);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Wraps a serverless handler: each invocation gets its own breadcrumbs, a thrown error is captured, and
|
|
187
|
+
// pending events are sent before the handler returns, because the runtime may freeze right after.
|
|
188
|
+
static wrap(handler) {
|
|
189
|
+
return function orbitraceWrapped(...args) {
|
|
190
|
+
return Orbitrace.withScope(async () => {
|
|
191
|
+
try {
|
|
192
|
+
return await handler.apply(this, args);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
await Orbitrace.captureException(error, { mechanism: "wrap" });
|
|
195
|
+
throw error;
|
|
196
|
+
} finally {
|
|
197
|
+
await Orbitrace.flush();
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Express middleware, registered before the routes, that gives each request its own breadcrumbs.
|
|
204
|
+
static expressRequestHandler() {
|
|
205
|
+
return function orbitraceRequestHandler(req, res, next) {
|
|
206
|
+
Orbitrace.withScope(() => next());
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Express error middleware, registered after the routes: captures the error and passes it on unchanged.
|
|
211
|
+
static expressErrorHandler() {
|
|
212
|
+
return function orbitraceErrorHandler(error, req, res, next) {
|
|
213
|
+
Orbitrace.captureException(error, {
|
|
214
|
+
mechanism: "express",
|
|
215
|
+
request: {
|
|
216
|
+
method: req.method,
|
|
217
|
+
path: String(req.originalUrl || req.url || "").split(/[?#]/)[0],
|
|
218
|
+
statusCode: error?.status || error?.statusCode || 500,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
next(error);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
115
225
|
// Sends counts still held by the dedupe window and waits for in-flight requests.
|
|
116
226
|
// Call it before a short-lived process or serverless invocation ends.
|
|
117
227
|
static async flush() {
|
|
@@ -119,6 +229,175 @@ class Orbitrace {
|
|
|
119
229
|
await Promise.all(this.#inFlight);
|
|
120
230
|
}
|
|
121
231
|
|
|
232
|
+
// Removes the handlers and wrappers automatic capture installed and forgets the global breadcrumbs.
|
|
233
|
+
static close() {
|
|
234
|
+
for (const undo of this.#restore.splice(0).reverse()) {
|
|
235
|
+
try {
|
|
236
|
+
undo();
|
|
237
|
+
} catch {
|
|
238
|
+
// Restoring one hook must not stop the others.
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.#fetch = null;
|
|
243
|
+
this.#breadcrumbs = [];
|
|
244
|
+
this.#handlingCrash = false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// autoCapture: true, or { exceptions, console, fetch } to switch parts off. Off unless asked for, so
|
|
248
|
+
// upgrading never changes how an existing application handles crashes or logs.
|
|
249
|
+
static #autoCaptureOptions(value) {
|
|
250
|
+
if (!value) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const options = value === true ? {} : value;
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
exceptions: options.exceptions !== false,
|
|
258
|
+
console: options.console !== false,
|
|
259
|
+
fetch: options.fetch !== false,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static #installAutoCapture(options) {
|
|
264
|
+
if (options.exceptions) {
|
|
265
|
+
const onUncaughtException = (error) => this.#handleCrash(error, "uncaughtException");
|
|
266
|
+
const onUnhandledRejection = (reason) => this.#handleCrash(reason, "unhandledRejection");
|
|
267
|
+
|
|
268
|
+
process.on("uncaughtException", onUncaughtException);
|
|
269
|
+
process.on("unhandledRejection", onUnhandledRejection);
|
|
270
|
+
this.#restore.push(() => {
|
|
271
|
+
process.removeListener("uncaughtException", onUncaughtException);
|
|
272
|
+
process.removeListener("unhandledRejection", onUnhandledRejection);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (options.console) {
|
|
277
|
+
let recording = false;
|
|
278
|
+
|
|
279
|
+
for (const [method, level] of Object.entries(CONSOLE_LEVELS)) {
|
|
280
|
+
const original = console[method];
|
|
281
|
+
|
|
282
|
+
if (typeof original !== "function") {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
console[method] = function orbitraceConsole(...args) {
|
|
287
|
+
// The guard stops a breadcrumb that logs from recording itself again.
|
|
288
|
+
if (!recording) {
|
|
289
|
+
recording = true;
|
|
290
|
+
try {
|
|
291
|
+
const message = util.formatWithOptions(
|
|
292
|
+
{
|
|
293
|
+
depth: 1,
|
|
294
|
+
maxArrayLength: 10,
|
|
295
|
+
maxStringLength: MAX_BREADCRUMB_MESSAGE_LENGTH,
|
|
296
|
+
breakLength: Infinity,
|
|
297
|
+
},
|
|
298
|
+
...args
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
if (!message.startsWith("Orbitrace:")) {
|
|
302
|
+
Orbitrace.addBreadcrumb({ category: "console", level, message });
|
|
303
|
+
}
|
|
304
|
+
} catch {
|
|
305
|
+
// Unformattable arguments only lose their breadcrumb.
|
|
306
|
+
} finally {
|
|
307
|
+
recording = false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return original.apply(this, args);
|
|
312
|
+
};
|
|
313
|
+
this.#restore.push(() => {
|
|
314
|
+
console[method] = original;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (options.fetch && typeof globalThis.fetch === "function") {
|
|
320
|
+
const originalFetch = globalThis.fetch;
|
|
321
|
+
const endpoint = this.#config.endpoint;
|
|
322
|
+
|
|
323
|
+
// Orbitrace's own requests use the original fetch, so they are never recorded.
|
|
324
|
+
this.#fetch = originalFetch;
|
|
325
|
+
globalThis.fetch = async function orbitraceFetch(input, init) {
|
|
326
|
+
const url = typeof input === "string" || input instanceof URL ? String(input) : input?.url;
|
|
327
|
+
|
|
328
|
+
if (!url || url.startsWith(endpoint)) {
|
|
329
|
+
return originalFetch.call(this, input, init);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const method = String(init?.method || input?.method || "GET").toUpperCase();
|
|
333
|
+
const started = Date.now();
|
|
334
|
+
const data = { method, url: cleanUrl(url) };
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const response = await originalFetch.call(this, input, init);
|
|
338
|
+
|
|
339
|
+
Orbitrace.addBreadcrumb({
|
|
340
|
+
category: "http",
|
|
341
|
+
level: response.status >= 500 ? "error" : response.status >= 400 ? "warning" : "info",
|
|
342
|
+
message: `${method} ${data.url} ${response.status}`,
|
|
343
|
+
data: { ...data, status: response.status, durationMs: Date.now() - started },
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return response;
|
|
347
|
+
} catch (error) {
|
|
348
|
+
Orbitrace.addBreadcrumb({
|
|
349
|
+
category: "http",
|
|
350
|
+
level: "error",
|
|
351
|
+
message: `${method} ${data.url} failed`,
|
|
352
|
+
data: { ...data, error: error?.message, durationMs: Date.now() - started },
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
throw error;
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
this.#restore.push(() => {
|
|
359
|
+
globalThis.fetch = originalFetch;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Captures a crash, then does what Node would have done without this handler: exit with code 1, or follow
|
|
365
|
+
// --unhandled-rejections. When the application registered its own handlers, it decides instead.
|
|
366
|
+
static #handleCrash(reason, kind) {
|
|
367
|
+
if (this.#handlingCrash) {
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const decidesExit = process.listenerCount(kind) === 1;
|
|
372
|
+
const mode = kind === "unhandledRejection" ? unhandledRejectionMode() : "throw";
|
|
373
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled rejection: ${util.inspect(reason)}`);
|
|
374
|
+
const exits = decidesExit && ["throw", "strict"].includes(mode);
|
|
375
|
+
|
|
376
|
+
if (exits) {
|
|
377
|
+
this.#handlingCrash = true;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const delivered = Promise.race([
|
|
381
|
+
this.captureException(error, { mechanism: kind }).then(() => this.flush()),
|
|
382
|
+
// Not unref'd: this timer keeps a crashing process alive until it exits with code 1.
|
|
383
|
+
new Promise((resolve) => setTimeout(resolve, CRASH_FLUSH_TIMEOUT_MS)),
|
|
384
|
+
]).catch(() => {});
|
|
385
|
+
|
|
386
|
+
if (!decidesExit || mode === "none") {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
process.stderr.write(`${error.stack || error.message}\n`);
|
|
391
|
+
|
|
392
|
+
if (mode === "warn-with-error-code") {
|
|
393
|
+
process.exitCode = 1;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (exits) {
|
|
397
|
+
delivered.finally(() => process.exit(1));
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
122
401
|
static async #capture(event, { metadata, ...fields }) {
|
|
123
402
|
if (!this.#config) {
|
|
124
403
|
console.error("Orbitrace: call Orbitrace.getInstance() before capturing events");
|
|
@@ -130,6 +409,7 @@ class Orbitrace {
|
|
|
130
409
|
return;
|
|
131
410
|
}
|
|
132
411
|
|
|
412
|
+
const breadcrumbs = [...(scopes.getStore()?.breadcrumbs ?? this.#breadcrumbs)];
|
|
133
413
|
const payload = {
|
|
134
414
|
...fields,
|
|
135
415
|
metadata: {
|
|
@@ -138,6 +418,7 @@ class Orbitrace {
|
|
|
138
418
|
version: this.#config.version,
|
|
139
419
|
service: this.#config.service,
|
|
140
420
|
},
|
|
421
|
+
...(breadcrumbs.length > 0 && { breadcrumbs }),
|
|
141
422
|
};
|
|
142
423
|
const key = JSON.stringify([event, payload.message, payload.stack ?? null]);
|
|
143
424
|
const pending = this.#pending.get(key);
|
|
@@ -187,7 +468,7 @@ class Orbitrace {
|
|
|
187
468
|
|
|
188
469
|
static async #post(event, payload, count) {
|
|
189
470
|
try {
|
|
190
|
-
const response = await fetch(this.#config.endpoint, {
|
|
471
|
+
const response = await (this.#fetch || fetch)(this.#config.endpoint, {
|
|
191
472
|
method: "POST",
|
|
192
473
|
headers: {
|
|
193
474
|
"Content-Type": "application/json",
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orbitai/orbitrace",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Error tracking
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Error tracking for Node.js services: manual and automatic capture with breadcrumbs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
8
8
|
],
|
|
9
9
|
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js",
|
|
10
11
|
"build": "mkdir -p dist && cp src/index.js dist/index.js",
|
|
11
12
|
"deploy": "npm version patch && npm run build && npm publish --access public"
|
|
12
13
|
},
|