@hellsoft/vaka-node 0.0.0-stage → 0.3.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 +24 -2
- package/dist/index.cjs +1020 -0
- package/dist/index.d.ts +196 -0
- package/dist/index.mjs +953 -0
- package/dist/integrations.d.ts +45 -0
- package/dist/otel.d.ts +38 -0
- package/dist/register.cjs +41 -0
- package/dist/register.d.ts +1 -0
- package/dist/register.mjs +16 -0
- package/dist/scope.d.ts +44 -0
- package/dist/stack.d.ts +16 -0
- package/dist/transport.d.ts +33 -0
- package/package.json +66 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
FeedbackError: () => FeedbackError,
|
|
34
|
+
SDK: () => SDK,
|
|
35
|
+
VakaScopeSpanProcessor: () => VakaScopeSpanProcessor,
|
|
36
|
+
_globalScope: () => globalScope,
|
|
37
|
+
_reset: () => _reset,
|
|
38
|
+
addBreadcrumb: () => addBreadcrumb,
|
|
39
|
+
captureError: () => captureError,
|
|
40
|
+
captureException: () => captureException,
|
|
41
|
+
captureFeedback: () => captureFeedback,
|
|
42
|
+
captureMessage: () => captureMessage,
|
|
43
|
+
close: () => close,
|
|
44
|
+
currentScope: () => currentScope,
|
|
45
|
+
errorHandler: () => errorHandler,
|
|
46
|
+
fastifyPlugin: () => fastifyPlugin,
|
|
47
|
+
flush: () => flush,
|
|
48
|
+
getFlag: () => getFlag,
|
|
49
|
+
getFlags: () => getFlags,
|
|
50
|
+
init: () => init,
|
|
51
|
+
isEnabled: () => isEnabled,
|
|
52
|
+
isInitialized: () => isInitialized,
|
|
53
|
+
parseStack: () => parseStack,
|
|
54
|
+
requestHandler: () => requestHandler,
|
|
55
|
+
setContext: () => setContext,
|
|
56
|
+
setTag: () => setTag,
|
|
57
|
+
setTags: () => setTags,
|
|
58
|
+
setUser: () => setUser,
|
|
59
|
+
setupExpressErrorHandler: () => setupExpressErrorHandler,
|
|
60
|
+
traceId: () => traceId,
|
|
61
|
+
track: () => track,
|
|
62
|
+
vakaLogRecordProcessor: () => vakaLogRecordProcessor,
|
|
63
|
+
vakaSpanExporter: () => vakaSpanExporter,
|
|
64
|
+
withScope: () => withScope,
|
|
65
|
+
wrapHandler: () => wrapHandler
|
|
66
|
+
});
|
|
67
|
+
module.exports = __toCommonJS(index_exports);
|
|
68
|
+
var import_node_crypto = require("node:crypto");
|
|
69
|
+
var import_node_os = require("node:os");
|
|
70
|
+
var import_api2 = require("@opentelemetry/api");
|
|
71
|
+
|
|
72
|
+
// src/stack.ts
|
|
73
|
+
var import_node_fs = require("node:fs");
|
|
74
|
+
var import_node_url = require("node:url");
|
|
75
|
+
var V8 = /^\s*at (?:(.+?) \()?((?:[a-z][a-z0-9+.-]*:|\/|[a-z]:\\|\\\\)[^)]*?):(\d+):(\d+)\)?\s*$/i;
|
|
76
|
+
var V8_NOPOS = /^\s*at (?:(.+?) \()?(native|<anonymous>|node:[^)]+)\)?\s*$/i;
|
|
77
|
+
function parseStack(stack, cwd = safeCwd()) {
|
|
78
|
+
if (!stack) return [];
|
|
79
|
+
const frames = [];
|
|
80
|
+
for (const line of stack.split("\n").slice(0, 100)) {
|
|
81
|
+
let fn;
|
|
82
|
+
let path;
|
|
83
|
+
let lineno;
|
|
84
|
+
let colno;
|
|
85
|
+
const m = V8.exec(line);
|
|
86
|
+
if (m) {
|
|
87
|
+
fn = m[1];
|
|
88
|
+
path = m[2];
|
|
89
|
+
lineno = +m[3];
|
|
90
|
+
colno = +m[4];
|
|
91
|
+
} else {
|
|
92
|
+
const n = V8_NOPOS.exec(line);
|
|
93
|
+
if (!n) continue;
|
|
94
|
+
fn = n[1];
|
|
95
|
+
path = n[2];
|
|
96
|
+
}
|
|
97
|
+
if (path.startsWith("file://")) {
|
|
98
|
+
try {
|
|
99
|
+
path = (0, import_node_url.fileURLToPath)(path);
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (fn) fn = fn.replace(/^async /, "").replace(/^new /, "").replace(/ \[as [^\]]+\]$/, "");
|
|
104
|
+
const internal = /^node:|^native$|^<anonymous>$|^internal\//.test(path);
|
|
105
|
+
const vendor = /[\\/]node_modules[\\/]/.test(path);
|
|
106
|
+
frames.push({
|
|
107
|
+
function: fn || "<anonymous>",
|
|
108
|
+
module: moduleName(path, cwd),
|
|
109
|
+
abs_path: path,
|
|
110
|
+
filename: relative(path, cwd),
|
|
111
|
+
lineno,
|
|
112
|
+
colno,
|
|
113
|
+
in_app: !internal && !vendor
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return frames;
|
|
117
|
+
}
|
|
118
|
+
function safeCwd() {
|
|
119
|
+
try {
|
|
120
|
+
return process.cwd();
|
|
121
|
+
} catch {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function relative(path, cwd) {
|
|
126
|
+
if (cwd && path.startsWith(cwd + "/")) return path.slice(cwd.length + 1);
|
|
127
|
+
if (cwd && path.startsWith(cwd + "\\")) return path.slice(cwd.length + 1).replace(/\\/g, "/");
|
|
128
|
+
return path;
|
|
129
|
+
}
|
|
130
|
+
function moduleName(path, cwd) {
|
|
131
|
+
const norm = path.replace(/\\/g, "/");
|
|
132
|
+
const i = norm.lastIndexOf("/node_modules/");
|
|
133
|
+
if (i >= 0) {
|
|
134
|
+
const rest = norm.slice(i + 14).split("/");
|
|
135
|
+
const pkg = rest[0].startsWith("@") ? rest.slice(0, 2).join("/") : rest[0];
|
|
136
|
+
const file = rest.slice(rest[0].startsWith("@") ? 2 : 1).join("/");
|
|
137
|
+
return `${pkg}:${file.replace(/\.[cm]?[jt]sx?$/, "")}`;
|
|
138
|
+
}
|
|
139
|
+
if (norm.startsWith("node:")) return norm.slice(5);
|
|
140
|
+
const rel = relative(path, cwd).replace(/\\/g, "/");
|
|
141
|
+
return rel.startsWith("/") ? void 0 : rel.replace(/\.[cm]?[jt]sx?$/, "");
|
|
142
|
+
}
|
|
143
|
+
var CONTEXT_LINES = 5;
|
|
144
|
+
var MAX_FILE_BYTES = 1 << 20;
|
|
145
|
+
var cache = /* @__PURE__ */ new Map();
|
|
146
|
+
function lines(path) {
|
|
147
|
+
if (cache.has(path)) return cache.get(path);
|
|
148
|
+
let out = null;
|
|
149
|
+
try {
|
|
150
|
+
if ((0, import_node_fs.statSync)(path).size <= MAX_FILE_BYTES) out = (0, import_node_fs.readFileSync)(path, "utf8").split(/\r?\n/);
|
|
151
|
+
} catch {
|
|
152
|
+
out = null;
|
|
153
|
+
}
|
|
154
|
+
if (cache.size > 200) cache.delete(cache.keys().next().value);
|
|
155
|
+
cache.set(path, out);
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function addSourceContext(frames, maxFrames = 10) {
|
|
159
|
+
let n = 0;
|
|
160
|
+
for (const f of frames) {
|
|
161
|
+
if (n >= maxFrames) break;
|
|
162
|
+
if (!f.in_app || !f.abs_path || !f.lineno) continue;
|
|
163
|
+
const src = lines(f.abs_path);
|
|
164
|
+
if (!src || f.lineno > src.length) continue;
|
|
165
|
+
n++;
|
|
166
|
+
const i = f.lineno - 1;
|
|
167
|
+
const cut = (s) => s.length > 300 ? s.slice(0, 300) + "\u2026" : s;
|
|
168
|
+
f.context_line = cut(src[i]);
|
|
169
|
+
f.pre_context = src.slice(Math.max(0, i - CONTEXT_LINES), i).map(cut);
|
|
170
|
+
f.post_context = src.slice(i + 1, i + 1 + CONTEXT_LINES).map(cut);
|
|
171
|
+
}
|
|
172
|
+
return frames;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/scope.ts
|
|
176
|
+
var import_node_async_hooks = require("node:async_hooks");
|
|
177
|
+
var MAX_BREADCRUMBS = 100;
|
|
178
|
+
var Scope = class _Scope {
|
|
179
|
+
user;
|
|
180
|
+
tags = {};
|
|
181
|
+
contexts = {};
|
|
182
|
+
breadcrumbs = [];
|
|
183
|
+
flags = {};
|
|
184
|
+
request;
|
|
185
|
+
transaction;
|
|
186
|
+
clone() {
|
|
187
|
+
const s = new _Scope();
|
|
188
|
+
s.user = this.user;
|
|
189
|
+
s.tags = { ...this.tags };
|
|
190
|
+
s.contexts = { ...this.contexts };
|
|
191
|
+
s.breadcrumbs = this.breadcrumbs.slice();
|
|
192
|
+
s.flags = { ...this.flags };
|
|
193
|
+
s.request = this.request;
|
|
194
|
+
s.transaction = this.transaction;
|
|
195
|
+
return s;
|
|
196
|
+
}
|
|
197
|
+
addBreadcrumb(b, max = MAX_BREADCRUMBS) {
|
|
198
|
+
this.breadcrumbs.push({ timestamp: (/* @__PURE__ */ new Date()).toISOString(), level: "info", ...b });
|
|
199
|
+
if (this.breadcrumbs.length > max) this.breadcrumbs.splice(0, this.breadcrumbs.length - max);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
var storage = new import_node_async_hooks.AsyncLocalStorage();
|
|
203
|
+
var globalScope = new Scope();
|
|
204
|
+
function currentScope() {
|
|
205
|
+
return storage.getStore() || globalScope;
|
|
206
|
+
}
|
|
207
|
+
function withScope(fn) {
|
|
208
|
+
const scope = currentScope().clone();
|
|
209
|
+
return storage.run(scope, () => fn(scope));
|
|
210
|
+
}
|
|
211
|
+
function _resetScopes() {
|
|
212
|
+
globalScope = new Scope();
|
|
213
|
+
storage.disable();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/transport.ts
|
|
217
|
+
var import_node_zlib = require("node:zlib");
|
|
218
|
+
var import_api = require("@opentelemetry/api");
|
|
219
|
+
var import_core = require("@opentelemetry/core");
|
|
220
|
+
var MAX_BATCH = 1e3;
|
|
221
|
+
var MAX_QUEUE = 5e3;
|
|
222
|
+
var Transport = class {
|
|
223
|
+
constructor(o) {
|
|
224
|
+
this.o = o;
|
|
225
|
+
}
|
|
226
|
+
queue = [];
|
|
227
|
+
timer;
|
|
228
|
+
backoffUntil = 0;
|
|
229
|
+
backoffMs = 0;
|
|
230
|
+
inflight = /* @__PURE__ */ new Set();
|
|
231
|
+
disabled = false;
|
|
232
|
+
send(item, urgent = false) {
|
|
233
|
+
if (this.disabled) return;
|
|
234
|
+
this.queue.push(item);
|
|
235
|
+
if (this.queue.length > MAX_QUEUE) this.queue.splice(0, this.queue.length - MAX_QUEUE);
|
|
236
|
+
if (urgent || this.queue.length >= 100) this.schedule(urgent ? 0 : 10);
|
|
237
|
+
else this.schedule(this.o.flushIntervalMs);
|
|
238
|
+
}
|
|
239
|
+
schedule(ms) {
|
|
240
|
+
if (this.timer) {
|
|
241
|
+
if (ms > 0) return;
|
|
242
|
+
clearTimeout(this.timer);
|
|
243
|
+
}
|
|
244
|
+
this.timer = setTimeout(() => {
|
|
245
|
+
this.timer = void 0;
|
|
246
|
+
void this.drain();
|
|
247
|
+
}, ms);
|
|
248
|
+
this.timer.unref?.();
|
|
249
|
+
}
|
|
250
|
+
async drain() {
|
|
251
|
+
while (this.queue.length && !this.disabled) {
|
|
252
|
+
if (Date.now() < this.backoffUntil) {
|
|
253
|
+
this.schedule(this.backoffUntil - Date.now());
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const items = this.queue.splice(0, MAX_BATCH);
|
|
257
|
+
const p = this.post(items);
|
|
258
|
+
this.inflight.add(p);
|
|
259
|
+
try {
|
|
260
|
+
await p;
|
|
261
|
+
} finally {
|
|
262
|
+
this.inflight.delete(p);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async post(items) {
|
|
267
|
+
const json = JSON.stringify({ sdk: this.o.sdk, items });
|
|
268
|
+
const headers2 = { "Content-Type": "application/json", "X-Vaka-Key": this.o.key, "X-Vaka-Sdk": `${this.o.sdk.name}/${this.o.sdk.version}` };
|
|
269
|
+
let body = json;
|
|
270
|
+
if (json.length > 2048) {
|
|
271
|
+
body = (0, import_node_zlib.gzipSync)(json);
|
|
272
|
+
headers2["Content-Encoding"] = "gzip";
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
const r = await import_api.context.with(
|
|
276
|
+
(0, import_core.suppressTracing)(import_api.context.active()),
|
|
277
|
+
() => fetch(`${this.o.endpoint}/ingest/v1/batch`, { method: "POST", headers: headers2, body, signal: AbortSignal.timeout(15e3) })
|
|
278
|
+
);
|
|
279
|
+
if (r.status === 429 || r.status >= 500) return this.retry(items, r.status);
|
|
280
|
+
this.backoffMs = 0;
|
|
281
|
+
if (r.status === 401) {
|
|
282
|
+
this.disabled = true;
|
|
283
|
+
this.queue = [];
|
|
284
|
+
this.o.log("ingest key rejected (401); disabling until restart");
|
|
285
|
+
} else if (r.status === 413 && items.length > 1) {
|
|
286
|
+
const half = Math.ceil(items.length / 2);
|
|
287
|
+
await this.post(items.slice(0, half));
|
|
288
|
+
await this.post(items.slice(half));
|
|
289
|
+
} else if (r.status >= 400) {
|
|
290
|
+
this.o.log("batch dropped", r.status, await r.text().catch(() => ""));
|
|
291
|
+
} else {
|
|
292
|
+
this.o.log("sent", items.length, "items", r.status);
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
this.retry(items, String(e?.message || e));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
retry(items, why) {
|
|
299
|
+
this.backoffMs = Math.min(3e5, this.backoffMs ? this.backoffMs * 2 : 2e3);
|
|
300
|
+
this.backoffUntil = Date.now() + this.backoffMs;
|
|
301
|
+
this.queue = items.concat(this.queue).slice(0, MAX_QUEUE);
|
|
302
|
+
this.o.log("send failed, retrying in", this.backoffMs, "ms:", why);
|
|
303
|
+
this.schedule(this.backoffMs);
|
|
304
|
+
}
|
|
305
|
+
/** Send everything queued now, waiting at most `timeoutMs`. Resolves true when all was sent. */
|
|
306
|
+
async flush(timeoutMs = 5e3) {
|
|
307
|
+
if (this.timer) {
|
|
308
|
+
clearTimeout(this.timer);
|
|
309
|
+
this.timer = void 0;
|
|
310
|
+
}
|
|
311
|
+
this.backoffUntil = 0;
|
|
312
|
+
const work = (async () => {
|
|
313
|
+
await Promise.all([...this.inflight]);
|
|
314
|
+
await this.drain();
|
|
315
|
+
return this.queue.length === 0;
|
|
316
|
+
})();
|
|
317
|
+
let t;
|
|
318
|
+
const timeout = new Promise((res) => {
|
|
319
|
+
t = setTimeout(() => res(false), timeoutMs);
|
|
320
|
+
t.unref?.();
|
|
321
|
+
});
|
|
322
|
+
try {
|
|
323
|
+
return await Promise.race([work, timeout]);
|
|
324
|
+
} finally {
|
|
325
|
+
if (t) clearTimeout(t);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
pending() {
|
|
329
|
+
return this.queue.length;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/otel.ts
|
|
334
|
+
var import_sdk_node = require("@opentelemetry/sdk-node");
|
|
335
|
+
var import_exporter_trace_otlp_proto = require("@opentelemetry/exporter-trace-otlp-proto");
|
|
336
|
+
var import_exporter_logs_otlp_proto = require("@opentelemetry/exporter-logs-otlp-proto");
|
|
337
|
+
var import_sdk_logs = require("@opentelemetry/sdk-logs");
|
|
338
|
+
var import_sdk_trace = require("@opentelemetry/sdk-trace");
|
|
339
|
+
var import_resources = require("@opentelemetry/resources");
|
|
340
|
+
var import_instrumentation_http = require("@opentelemetry/instrumentation-http");
|
|
341
|
+
var headers = (key) => ({ "x-vaka-key": key });
|
|
342
|
+
function vakaSpanExporter(o) {
|
|
343
|
+
return new import_exporter_trace_otlp_proto.OTLPTraceExporter({ url: `${o.endpoint.replace(/\/+$/, "")}/ingest/otlp/v1/traces`, headers: headers(o.key) });
|
|
344
|
+
}
|
|
345
|
+
function vakaLogRecordProcessor(o) {
|
|
346
|
+
return new import_sdk_logs.BatchLogRecordProcessor({ exporter: new import_exporter_logs_otlp_proto.OTLPLogExporter({ url: `${o.endpoint.replace(/\/+$/, "")}/ingest/otlp/v1/logs`, headers: headers(o.key) }) });
|
|
347
|
+
}
|
|
348
|
+
var VakaScopeSpanProcessor = class {
|
|
349
|
+
onStart(span, _ctx) {
|
|
350
|
+
const s = currentScope();
|
|
351
|
+
if (s.user?.id) span.setAttribute("enduser.id", s.user.id);
|
|
352
|
+
for (const k of Object.keys(s.flags)) span.setAttribute(`feature_flag.${k}`, s.flags[k]);
|
|
353
|
+
}
|
|
354
|
+
onEnd() {
|
|
355
|
+
}
|
|
356
|
+
forceFlush() {
|
|
357
|
+
return Promise.resolve();
|
|
358
|
+
}
|
|
359
|
+
shutdown() {
|
|
360
|
+
return Promise.resolve();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
function startOtel(c) {
|
|
364
|
+
const attrs = { ...c.resourceAttributes || {} };
|
|
365
|
+
if (c.serviceName) attrs["service.name"] = c.serviceName;
|
|
366
|
+
if (c.release) attrs["service.version"] = c.release;
|
|
367
|
+
if (c.environment) attrs["deployment.environment.name"] = c.environment;
|
|
368
|
+
const rate = Number.isFinite(c.tracesSampleRate) ? Math.max(0, Math.min(1, c.tracesSampleRate)) : 1;
|
|
369
|
+
const sdk = new import_sdk_node.NodeSDK({
|
|
370
|
+
resource: (0, import_resources.defaultResource)().merge((0, import_resources.resourceFromAttributes)(attrs)),
|
|
371
|
+
serviceName: c.serviceName,
|
|
372
|
+
sampler: new import_sdk_trace.ParentBasedSampler({ root: new import_sdk_trace.TraceIdRatioBasedSampler(rate) }),
|
|
373
|
+
spanProcessors: [new VakaScopeSpanProcessor(), new import_sdk_trace.BatchSpanProcessor({ exporter: vakaSpanExporter(c) })],
|
|
374
|
+
logRecordProcessors: c.logs ? [vakaLogRecordProcessor(c)] : [],
|
|
375
|
+
metricReaders: [],
|
|
376
|
+
instrumentations: c.instrumentations || [new import_instrumentation_http.HttpInstrumentation()]
|
|
377
|
+
});
|
|
378
|
+
sdk.start();
|
|
379
|
+
return sdk;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/integrations.ts
|
|
383
|
+
var import_node_diagnostics_channel = __toESM(require("node:diagnostics_channel"));
|
|
384
|
+
var import_node_util = require("node:util");
|
|
385
|
+
var addCrumb = () => void 0;
|
|
386
|
+
var ownEndpoint = "";
|
|
387
|
+
var unhooks = [];
|
|
388
|
+
function installBreadcrumbs(endpoint, add) {
|
|
389
|
+
addCrumb = add;
|
|
390
|
+
ownEndpoint = endpoint;
|
|
391
|
+
for (const level of ["warn", "error"]) {
|
|
392
|
+
const orig = console[level];
|
|
393
|
+
const wrapped = function(...args) {
|
|
394
|
+
try {
|
|
395
|
+
addCrumb({ category: "console", level: level === "warn" ? "warning" : "error", message: (0, import_node_util.format)(...args).slice(0, 500) });
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
return orig.apply(this, args);
|
|
399
|
+
};
|
|
400
|
+
console[level] = wrapped;
|
|
401
|
+
unhooks.push(() => {
|
|
402
|
+
if (console[level] === wrapped) console[level] = orig;
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
const starts = /* @__PURE__ */ new WeakMap();
|
|
406
|
+
sub("undici:request:create", (m) => {
|
|
407
|
+
starts.set(m.request, { t: Date.now(), scope: currentScope() });
|
|
408
|
+
});
|
|
409
|
+
sub("undici:request:headers", (m) => {
|
|
410
|
+
const s = starts.get(m.request);
|
|
411
|
+
httpCrumb(s?.scope, m.request.method || "GET", `${m.request.origin || ""}${m.request.path || ""}`, m.response.statusCode, s?.t);
|
|
412
|
+
});
|
|
413
|
+
sub("undici:request:error", (m) => {
|
|
414
|
+
const s = starts.get(m.request);
|
|
415
|
+
httpCrumb(s?.scope, m.request.method || "GET", `${m.request.origin || ""}${m.request.path || ""}`, void 0, s?.t, m.error);
|
|
416
|
+
});
|
|
417
|
+
const httpStarts = /* @__PURE__ */ new WeakMap();
|
|
418
|
+
sub("http.client.request.start", (m) => httpStarts.set(m.request, { t: Date.now(), scope: currentScope() }));
|
|
419
|
+
sub("http.client.response.finish", (m) => {
|
|
420
|
+
const s = httpStarts.get(m.request);
|
|
421
|
+
httpCrumb(s?.scope, m.request.method || "GET", `${m.request.protocol || "http:"}//${m.request.host || ""}${m.request.path || ""}`, m.response.statusCode, s?.t);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function sub(name, fn) {
|
|
425
|
+
const safe = (m) => {
|
|
426
|
+
try {
|
|
427
|
+
fn(m);
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
import_node_diagnostics_channel.default.subscribe(name, safe);
|
|
432
|
+
unhooks.push(() => import_node_diagnostics_channel.default.unsubscribe(name, safe));
|
|
433
|
+
}
|
|
434
|
+
function stripQuery(url) {
|
|
435
|
+
const i = url.search(/[?#]/);
|
|
436
|
+
return i >= 0 ? url.slice(0, i) : url;
|
|
437
|
+
}
|
|
438
|
+
function httpCrumb(scope, method, url, status, t, err) {
|
|
439
|
+
if (ownEndpoint && url.startsWith(ownEndpoint)) return;
|
|
440
|
+
const clean2 = stripQuery(url);
|
|
441
|
+
const level = err ? "error" : status && status >= 500 ? "error" : status && status >= 400 ? "warning" : "info";
|
|
442
|
+
const b = {
|
|
443
|
+
category: "http",
|
|
444
|
+
level,
|
|
445
|
+
message: err ? `${method} ${clean2} failed: ${err.message}` : `${method} ${clean2} ${status}`,
|
|
446
|
+
data: { method, url: clean2, status_code: status, duration_ms: t ? Date.now() - t : void 0 }
|
|
447
|
+
};
|
|
448
|
+
if (scope) scope.addBreadcrumb(b);
|
|
449
|
+
else addCrumb(b);
|
|
450
|
+
}
|
|
451
|
+
function uninstallIntegrations() {
|
|
452
|
+
while (unhooks.length) unhooks.pop()();
|
|
453
|
+
}
|
|
454
|
+
function installProcessHandlers(h) {
|
|
455
|
+
const onUncaught = (err, origin) => {
|
|
456
|
+
h.capture(err, true, origin === "unhandledRejection" ? "unhandledRejection" : "uncaughtException");
|
|
457
|
+
if (!h.exitOnUncaught || process.listenerCount("uncaughtException") > 1) return;
|
|
458
|
+
process.stderr.write(`${err?.stack || String(err)}
|
|
459
|
+
`);
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
h.flush(h.shutdownTimeoutMs).finally(() => process.exit(1));
|
|
462
|
+
};
|
|
463
|
+
const onRejection = (reason) => {
|
|
464
|
+
const fatal = h.unhandledRejection === "strict" && process.listenerCount("unhandledRejection") <= 1;
|
|
465
|
+
h.capture(reason, fatal, "unhandledRejection");
|
|
466
|
+
if (process.listenerCount("unhandledRejection") > 1 || h.unhandledRejection === "none") return;
|
|
467
|
+
const text = reason?.stack || String(reason);
|
|
468
|
+
if (h.unhandledRejection === "warn") {
|
|
469
|
+
process.emitWarning(`Unhandled promise rejection: ${text}`);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
process.stderr.write(`${text}
|
|
473
|
+
`);
|
|
474
|
+
process.exitCode = 1;
|
|
475
|
+
h.flush(h.shutdownTimeoutMs).finally(() => process.exit(1));
|
|
476
|
+
};
|
|
477
|
+
process.on("uncaughtException", onUncaught);
|
|
478
|
+
process.on("unhandledRejection", onRejection);
|
|
479
|
+
unhooks.push(() => {
|
|
480
|
+
process.off("uncaughtException", onUncaught);
|
|
481
|
+
process.off("unhandledRejection", onRejection);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
function nodeRejectionMode() {
|
|
485
|
+
const args = [...process.execArgv, ...(process.env.NODE_OPTIONS || "").split(/\s+/)];
|
|
486
|
+
const flag = args.find((a) => a.startsWith("--unhandled-rejections="));
|
|
487
|
+
const v = flag ? flag.split("=")[1] : "throw";
|
|
488
|
+
if (v === "none") return "none";
|
|
489
|
+
if (v === "warn" || v === "warn-with-error-code") return "warn";
|
|
490
|
+
return "strict";
|
|
491
|
+
}
|
|
492
|
+
var SAFE_HEADERS = ["user-agent", "content-type", "content-length", "accept", "referer", "x-request-id", "traceparent"];
|
|
493
|
+
function requestInfo(req) {
|
|
494
|
+
const raw = req.originalUrl || req.url || "";
|
|
495
|
+
const q = raw.indexOf("?");
|
|
496
|
+
const path = q >= 0 ? raw.slice(0, q) : raw;
|
|
497
|
+
const headers2 = {};
|
|
498
|
+
for (const h of SAFE_HEADERS) {
|
|
499
|
+
const v = req.headers?.[h];
|
|
500
|
+
if (v) headers2[h] = Array.isArray(v) ? v.join(", ") : v;
|
|
501
|
+
}
|
|
502
|
+
const host = req.headers?.["x-forwarded-host"] || req.headers?.host;
|
|
503
|
+
const proto = req.protocol || req.headers?.["x-forwarded-proto"] || "http";
|
|
504
|
+
return {
|
|
505
|
+
method: req.method,
|
|
506
|
+
url: host ? `${proto}://${host}${path}` : path,
|
|
507
|
+
route: routeOf(req),
|
|
508
|
+
query_string: q >= 0 ? raw.slice(q + 1, q + 1 + 500) : void 0,
|
|
509
|
+
headers: headers2
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
function routeOf(req) {
|
|
513
|
+
return req.routeOptions?.url || req.routerPath || req.route?.path;
|
|
514
|
+
}
|
|
515
|
+
function makeRequestHandler() {
|
|
516
|
+
return function vakaRequestHandler(req, _res, next) {
|
|
517
|
+
const scope = currentScope().clone();
|
|
518
|
+
scope.breadcrumbs = [];
|
|
519
|
+
scope.request = requestInfo(req);
|
|
520
|
+
storage.run(scope, () => next());
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function makeErrorHandler(capture, shouldCapture2) {
|
|
524
|
+
return function vakaErrorHandler(err, req, res, next) {
|
|
525
|
+
const e = err;
|
|
526
|
+
const status = e?.status || e?.statusCode || (res?.statusCode && res.statusCode >= 400 ? res.statusCode : 500);
|
|
527
|
+
if (shouldCapture2(status)) {
|
|
528
|
+
const scope = currentScope();
|
|
529
|
+
scope.request = { ...requestInfo(req), ...scope.request?.route ? { route: scope.request.route } : {} };
|
|
530
|
+
if (!scope.request.route) scope.request.route = routeOf(req);
|
|
531
|
+
capture(err, { handled: false, mechanism: "express", status });
|
|
532
|
+
}
|
|
533
|
+
next(err);
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
function makeFastifyPlugin(capture, shouldCapture2) {
|
|
537
|
+
const plugin = function vakaFastify(fastify, _opts, done) {
|
|
538
|
+
fastify.addHook("onRequest", (req, _reply, next) => {
|
|
539
|
+
const scope = currentScope().clone();
|
|
540
|
+
scope.breadcrumbs = [];
|
|
541
|
+
scope.request = requestInfo(req);
|
|
542
|
+
storage.run(scope, next);
|
|
543
|
+
});
|
|
544
|
+
fastify.addHook("onError", (req, _reply, err, next) => {
|
|
545
|
+
const status = err?.statusCode || 500;
|
|
546
|
+
if (shouldCapture2(status)) {
|
|
547
|
+
const scope = currentScope();
|
|
548
|
+
scope.request = requestInfo(req);
|
|
549
|
+
capture(err, { handled: false, mechanism: "fastify", status });
|
|
550
|
+
}
|
|
551
|
+
next();
|
|
552
|
+
});
|
|
553
|
+
done();
|
|
554
|
+
};
|
|
555
|
+
plugin[Symbol.for("skip-override")] = true;
|
|
556
|
+
plugin[Symbol.for("fastify.display-name")] = "vaka";
|
|
557
|
+
return plugin;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// src/index.ts
|
|
561
|
+
var SDK = { name: "vaka-node", version: "0.3.0" };
|
|
562
|
+
var opts;
|
|
563
|
+
var transport;
|
|
564
|
+
var otelSdk;
|
|
565
|
+
var processId = (0, import_node_crypto.randomUUID)();
|
|
566
|
+
function log(...a) {
|
|
567
|
+
if (opts?.debug) process.stderr.write(`[vaka] ${a.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join(" ")}
|
|
568
|
+
`);
|
|
569
|
+
}
|
|
570
|
+
function env(...names) {
|
|
571
|
+
for (const n of names) {
|
|
572
|
+
const v = process.env[n];
|
|
573
|
+
if (v) return v;
|
|
574
|
+
}
|
|
575
|
+
return void 0;
|
|
576
|
+
}
|
|
577
|
+
function init(options = {}) {
|
|
578
|
+
if (opts) return;
|
|
579
|
+
options = Object.fromEntries(Object.entries(options).filter(([, v]) => v !== void 0));
|
|
580
|
+
const key = options.key || env("VAKA_KEY", "VAKA_INGEST_KEY");
|
|
581
|
+
const endpoint = (options.endpoint || env("VAKA_ENDPOINT") || "").replace(/\/+$/, "");
|
|
582
|
+
if (!key || !endpoint) {
|
|
583
|
+
process.emitWarning("[vaka] init needs { key, endpoint } (or VAKA_KEY and VAKA_ENDPOINT); SDK disabled");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
opts = {
|
|
587
|
+
otel: true,
|
|
588
|
+
tracesSampleRate: 1,
|
|
589
|
+
logs: true,
|
|
590
|
+
captureUnhandled: true,
|
|
591
|
+
exitOnUncaught: true,
|
|
592
|
+
shutdownTimeoutMs: 2e3,
|
|
593
|
+
breadcrumbs: true,
|
|
594
|
+
maxBreadcrumbs: 100,
|
|
595
|
+
sourceContext: true,
|
|
596
|
+
errorSampleRate: 1,
|
|
597
|
+
flagsCacheTtlMs: 3e4,
|
|
598
|
+
flushIntervalMs: 2e3,
|
|
599
|
+
...options,
|
|
600
|
+
key,
|
|
601
|
+
endpoint,
|
|
602
|
+
release: options.release || env("VAKA_RELEASE", "npm_package_version"),
|
|
603
|
+
environment: options.environment || env("VAKA_ENVIRONMENT", "NODE_ENV") || "production",
|
|
604
|
+
serviceName: options.serviceName || env("OTEL_SERVICE_NAME", "npm_package_name")
|
|
605
|
+
};
|
|
606
|
+
const o = opts;
|
|
607
|
+
transport = new Transport({ endpoint, key, sdk: SDK, flushIntervalMs: o.flushIntervalMs, log });
|
|
608
|
+
if (o.breadcrumbs) installBreadcrumbs(endpoint, (b) => addBreadcrumb(b));
|
|
609
|
+
if (o.captureUnhandled) {
|
|
610
|
+
installProcessHandlers({
|
|
611
|
+
capture: (err, fatal, mechanism) => {
|
|
612
|
+
captureException(err, { handled: false, level: fatal ? "fatal" : "error", tags: { mechanism } }, fatal);
|
|
613
|
+
},
|
|
614
|
+
flush: (ms) => close(ms),
|
|
615
|
+
unhandledRejection: o.unhandledRejection || nodeRejectionMode(),
|
|
616
|
+
exitOnUncaught: o.exitOnUncaught,
|
|
617
|
+
shutdownTimeoutMs: o.shutdownTimeoutMs
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
if (o.otel) {
|
|
621
|
+
try {
|
|
622
|
+
otelSdk = startOtel({
|
|
623
|
+
endpoint,
|
|
624
|
+
key,
|
|
625
|
+
serviceName: o.serviceName,
|
|
626
|
+
release: o.release,
|
|
627
|
+
environment: o.environment,
|
|
628
|
+
tracesSampleRate: o.tracesSampleRate,
|
|
629
|
+
logs: o.logs,
|
|
630
|
+
instrumentations: o.instrumentations,
|
|
631
|
+
resourceAttributes: o.resourceAttributes
|
|
632
|
+
});
|
|
633
|
+
} catch (e) {
|
|
634
|
+
log("OpenTelemetry setup failed:", String(e));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
process.once("beforeExit", () => void flush());
|
|
638
|
+
log("initialized", endpoint, o.release || "(no release)", o.environment);
|
|
639
|
+
}
|
|
640
|
+
function isInitialized() {
|
|
641
|
+
return !!opts;
|
|
642
|
+
}
|
|
643
|
+
function setUser(user) {
|
|
644
|
+
currentScope().user = user === null ? void 0 : typeof user === "string" ? { id: user } : user;
|
|
645
|
+
const span = import_api2.trace.getActiveSpan();
|
|
646
|
+
const id = currentScope().user?.id;
|
|
647
|
+
if (span && id) span.setAttribute("enduser.id", id);
|
|
648
|
+
}
|
|
649
|
+
function setTag(key, value) {
|
|
650
|
+
currentScope().tags[key] = String(value);
|
|
651
|
+
}
|
|
652
|
+
function setTags(tags) {
|
|
653
|
+
for (const k of Object.keys(tags)) setTag(k, tags[k]);
|
|
654
|
+
}
|
|
655
|
+
function setContext(name, value) {
|
|
656
|
+
if (value === null) delete currentScope().contexts[name];
|
|
657
|
+
else currentScope().contexts[name] = value;
|
|
658
|
+
}
|
|
659
|
+
function addBreadcrumb(b) {
|
|
660
|
+
currentScope().addBreadcrumb(b, opts?.maxBreadcrumbs);
|
|
661
|
+
}
|
|
662
|
+
function traceId() {
|
|
663
|
+
const ctx = import_api2.trace.getActiveSpan()?.spanContext();
|
|
664
|
+
return ctx && ctx.traceId !== "00000000000000000000000000000000" ? ctx.traceId : void 0;
|
|
665
|
+
}
|
|
666
|
+
function exceptionChain(error) {
|
|
667
|
+
const out = [];
|
|
668
|
+
let e = error;
|
|
669
|
+
const seen = /* @__PURE__ */ new Set();
|
|
670
|
+
for (let i = 0; i < 6 && e !== void 0 && e !== null && !seen.has(e); i++) {
|
|
671
|
+
seen.add(e);
|
|
672
|
+
if (e instanceof Error || typeof e === "object" && "message" in e) {
|
|
673
|
+
const err = e;
|
|
674
|
+
let frames = parseStack(err.stack);
|
|
675
|
+
if (opts?.sourceContext) frames = addSourceContext(frames);
|
|
676
|
+
const type = err.name && err.name !== "Error" ? err.name : err.constructor?.name && err.constructor.name !== "Object" ? err.constructor.name : "Error";
|
|
677
|
+
out.push({ type: err.code && type === "Error" ? `Error [${err.code}]` : type, value: String(err.message ?? ""), stacktrace: frames });
|
|
678
|
+
e = err.cause;
|
|
679
|
+
} else {
|
|
680
|
+
out.push({ type: "Error", value: typeof e === "string" ? e : safeJson(e), stacktrace: [] });
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return out;
|
|
685
|
+
}
|
|
686
|
+
function safeJson(v) {
|
|
687
|
+
try {
|
|
688
|
+
return JSON.stringify(v)?.slice(0, 1e3) ?? String(v);
|
|
689
|
+
} catch {
|
|
690
|
+
return String(v);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function ignored(error) {
|
|
694
|
+
const pats = opts?.ignoreErrors;
|
|
695
|
+
if (!pats || !pats.length) return false;
|
|
696
|
+
const msg = String(error?.message ?? error);
|
|
697
|
+
return pats.some((p) => typeof p === "string" ? msg.includes(p) : p.test(msg));
|
|
698
|
+
}
|
|
699
|
+
function baseItem(scope, ctx) {
|
|
700
|
+
const span = import_api2.trace.getActiveSpan()?.spanContext();
|
|
701
|
+
const tid = span && span.traceId !== "00000000000000000000000000000000" ? span.traceId : void 0;
|
|
702
|
+
const contexts = {
|
|
703
|
+
runtime: { name: "node", version: process.version },
|
|
704
|
+
os: { name: (0, import_node_os.type)(), version: (0, import_node_os.release)() },
|
|
705
|
+
...scope.contexts
|
|
706
|
+
};
|
|
707
|
+
if (ctx.extra) contexts.extra = ctx.extra;
|
|
708
|
+
if (scope.request) contexts.request = scope.request;
|
|
709
|
+
if (tid) contexts.trace = { trace_id: tid, span_id: span.spanId };
|
|
710
|
+
const user = ctx.user || scope.user;
|
|
711
|
+
const tags = { server_name: (0, import_node_os.hostname)(), ...scope.tags, ...ctx.tags || {} };
|
|
712
|
+
if (scope.request?.method && scope.request.route) tags.transaction = `${scope.request.method} ${scope.request.route}`;
|
|
713
|
+
if (opts?.serviceName) tags.service = opts.serviceName;
|
|
714
|
+
return {
|
|
715
|
+
event_id: (0, import_node_crypto.randomUUID)(),
|
|
716
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
717
|
+
platform: "node",
|
|
718
|
+
release: opts?.release,
|
|
719
|
+
environment: opts?.environment,
|
|
720
|
+
breadcrumbs: scope.breadcrumbs.slice(),
|
|
721
|
+
tags,
|
|
722
|
+
contexts,
|
|
723
|
+
user: user ? { id: user.id } : void 0,
|
|
724
|
+
user_id: user?.id,
|
|
725
|
+
url: scope.request?.url,
|
|
726
|
+
trace_id: tid,
|
|
727
|
+
flags: Object.keys(scope.flags).length ? { ...scope.flags } : void 0
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
function captureException(error, ctx = {}, fatal = false) {
|
|
731
|
+
if (!opts || !transport) return;
|
|
732
|
+
if (ignored(error)) return;
|
|
733
|
+
if (!fatal && (opts.errorSampleRate ?? 1) < 1 && Math.random() >= (opts.errorSampleRate ?? 1)) return;
|
|
734
|
+
const scope = currentScope();
|
|
735
|
+
const item = {
|
|
736
|
+
type: "error",
|
|
737
|
+
...baseItem(scope, ctx),
|
|
738
|
+
level: ctx.level || (fatal ? "fatal" : "error"),
|
|
739
|
+
handled: ctx.handled !== false,
|
|
740
|
+
fatal,
|
|
741
|
+
exceptions: exceptionChain(error),
|
|
742
|
+
fingerprint: ctx.fingerprint
|
|
743
|
+
};
|
|
744
|
+
const id = item.event_id;
|
|
745
|
+
if (!enqueue(item, true)) return;
|
|
746
|
+
import_api2.trace.getActiveSpan()?.setAttribute("vaka.error_captured", true);
|
|
747
|
+
scope.addBreadcrumb({ category: "error", level: "error", message: String(error?.message ?? error).slice(0, 300) }, opts.maxBreadcrumbs);
|
|
748
|
+
return id;
|
|
749
|
+
}
|
|
750
|
+
var captureError = captureException;
|
|
751
|
+
function captureMessage(message, level = "info", ctx = {}) {
|
|
752
|
+
if (!opts || !transport) return;
|
|
753
|
+
const item = { type: "error", ...baseItem(currentScope(), ctx), level, message, fingerprint: ctx.fingerprint };
|
|
754
|
+
return enqueue(item, level === "error" || level === "fatal") ? item.event_id : void 0;
|
|
755
|
+
}
|
|
756
|
+
var FeedbackError = class extends Error {
|
|
757
|
+
/** HTTP status, or 0 when nothing was sent. 429 means a rate limit was hit. */
|
|
758
|
+
status;
|
|
759
|
+
constructor(message, status = 0) {
|
|
760
|
+
super(message);
|
|
761
|
+
this.name = "FeedbackError";
|
|
762
|
+
this.status = status;
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
function contentTypeOf(filename) {
|
|
766
|
+
const ext = filename.toLowerCase().split(".").pop();
|
|
767
|
+
return ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg";
|
|
768
|
+
}
|
|
769
|
+
async function captureFeedback(f) {
|
|
770
|
+
if (!opts || !transport) throw new FeedbackError("vaka is not initialized");
|
|
771
|
+
const message = String(f?.message ?? "").trim();
|
|
772
|
+
if (!message) throw new FeedbackError("feedback needs a message");
|
|
773
|
+
const files = (f.attachments || []).filter(Boolean);
|
|
774
|
+
if (files.length > 3) throw new FeedbackError("at most 3 attachments per feedback");
|
|
775
|
+
const scope = currentScope();
|
|
776
|
+
const userId = f.userId || scope.user?.id;
|
|
777
|
+
const item = {
|
|
778
|
+
id: (0, import_node_crypto.randomUUID)(),
|
|
779
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
780
|
+
platform: "node",
|
|
781
|
+
source: "api",
|
|
782
|
+
message: message.slice(0, 5e3),
|
|
783
|
+
email: f.email?.trim() || void 0,
|
|
784
|
+
name: f.name?.trim() || void 0,
|
|
785
|
+
event_id: f.eventId,
|
|
786
|
+
issue: f.issue,
|
|
787
|
+
user_id: userId,
|
|
788
|
+
anon_id: userId ? void 0 : processId,
|
|
789
|
+
session_id: f.sessionId,
|
|
790
|
+
url: f.url || scope.request?.url,
|
|
791
|
+
release: opts.release,
|
|
792
|
+
environment: opts.environment,
|
|
793
|
+
tags: { ...scope.tags, ...f.tags || {} },
|
|
794
|
+
flags: Object.keys(scope.flags).length ? { ...scope.flags } : void 0
|
|
795
|
+
};
|
|
796
|
+
for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
|
|
797
|
+
const headers2 = { "X-Vaka-Key": opts.key, "X-Vaka-Sdk": `${SDK.name}/${SDK.version}` };
|
|
798
|
+
if (f.ip) headers2["X-Forwarded-For"] = f.ip;
|
|
799
|
+
let body;
|
|
800
|
+
if (files.length) {
|
|
801
|
+
const form = new FormData();
|
|
802
|
+
form.append("feedback", JSON.stringify(item));
|
|
803
|
+
for (const a of files) {
|
|
804
|
+
const data = a.data instanceof ArrayBuffer ? new Uint8Array(a.data) : a.data;
|
|
805
|
+
form.append("attachment", new Blob([data], { type: a.contentType || contentTypeOf(a.filename) }), a.filename);
|
|
806
|
+
}
|
|
807
|
+
body = form;
|
|
808
|
+
} else {
|
|
809
|
+
headers2["Content-Type"] = "application/json";
|
|
810
|
+
body = JSON.stringify(item);
|
|
811
|
+
}
|
|
812
|
+
const { context: context2 } = await import("@opentelemetry/api");
|
|
813
|
+
const { suppressTracing: suppressTracing2 } = await import("@opentelemetry/core");
|
|
814
|
+
let r;
|
|
815
|
+
try {
|
|
816
|
+
r = await context2.with(
|
|
817
|
+
suppressTracing2(context2.active()),
|
|
818
|
+
() => fetch(`${opts.endpoint}/ingest/v1/feedback`, { method: "POST", headers: headers2, body, signal: AbortSignal.timeout(3e4) })
|
|
819
|
+
);
|
|
820
|
+
} catch (e) {
|
|
821
|
+
throw new FeedbackError(`vaka feedback: ${String(e?.message || e)}`);
|
|
822
|
+
}
|
|
823
|
+
const text = await r.text().catch(() => "");
|
|
824
|
+
let json = {};
|
|
825
|
+
try {
|
|
826
|
+
json = text ? JSON.parse(text) : {};
|
|
827
|
+
} catch {
|
|
828
|
+
}
|
|
829
|
+
if (r.status < 200 || r.status >= 300) throw new FeedbackError(`vaka feedback: HTTP ${r.status}${json.error ? `: ${json.error}` : ""}`, r.status);
|
|
830
|
+
scope.addBreadcrumb({ category: "feedback", message: "user feedback sent" }, opts.maxBreadcrumbs);
|
|
831
|
+
return { id: json.id || String(item.id), shortId: json.short_id || "" };
|
|
832
|
+
}
|
|
833
|
+
function track(name, props, o = {}) {
|
|
834
|
+
if (!opts || !name) return;
|
|
835
|
+
const scope = currentScope();
|
|
836
|
+
enqueue({
|
|
837
|
+
type: "event",
|
|
838
|
+
name,
|
|
839
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
840
|
+
props: clean(props),
|
|
841
|
+
user_id: o.userId || scope.user?.id,
|
|
842
|
+
anon_id: o.userId || scope.user?.id ? void 0 : processId,
|
|
843
|
+
url: o.url,
|
|
844
|
+
release: opts.release,
|
|
845
|
+
environment: opts.environment,
|
|
846
|
+
flags: Object.keys(scope.flags).length ? { ...scope.flags } : void 0
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
function clean(props) {
|
|
850
|
+
if (!props) return void 0;
|
|
851
|
+
const out = {};
|
|
852
|
+
for (const k of Object.keys(props)) {
|
|
853
|
+
const v = props[k];
|
|
854
|
+
if (v !== null && v !== void 0) out[k] = v;
|
|
855
|
+
}
|
|
856
|
+
return Object.keys(out).length ? out : void 0;
|
|
857
|
+
}
|
|
858
|
+
function enqueue(item, urgent = false) {
|
|
859
|
+
if (!opts || !transport) return false;
|
|
860
|
+
for (const k of Object.keys(item)) if (item[k] === void 0) delete item[k];
|
|
861
|
+
let final = item;
|
|
862
|
+
if (opts.beforeSend) {
|
|
863
|
+
try {
|
|
864
|
+
final = opts.beforeSend(item);
|
|
865
|
+
} catch (e) {
|
|
866
|
+
log("beforeSend threw:", String(e));
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (!final) return false;
|
|
870
|
+
transport.send(final, urgent);
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
var flagCache = /* @__PURE__ */ new Map();
|
|
874
|
+
async function getFlags(ctx = {}) {
|
|
875
|
+
if (!opts || !transport || transport.disabled) return {};
|
|
876
|
+
const scope = currentScope();
|
|
877
|
+
const userId = ctx.userId || scope.user?.id;
|
|
878
|
+
const distinct = ctx.distinctId || userId || processId;
|
|
879
|
+
const body = { distinct_id: distinct, user_id: userId, environment: opts.environment, properties: clean(ctx.properties) };
|
|
880
|
+
const cacheKey = JSON.stringify(body);
|
|
881
|
+
const ttl = opts.flagsCacheTtlMs ?? 3e4;
|
|
882
|
+
const hit = flagCache.get(cacheKey);
|
|
883
|
+
let flags;
|
|
884
|
+
if (hit && ttl > 0 && Date.now() - hit.at < ttl) {
|
|
885
|
+
flags = hit.flags;
|
|
886
|
+
} else {
|
|
887
|
+
const { context: context2 } = await import("@opentelemetry/api");
|
|
888
|
+
const { suppressTracing: suppressTracing2 } = await import("@opentelemetry/core");
|
|
889
|
+
const r = await context2.with(
|
|
890
|
+
suppressTracing2(context2.active()),
|
|
891
|
+
() => fetch(`${opts.endpoint}/ingest/v1/flags`, {
|
|
892
|
+
method: "POST",
|
|
893
|
+
headers: { "Content-Type": "application/json", "X-Vaka-Key": opts.key },
|
|
894
|
+
body: JSON.stringify(body),
|
|
895
|
+
signal: AbortSignal.timeout(5e3)
|
|
896
|
+
})
|
|
897
|
+
);
|
|
898
|
+
if (r.status !== 200) throw new Error(`vaka flags: HTTP ${r.status}`);
|
|
899
|
+
flags = (await r.json()).flags || {};
|
|
900
|
+
if (ttl > 0) {
|
|
901
|
+
if (flagCache.size >= 1e3) flagCache.delete(flagCache.keys().next().value);
|
|
902
|
+
flagCache.set(cacheKey, { at: Date.now(), flags });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
for (const k of Object.keys(flags)) scope.flags[k] = String(flags[k]);
|
|
906
|
+
const span = import_api2.trace.getActiveSpan();
|
|
907
|
+
if (span) for (const k of Object.keys(flags)) span.setAttribute(`feature_flag.${k}`, String(flags[k]));
|
|
908
|
+
return flags;
|
|
909
|
+
}
|
|
910
|
+
async function getFlag(key, fallback = false, ctx = {}) {
|
|
911
|
+
try {
|
|
912
|
+
const flags = await getFlags(ctx);
|
|
913
|
+
return key in flags ? flags[key] : fallback;
|
|
914
|
+
} catch (e) {
|
|
915
|
+
log("flag evaluation failed:", String(e));
|
|
916
|
+
return fallback;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async function isEnabled(key, ctx = {}) {
|
|
920
|
+
const v = await getFlag(key, false, ctx);
|
|
921
|
+
return v !== false && v !== "false";
|
|
922
|
+
}
|
|
923
|
+
function middlewareCapture(err, c) {
|
|
924
|
+
captureException(err, { handled: c.handled, tags: { mechanism: c.mechanism } });
|
|
925
|
+
}
|
|
926
|
+
var shouldCapture = (status) => opts?.shouldCaptureStatus ? opts.shouldCaptureStatus(status) : status >= 500;
|
|
927
|
+
function requestHandler() {
|
|
928
|
+
return makeRequestHandler();
|
|
929
|
+
}
|
|
930
|
+
function errorHandler() {
|
|
931
|
+
return makeErrorHandler(middlewareCapture, shouldCapture);
|
|
932
|
+
}
|
|
933
|
+
function setupExpressErrorHandler(app) {
|
|
934
|
+
app.use(errorHandler());
|
|
935
|
+
}
|
|
936
|
+
var fastifyPlugin = makeFastifyPlugin(middlewareCapture, shouldCapture);
|
|
937
|
+
function wrapHandler(fn) {
|
|
938
|
+
return (...args) => withScope((scope) => {
|
|
939
|
+
scope.breadcrumbs = [];
|
|
940
|
+
try {
|
|
941
|
+
const r = fn(...args);
|
|
942
|
+
if (r && typeof r.then === "function") {
|
|
943
|
+
return r.catch((e) => {
|
|
944
|
+
captureException(e, { handled: false, tags: { mechanism: "wrapHandler" } });
|
|
945
|
+
throw e;
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
return r;
|
|
949
|
+
} catch (e) {
|
|
950
|
+
captureException(e, { handled: false, tags: { mechanism: "wrapHandler" } });
|
|
951
|
+
throw e;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async function flush(timeoutMs = 5e3) {
|
|
956
|
+
if (!transport) return true;
|
|
957
|
+
return transport.flush(timeoutMs);
|
|
958
|
+
}
|
|
959
|
+
async function close(timeoutMs = 5e3) {
|
|
960
|
+
const sent = await flush(timeoutMs);
|
|
961
|
+
if (otelSdk) {
|
|
962
|
+
const sdk = otelSdk;
|
|
963
|
+
otelSdk = void 0;
|
|
964
|
+
let t;
|
|
965
|
+
await Promise.race([
|
|
966
|
+
sdk.shutdown().catch(() => void 0),
|
|
967
|
+
new Promise((r) => {
|
|
968
|
+
t = setTimeout(r, timeoutMs);
|
|
969
|
+
t.unref?.();
|
|
970
|
+
})
|
|
971
|
+
]);
|
|
972
|
+
if (t) clearTimeout(t);
|
|
973
|
+
}
|
|
974
|
+
return sent;
|
|
975
|
+
}
|
|
976
|
+
async function _reset() {
|
|
977
|
+
uninstallIntegrations();
|
|
978
|
+
if (otelSdk) await otelSdk.shutdown().catch(() => void 0);
|
|
979
|
+
otelSdk = void 0;
|
|
980
|
+
opts = void 0;
|
|
981
|
+
transport = void 0;
|
|
982
|
+
flagCache.clear();
|
|
983
|
+
_resetScopes();
|
|
984
|
+
}
|
|
985
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
986
|
+
0 && (module.exports = {
|
|
987
|
+
FeedbackError,
|
|
988
|
+
SDK,
|
|
989
|
+
VakaScopeSpanProcessor,
|
|
990
|
+
_globalScope,
|
|
991
|
+
_reset,
|
|
992
|
+
addBreadcrumb,
|
|
993
|
+
captureError,
|
|
994
|
+
captureException,
|
|
995
|
+
captureFeedback,
|
|
996
|
+
captureMessage,
|
|
997
|
+
close,
|
|
998
|
+
currentScope,
|
|
999
|
+
errorHandler,
|
|
1000
|
+
fastifyPlugin,
|
|
1001
|
+
flush,
|
|
1002
|
+
getFlag,
|
|
1003
|
+
getFlags,
|
|
1004
|
+
init,
|
|
1005
|
+
isEnabled,
|
|
1006
|
+
isInitialized,
|
|
1007
|
+
parseStack,
|
|
1008
|
+
requestHandler,
|
|
1009
|
+
setContext,
|
|
1010
|
+
setTag,
|
|
1011
|
+
setTags,
|
|
1012
|
+
setUser,
|
|
1013
|
+
setupExpressErrorHandler,
|
|
1014
|
+
traceId,
|
|
1015
|
+
track,
|
|
1016
|
+
vakaLogRecordProcessor,
|
|
1017
|
+
vakaSpanExporter,
|
|
1018
|
+
withScope,
|
|
1019
|
+
wrapHandler
|
|
1020
|
+
});
|