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