@monocle.sh/adonisjs-agent 1.0.0-beta.4 → 1.0.0-beta.6
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 +2 -2
- package/dist/configure.mjs +1 -1
- package/dist/init.mjs +1 -3
- package/dist/monocle_provider.mjs +19 -9
- package/dist/src/cli_instrumentation.mjs +6 -8
- package/dist/src/context_lines.mjs +294 -0
- package/dist/src/error_serializer.mjs +77 -0
- package/dist/src/monocle.d.mts +4 -1
- package/dist/src/monocle.mjs +9 -7
- package/package.json +12 -3
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ import { defineConfig } from '@monocle.sh/adonisjs-agent'
|
|
|
85
85
|
import env from '#start/env'
|
|
86
86
|
|
|
87
87
|
export default defineConfig({
|
|
88
|
-
//
|
|
88
|
+
// Optional: Your Monocle API key
|
|
89
89
|
apiKey: env.get('MONOCLE_API_KEY'),
|
|
90
90
|
|
|
91
91
|
// Optional: Custom ingestion endpoint (for development)
|
|
@@ -123,7 +123,7 @@ export default defineConfig({
|
|
|
123
123
|
|
|
124
124
|
| Variable | Description | Required |
|
|
125
125
|
| ----------------- | ------------------------------------------------------ | -------- |
|
|
126
|
-
| `MONOCLE_API_KEY` | Your Monocle API key |
|
|
126
|
+
| `MONOCLE_API_KEY` | Your Monocle API key | No |
|
|
127
127
|
| `APP_NAME` | Service name for identification | Yes |
|
|
128
128
|
| `APP_VERSION` | Service version (e.g., git sha, semver) | Yes |
|
|
129
129
|
| `APP_ENV` | Environment: `development`, `staging`, or `production` | Yes |
|
package/dist/configure.mjs
CHANGED
|
@@ -53,7 +53,7 @@ async function configure(command) {
|
|
|
53
53
|
APP_NAME: "Env.schema.string()",
|
|
54
54
|
APP_VERSION: "Env.schema.string()",
|
|
55
55
|
APP_ENV: `Env.schema.enum(['development', 'staging', 'production'] as const)`,
|
|
56
|
-
MONOCLE_API_KEY: "Env.schema.string()"
|
|
56
|
+
MONOCLE_API_KEY: "Env.schema.string.optional()"
|
|
57
57
|
} });
|
|
58
58
|
}
|
|
59
59
|
|
package/dist/init.mjs
CHANGED
|
@@ -69,9 +69,7 @@ async function init(dirname$1) {
|
|
|
69
69
|
const manager = OtelManager.create(configWithProcessors);
|
|
70
70
|
manager?.start();
|
|
71
71
|
const shutdown = async () => {
|
|
72
|
-
await manager?.shutdown().catch(() => {
|
|
73
|
-
console.error("Error during OTEL shutdown");
|
|
74
|
-
});
|
|
72
|
+
await manager?.shutdown().catch(() => {});
|
|
75
73
|
};
|
|
76
74
|
process.on("beforeExit", shutdown);
|
|
77
75
|
process.on("SIGINT", async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { recordExceptionWithContext } from "./src/context_lines.mjs";
|
|
2
2
|
import { getCurrentSpan } from "@adonisjs/otel/helpers";
|
|
3
|
+
import is from "@sindresorhus/is";
|
|
3
4
|
import OtelMiddleware from "@adonisjs/otel/otel_middleware";
|
|
4
5
|
import { OtelManager } from "@adonisjs/otel";
|
|
5
6
|
import { ExceptionHandler } from "@adonisjs/core/http";
|
|
@@ -15,6 +16,15 @@ var OtelProvider = class {
|
|
|
15
16
|
this.app = app;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
19
|
+
* Convert any error to an HttpError-like object
|
|
20
|
+
*/
|
|
21
|
+
#toHttpError(error) {
|
|
22
|
+
const httpError = is.object(error) ? error : new Error(String(error));
|
|
23
|
+
if (!httpError.message) httpError.message = "Internal server error";
|
|
24
|
+
if (!httpError.status) httpError.status = 500;
|
|
25
|
+
return httpError;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
18
28
|
* Hook into ExceptionHandler to record exceptions in spans.
|
|
19
29
|
*
|
|
20
30
|
* We always record the exception on the span (for trace visibility), but we also
|
|
@@ -26,17 +36,17 @@ var OtelProvider = class {
|
|
|
26
36
|
*/
|
|
27
37
|
#registerExceptionHandler() {
|
|
28
38
|
const originalReport = ExceptionHandler.prototype.report;
|
|
39
|
+
const toHttpError = this.#toHttpError;
|
|
29
40
|
ExceptionHandler.macro("report", async function(error, ctx) {
|
|
30
41
|
const span = getCurrentSpan();
|
|
31
|
-
if (span
|
|
32
|
-
|
|
33
|
-
span.setStatus({
|
|
34
|
-
code: SpanStatusCode.ERROR,
|
|
35
|
-
message: error.message
|
|
36
|
-
});
|
|
37
|
-
const httpError = this.toHttpError(error);
|
|
42
|
+
if (span) {
|
|
43
|
+
const httpError = toHttpError(error);
|
|
38
44
|
const shouldReport = this.shouldReport(httpError);
|
|
39
|
-
|
|
45
|
+
await recordExceptionWithContext({
|
|
46
|
+
span,
|
|
47
|
+
error: is.error(error) ? error : new Error(String(error)),
|
|
48
|
+
shouldReport
|
|
49
|
+
});
|
|
40
50
|
}
|
|
41
51
|
return originalReport.call(this, error, ctx);
|
|
42
52
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { recordExceptionWithContext } from "./context_lines.mjs";
|
|
1
2
|
import { createRequire } from "node:module";
|
|
2
|
-
import { SpanKind,
|
|
3
|
+
import { SpanKind, context, trace } from "@opentelemetry/api";
|
|
3
4
|
|
|
4
5
|
//#region src/cli_instrumentation.ts
|
|
5
6
|
/**
|
|
@@ -71,13 +72,10 @@ async function instrumentCliCommands(config, appRoot) {
|
|
|
71
72
|
try {
|
|
72
73
|
return await originalExec.call(this);
|
|
73
74
|
} catch (error) {
|
|
74
|
-
if (error instanceof Error) {
|
|
75
|
-
span
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
message: error.message
|
|
79
|
-
});
|
|
80
|
-
}
|
|
75
|
+
if (error instanceof Error) await recordExceptionWithContext({
|
|
76
|
+
span,
|
|
77
|
+
error
|
|
78
|
+
});
|
|
81
79
|
throw error;
|
|
82
80
|
} finally {
|
|
83
81
|
span.end();
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { MAX_CAUSE_DEPTH, buildCauseChain, getErrorCause, isErrorLike, stackWithCauses } from "./error_serializer.mjs";
|
|
2
|
+
import { SpanStatusCode } from "@opentelemetry/api";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { createReadStream } from "node:fs";
|
|
5
|
+
import { parseStack } from "error-stack-parser-es/lite";
|
|
6
|
+
|
|
7
|
+
//#region src/context_lines.ts
|
|
8
|
+
const DEFAULT_CONTEXT_LINES = 7;
|
|
9
|
+
const MAX_LINENO = 1e4;
|
|
10
|
+
const MAX_COLNO = 1e3;
|
|
11
|
+
const MAX_LINE_LENGTH = 200;
|
|
12
|
+
const FILE_CACHE_SIZE = 10;
|
|
13
|
+
const FAILED_CACHE_SIZE = 20;
|
|
14
|
+
/**
|
|
15
|
+
* Simple LRU cache implementation.
|
|
16
|
+
*/
|
|
17
|
+
var LRUCache = class {
|
|
18
|
+
#maxSize;
|
|
19
|
+
#cache = /* @__PURE__ */ new Map();
|
|
20
|
+
constructor(maxSize) {
|
|
21
|
+
this.#maxSize = maxSize;
|
|
22
|
+
}
|
|
23
|
+
get(key) {
|
|
24
|
+
const value = this.#cache.get(key);
|
|
25
|
+
if (value === void 0) return void 0;
|
|
26
|
+
this.#cache.delete(key);
|
|
27
|
+
this.#cache.set(key, value);
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
set(key, value) {
|
|
31
|
+
this.#cache.delete(key);
|
|
32
|
+
if (this.#cache.size >= this.#maxSize) {
|
|
33
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
34
|
+
if (oldestKey !== void 0) this.#cache.delete(oldestKey);
|
|
35
|
+
}
|
|
36
|
+
this.#cache.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
has(key) {
|
|
39
|
+
return this.#cache.has(key);
|
|
40
|
+
}
|
|
41
|
+
clear() {
|
|
42
|
+
this.#cache.clear();
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const fileCache = new LRUCache(FILE_CACHE_SIZE);
|
|
46
|
+
const failedCache = new LRUCache(FAILED_CACHE_SIZE);
|
|
47
|
+
/**
|
|
48
|
+
* Normalize file path by stripping file:// protocol.
|
|
49
|
+
*/
|
|
50
|
+
function normalizeFilePath(path) {
|
|
51
|
+
if (path.startsWith("file://")) return path.slice(7);
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if a file should be skipped for context extraction.
|
|
56
|
+
*/
|
|
57
|
+
function shouldSkipFile(path) {
|
|
58
|
+
if (path.startsWith("node:")) return true;
|
|
59
|
+
if (path.startsWith("data:")) return true;
|
|
60
|
+
if (path.endsWith(".min.js")) return true;
|
|
61
|
+
if (path.endsWith(".min.cjs")) return true;
|
|
62
|
+
if (path.endsWith(".min.mjs")) return true;
|
|
63
|
+
if (path.includes("/node_modules/")) return true;
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Check if a frame should be skipped based on line/column numbers.
|
|
68
|
+
*/
|
|
69
|
+
function shouldSkipFrame(frame) {
|
|
70
|
+
if (frame.line === void 0) return true;
|
|
71
|
+
if (frame.line > MAX_LINENO) return true;
|
|
72
|
+
if (frame.col !== void 0 && frame.col > MAX_COLNO) return true;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Check if a frame is from the application code (not node_modules).
|
|
77
|
+
*/
|
|
78
|
+
function isInApp(filename) {
|
|
79
|
+
return !filename.includes("/node_modules/");
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Truncate long lines to prevent huge payloads.
|
|
83
|
+
*/
|
|
84
|
+
function snipLine(line) {
|
|
85
|
+
if (line.length <= MAX_LINE_LENGTH) return line;
|
|
86
|
+
return line.slice(0, MAX_LINE_LENGTH) + "...";
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Create context range around a line number.
|
|
90
|
+
*/
|
|
91
|
+
function makeContextRange(lineno, contextLines) {
|
|
92
|
+
return [Math.max(1, lineno - contextLines), lineno + contextLines];
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Merge overlapping line ranges for efficient file reading.
|
|
96
|
+
*/
|
|
97
|
+
function mergeRanges(ranges) {
|
|
98
|
+
if (ranges.length === 0) return [];
|
|
99
|
+
const sorted = [...ranges].sort((a, b) => a[0] - b[0]);
|
|
100
|
+
const merged = [sorted[0]];
|
|
101
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
102
|
+
const current = sorted[i];
|
|
103
|
+
const last = merged[merged.length - 1];
|
|
104
|
+
if (current[0] <= last[1] + 1) last[1] = Math.max(last[1], current[1]);
|
|
105
|
+
else merged.push(current);
|
|
106
|
+
}
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read specific line ranges from a file.
|
|
111
|
+
* Follows Sentry's battle-tested patterns for stream handling.
|
|
112
|
+
*/
|
|
113
|
+
async function readFileLines(filepath, ranges) {
|
|
114
|
+
return new Promise((resolve) => {
|
|
115
|
+
const output = {};
|
|
116
|
+
let resolved = false;
|
|
117
|
+
const stream = createReadStream(filepath);
|
|
118
|
+
const lineReader = createInterface({ input: stream });
|
|
119
|
+
function destroyStreamAndResolve() {
|
|
120
|
+
if (resolved) return;
|
|
121
|
+
resolved = true;
|
|
122
|
+
stream.destroy();
|
|
123
|
+
resolve(output);
|
|
124
|
+
}
|
|
125
|
+
if (ranges.length === 0) {
|
|
126
|
+
destroyStreamAndResolve();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let lineNumber = 0;
|
|
130
|
+
let currentRangeIndex = 0;
|
|
131
|
+
let rangeStart = ranges[0][0];
|
|
132
|
+
let rangeEnd = ranges[0][1];
|
|
133
|
+
function onStreamError() {
|
|
134
|
+
failedCache.set(filepath, true);
|
|
135
|
+
lineReader.close();
|
|
136
|
+
lineReader.removeAllListeners();
|
|
137
|
+
destroyStreamAndResolve();
|
|
138
|
+
}
|
|
139
|
+
stream.on("error", onStreamError);
|
|
140
|
+
lineReader.on("error", onStreamError);
|
|
141
|
+
lineReader.on("close", destroyStreamAndResolve);
|
|
142
|
+
lineReader.on("line", (line) => {
|
|
143
|
+
try {
|
|
144
|
+
lineNumber++;
|
|
145
|
+
if (lineNumber < rangeStart) return;
|
|
146
|
+
output[lineNumber] = snipLine(line);
|
|
147
|
+
if (lineNumber >= rangeEnd) {
|
|
148
|
+
if (currentRangeIndex === ranges.length - 1) {
|
|
149
|
+
lineReader.close();
|
|
150
|
+
lineReader.removeAllListeners();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
currentRangeIndex++;
|
|
154
|
+
const nextRange = ranges[currentRangeIndex];
|
|
155
|
+
rangeStart = nextRange[0];
|
|
156
|
+
rangeEnd = nextRange[1];
|
|
157
|
+
}
|
|
158
|
+
} catch {}
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Extract context lines for a stack trace.
|
|
164
|
+
*/
|
|
165
|
+
async function extractContextLines(stack, contextLines = DEFAULT_CONTEXT_LINES) {
|
|
166
|
+
const parsedFrames = parseStack(stack);
|
|
167
|
+
const framesToEnrich = [];
|
|
168
|
+
for (const frame of parsedFrames) {
|
|
169
|
+
if (!frame.file || !frame.line) continue;
|
|
170
|
+
const filepath = normalizeFilePath(frame.file);
|
|
171
|
+
if (shouldSkipFile(filepath)) continue;
|
|
172
|
+
if (shouldSkipFrame(frame)) continue;
|
|
173
|
+
const range = makeContextRange(frame.line, contextLines);
|
|
174
|
+
framesToEnrich.push({
|
|
175
|
+
frame,
|
|
176
|
+
filepath,
|
|
177
|
+
range
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
if (framesToEnrich.length === 0) return [];
|
|
181
|
+
const fileToRanges = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const { filepath, range } of framesToEnrich) {
|
|
183
|
+
const existing = fileToRanges.get(filepath) || [];
|
|
184
|
+
existing.push(range);
|
|
185
|
+
fileToRanges.set(filepath, existing);
|
|
186
|
+
}
|
|
187
|
+
const readPromises = [];
|
|
188
|
+
for (const [filepath, ranges] of fileToRanges) {
|
|
189
|
+
if (failedCache.has(filepath)) continue;
|
|
190
|
+
const cachedContent = fileCache.get(filepath);
|
|
191
|
+
const mergedRanges = mergeRanges(ranges);
|
|
192
|
+
if (cachedContent && mergedRanges.every(([start, end]) => {
|
|
193
|
+
for (let i = start; i <= end; i++) if (cachedContent[i] === void 0) return false;
|
|
194
|
+
return true;
|
|
195
|
+
})) continue;
|
|
196
|
+
readPromises.push(readFileLines(filepath, mergedRanges).then((lines) => {
|
|
197
|
+
const existing = fileCache.get(filepath) || {};
|
|
198
|
+
fileCache.set(filepath, {
|
|
199
|
+
...existing,
|
|
200
|
+
...lines
|
|
201
|
+
});
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
await Promise.all(readPromises).catch(() => {});
|
|
205
|
+
const enrichedFrames = [];
|
|
206
|
+
for (const { frame, filepath, range } of framesToEnrich) {
|
|
207
|
+
const fileContent = fileCache.get(filepath);
|
|
208
|
+
if (!fileContent) continue;
|
|
209
|
+
const [rangeStart, rangeEnd] = range;
|
|
210
|
+
const preContext = [];
|
|
211
|
+
const postContext = [];
|
|
212
|
+
for (let i = rangeStart; i < frame.line; i++) {
|
|
213
|
+
const line = fileContent[i];
|
|
214
|
+
if (line !== void 0) preContext.push(line);
|
|
215
|
+
}
|
|
216
|
+
const contextLine = fileContent[frame.line];
|
|
217
|
+
if (contextLine === void 0) continue;
|
|
218
|
+
for (let i = frame.line + 1; i <= rangeEnd; i++) {
|
|
219
|
+
const line = fileContent[i];
|
|
220
|
+
if (line !== void 0) postContext.push(line);
|
|
221
|
+
}
|
|
222
|
+
enrichedFrames.push({
|
|
223
|
+
filename: filepath,
|
|
224
|
+
lineno: frame.line,
|
|
225
|
+
colno: frame.col,
|
|
226
|
+
function: frame.function,
|
|
227
|
+
in_app: isInApp(filepath),
|
|
228
|
+
pre_context: preContext,
|
|
229
|
+
context_line: contextLine,
|
|
230
|
+
post_context: postContext
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return enrichedFrames;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Build a cause chain with context lines for each cause.
|
|
237
|
+
* This extracts context lines from each error's stack trace.
|
|
238
|
+
*/
|
|
239
|
+
async function buildCauseChainWithContext(error, contextLines = DEFAULT_CONTEXT_LINES) {
|
|
240
|
+
return _buildCauseChainWithContext(error, /* @__PURE__ */ new Set(), 0, contextLines);
|
|
241
|
+
}
|
|
242
|
+
async function _buildCauseChainWithContext(err, seen, depth, contextLines) {
|
|
243
|
+
if (!isErrorLike(err)) return [];
|
|
244
|
+
if (depth >= MAX_CAUSE_DEPTH) return [{
|
|
245
|
+
type: "Error",
|
|
246
|
+
message: "(max cause depth reached)"
|
|
247
|
+
}];
|
|
248
|
+
if (seen.has(err)) return [{
|
|
249
|
+
type: "Error",
|
|
250
|
+
message: "(circular reference detected)"
|
|
251
|
+
}];
|
|
252
|
+
seen.add(err);
|
|
253
|
+
let frames;
|
|
254
|
+
if (err.stack) try {
|
|
255
|
+
const extracted = await extractContextLines(err.stack, contextLines);
|
|
256
|
+
if (extracted.length > 0) frames = extracted;
|
|
257
|
+
} catch {}
|
|
258
|
+
const current = {
|
|
259
|
+
type: err.name || "Error",
|
|
260
|
+
message: err.message || "",
|
|
261
|
+
stack: err.stack,
|
|
262
|
+
frames
|
|
263
|
+
};
|
|
264
|
+
const cause = getErrorCause(err);
|
|
265
|
+
if (cause) return [current, ...await _buildCauseChainWithContext(cause, seen, depth + 1, contextLines)];
|
|
266
|
+
return [current];
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Record an exception on a span with context lines.
|
|
270
|
+
* This replaces `span.recordException()` with an enriched version.
|
|
271
|
+
*/
|
|
272
|
+
async function recordExceptionWithContext(options) {
|
|
273
|
+
const { span, error, shouldReport = true, contextLines = DEFAULT_CONTEXT_LINES } = options;
|
|
274
|
+
span.setStatus({
|
|
275
|
+
code: SpanStatusCode.ERROR,
|
|
276
|
+
message: error.message
|
|
277
|
+
});
|
|
278
|
+
span.setAttribute("monocle.exception.should_report", shouldReport);
|
|
279
|
+
const causeChain = shouldReport ? await buildCauseChainWithContext(error, contextLines) : buildCauseChain(error);
|
|
280
|
+
const hasCauses = causeChain.length > 1;
|
|
281
|
+
const attributes = {
|
|
282
|
+
"exception.type": error.name,
|
|
283
|
+
"exception.message": error.message,
|
|
284
|
+
"exception.stacktrace": hasCauses ? stackWithCauses(error) : error.stack || ""
|
|
285
|
+
};
|
|
286
|
+
const actualCauses = causeChain.slice(1);
|
|
287
|
+
if (actualCauses.length > 0) attributes["monocle.exception.cause_chain"] = JSON.stringify(actualCauses);
|
|
288
|
+
const mainFrames = causeChain[0]?.frames;
|
|
289
|
+
if (mainFrames && mainFrames.length > 0) attributes["monocle.exception.frames"] = JSON.stringify(mainFrames);
|
|
290
|
+
span.addEvent("exception", attributes);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//#endregion
|
|
294
|
+
export { recordExceptionWithContext };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
//#region src/error_serializer.ts
|
|
2
|
+
/**
|
|
3
|
+
* Maximum number of causes to capture (prevents infinite loops).
|
|
4
|
+
*/
|
|
5
|
+
const MAX_CAUSE_DEPTH = 5;
|
|
6
|
+
/**
|
|
7
|
+
* Check if a value is error-like (has a message property).
|
|
8
|
+
*/
|
|
9
|
+
function isErrorLike(err) {
|
|
10
|
+
return err != null && typeof err.message === "string";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Safely extract the cause from an error.
|
|
14
|
+
* Handles both standard Error.cause and VError-style cause() functions.
|
|
15
|
+
*/
|
|
16
|
+
function getErrorCause(err) {
|
|
17
|
+
if (!err) return void 0;
|
|
18
|
+
const cause = err.cause;
|
|
19
|
+
if (typeof cause === "function") {
|
|
20
|
+
const causeResult = cause();
|
|
21
|
+
return isErrorLike(causeResult) ? causeResult : void 0;
|
|
22
|
+
}
|
|
23
|
+
return isErrorLike(cause) ? cause : void 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Build a combined stack trace from an error and all its causes.
|
|
27
|
+
* Format:
|
|
28
|
+
* Error: Top-level message
|
|
29
|
+
* at file.js:10:5
|
|
30
|
+
* caused by: Error: Mid-level message
|
|
31
|
+
* at file.js:20:3
|
|
32
|
+
*/
|
|
33
|
+
function stackWithCauses(err) {
|
|
34
|
+
return _stackWithCauses(err, /* @__PURE__ */ new Set());
|
|
35
|
+
}
|
|
36
|
+
function _stackWithCauses(err, seen) {
|
|
37
|
+
if (!isErrorLike(err)) return "";
|
|
38
|
+
const stack = err.stack || "";
|
|
39
|
+
if (seen.has(err)) return stack + "\ncauses have become circular...";
|
|
40
|
+
const cause = getErrorCause(err);
|
|
41
|
+
if (cause) {
|
|
42
|
+
seen.add(err);
|
|
43
|
+
return stack + "\ncaused by: " + _stackWithCauses(cause, seen);
|
|
44
|
+
}
|
|
45
|
+
return stack;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build a structured cause chain from an error.
|
|
49
|
+
* Returns an array of { type, message, stack } objects.
|
|
50
|
+
* Limited to MAX_CAUSE_DEPTH to prevent infinite loops.
|
|
51
|
+
*/
|
|
52
|
+
function buildCauseChain(err) {
|
|
53
|
+
return _buildCauseChain(err, /* @__PURE__ */ new Set(), 0);
|
|
54
|
+
}
|
|
55
|
+
function _buildCauseChain(err, seen, depth) {
|
|
56
|
+
if (!isErrorLike(err)) return [];
|
|
57
|
+
if (depth >= MAX_CAUSE_DEPTH) return [{
|
|
58
|
+
type: "Error",
|
|
59
|
+
message: "(max cause depth reached)"
|
|
60
|
+
}];
|
|
61
|
+
if (seen.has(err)) return [{
|
|
62
|
+
type: "Error",
|
|
63
|
+
message: "(circular reference detected)"
|
|
64
|
+
}];
|
|
65
|
+
seen.add(err);
|
|
66
|
+
const current = {
|
|
67
|
+
type: err.name || "Error",
|
|
68
|
+
message: err.message || "",
|
|
69
|
+
stack: err.stack
|
|
70
|
+
};
|
|
71
|
+
const cause = getErrorCause(err);
|
|
72
|
+
if (cause) return [current, ..._buildCauseChain(cause, seen, depth + 1)];
|
|
73
|
+
return [current];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { MAX_CAUSE_DEPTH, buildCauseChain, getErrorCause, isErrorLike, stackWithCauses };
|
package/dist/src/monocle.d.mts
CHANGED
|
@@ -17,8 +17,11 @@ declare class Monocle {
|
|
|
17
17
|
/**
|
|
18
18
|
* Capture an exception and record it on the current active span.
|
|
19
19
|
* If no span is active, the exception is silently ignored.
|
|
20
|
+
*
|
|
21
|
+
* This method is async to allow for source context extraction.
|
|
22
|
+
* You can choose to await it or fire-and-forget.
|
|
20
23
|
*/
|
|
21
|
-
static captureException(error: unknown, context?: CaptureExceptionContext): void
|
|
24
|
+
static captureException(error: unknown, context?: CaptureExceptionContext): Promise<void>;
|
|
22
25
|
/**
|
|
23
26
|
* Set user information on the current active span.
|
|
24
27
|
*/
|
package/dist/src/monocle.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { recordExceptionWithContext } from "./context_lines.mjs";
|
|
2
|
+
import { trace } from "@opentelemetry/api";
|
|
2
3
|
import { setUser } from "@adonisjs/otel/helpers";
|
|
3
4
|
|
|
4
5
|
//#region src/monocle.ts
|
|
@@ -9,15 +10,16 @@ var Monocle = class {
|
|
|
9
10
|
/**
|
|
10
11
|
* Capture an exception and record it on the current active span.
|
|
11
12
|
* If no span is active, the exception is silently ignored.
|
|
13
|
+
*
|
|
14
|
+
* This method is async to allow for source context extraction.
|
|
15
|
+
* You can choose to await it or fire-and-forget.
|
|
12
16
|
*/
|
|
13
|
-
static captureException(error, context$1) {
|
|
17
|
+
static async captureException(error, context$1) {
|
|
14
18
|
const span = trace.getActiveSpan();
|
|
15
19
|
if (!span) return;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
code: SpanStatusCode.ERROR,
|
|
20
|
-
message: err.message
|
|
20
|
+
await recordExceptionWithContext({
|
|
21
|
+
span,
|
|
22
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
21
23
|
});
|
|
22
24
|
if (context$1?.user) {
|
|
23
25
|
if (context$1.user.id) span.setAttribute("user.id", context$1.user.id);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@monocle.sh/adonisjs-agent",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.6",
|
|
4
4
|
"description": "Monocle agent for AdonisJS - sends telemetry to Monocle cloud",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"adonisjs",
|
|
@@ -52,7 +52,9 @@
|
|
|
52
52
|
"build": "tsdown",
|
|
53
53
|
"dev": "tsdown",
|
|
54
54
|
"typecheck": "tsc --noEmit",
|
|
55
|
-
"
|
|
55
|
+
"test": "node --import tsx bin/test.ts",
|
|
56
|
+
"quick:test": "node --import tsx bin/test.ts",
|
|
57
|
+
"release": "release-it --preRelease=beta",
|
|
56
58
|
"prepublishOnly": "pnpm run build"
|
|
57
59
|
},
|
|
58
60
|
"dependencies": {
|
|
@@ -65,12 +67,19 @@
|
|
|
65
67
|
"@opentelemetry/sdk-metrics": "^2.2.0",
|
|
66
68
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
67
69
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
70
|
+
"@sindresorhus/is": "^7.2.0",
|
|
71
|
+
"error-stack-parser-es": "^1.0.5",
|
|
68
72
|
"import-in-the-middle": "^2.0.1"
|
|
69
73
|
},
|
|
70
74
|
"devDependencies": {
|
|
71
75
|
"@adonisjs/core": "catalog:",
|
|
72
76
|
"@adonisjs/tsconfig": "catalog:",
|
|
73
|
-
"
|
|
77
|
+
"@japa/assert": "^4.2.0",
|
|
78
|
+
"@japa/file-system": "^2.3.2",
|
|
79
|
+
"@japa/runner": "^4.4.0",
|
|
80
|
+
"@japa/snapshot": "^2.0.10",
|
|
81
|
+
"release-it": "^19.2.2",
|
|
82
|
+
"tsx": "^4.19.4"
|
|
74
83
|
},
|
|
75
84
|
"peerDependencies": {
|
|
76
85
|
"@adonisjs/core": "^6.2.0 || ^7.0.0"
|