@koderlabs/tasks-sdk 0.1.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/LICENSE +179 -0
- package/README.md +9 -0
- package/dist/index.cjs +908 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +187 -0
- package/dist/index.d.ts +187 -0
- package/dist/index.js +876 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,908 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
Client: () => Client,
|
|
25
|
+
captureWebVitals: () => captureWebVitals,
|
|
26
|
+
defaultScrubEvent: () => defaultScrubEvent,
|
|
27
|
+
getClient: () => getClient,
|
|
28
|
+
init: () => init,
|
|
29
|
+
newSpanId: () => newSpanId,
|
|
30
|
+
newTraceId: () => newTraceId,
|
|
31
|
+
signRequest: () => signRequest
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/transport.ts
|
|
36
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
37
|
+
var MAX_EVENT_BYTES = 500 * 1024;
|
|
38
|
+
var PayloadTooLargeError = class PayloadTooLargeError2 extends Error {
|
|
39
|
+
static {
|
|
40
|
+
__name(this, "PayloadTooLargeError");
|
|
41
|
+
}
|
|
42
|
+
bytes;
|
|
43
|
+
constructor(bytes) {
|
|
44
|
+
super(`InstantTasks SDK payload too large: ${bytes} bytes (max ${MAX_EVENT_BYTES})`), this.bytes = bytes;
|
|
45
|
+
this.name = "PayloadTooLargeError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function timeoutSignal(ms) {
|
|
49
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
50
|
+
return AbortSignal.timeout(ms);
|
|
51
|
+
}
|
|
52
|
+
const ctrl = new AbortController();
|
|
53
|
+
setTimeout(() => ctrl.abort(), ms);
|
|
54
|
+
return ctrl.signal;
|
|
55
|
+
}
|
|
56
|
+
__name(timeoutSignal, "timeoutSignal");
|
|
57
|
+
function pickFetch(opts) {
|
|
58
|
+
return opts?.fetch ?? globalThis.fetch;
|
|
59
|
+
}
|
|
60
|
+
__name(pickFetch, "pickFetch");
|
|
61
|
+
async function signRequest(body, secret) {
|
|
62
|
+
const ts = String(Date.now());
|
|
63
|
+
const nonce = typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
64
|
+
try {
|
|
65
|
+
const subtle = globalThis.crypto?.subtle;
|
|
66
|
+
if (!subtle) return {};
|
|
67
|
+
const enc = new TextEncoder();
|
|
68
|
+
const hashBuf = await subtle.digest("SHA-256", enc.encode(body));
|
|
69
|
+
const bodyHash = bufToHex(hashBuf);
|
|
70
|
+
const key = await subtle.importKey("raw", enc.encode(secret), {
|
|
71
|
+
name: "HMAC",
|
|
72
|
+
hash: "SHA-256"
|
|
73
|
+
}, false, [
|
|
74
|
+
"sign"
|
|
75
|
+
]);
|
|
76
|
+
const sigBuf = await subtle.sign("HMAC", key, enc.encode(`${ts}.${nonce}.${bodyHash}`));
|
|
77
|
+
return {
|
|
78
|
+
"X-IT-Timestamp": ts,
|
|
79
|
+
"X-IT-Nonce": nonce,
|
|
80
|
+
"X-IT-Signature": bufToHex(sigBuf)
|
|
81
|
+
};
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
__name(signRequest, "signRequest");
|
|
87
|
+
function bufToHex(buf) {
|
|
88
|
+
const bytes = new Uint8Array(buf);
|
|
89
|
+
let hex = "";
|
|
90
|
+
for (let i = 0; i < bytes.length; i++) hex += bytes[i].toString(16).padStart(2, "0");
|
|
91
|
+
return hex;
|
|
92
|
+
}
|
|
93
|
+
__name(bufToHex, "bufToHex");
|
|
94
|
+
async function postSpans(endpoint, projectId, accessKey, spans, opts) {
|
|
95
|
+
if (!spans.length) return {
|
|
96
|
+
accepted: 0
|
|
97
|
+
};
|
|
98
|
+
const url = `${endpoint}/api/v1/sdk/v1/spans`;
|
|
99
|
+
const body = JSON.stringify({
|
|
100
|
+
spans
|
|
101
|
+
});
|
|
102
|
+
const headers = {
|
|
103
|
+
Authorization: `Bearer ${accessKey}`,
|
|
104
|
+
"X-Project-Id": projectId,
|
|
105
|
+
"Content-Type": "application/json"
|
|
106
|
+
};
|
|
107
|
+
if (opts?.signingSecret) Object.assign(headers, await signRequest(body, opts.signingSecret));
|
|
108
|
+
const f = pickFetch(opts);
|
|
109
|
+
const res = await f(url, {
|
|
110
|
+
method: "POST",
|
|
111
|
+
headers,
|
|
112
|
+
body,
|
|
113
|
+
signal: timeoutSignal(REQUEST_TIMEOUT_MS)
|
|
114
|
+
});
|
|
115
|
+
if (!res.ok) {
|
|
116
|
+
const text = (await res.text().catch(() => "")).slice(0, 512);
|
|
117
|
+
throw new Error(`InstantTasks SDK spans ingest failed: ${res.status} ${text}`);
|
|
118
|
+
}
|
|
119
|
+
return await res.json();
|
|
120
|
+
}
|
|
121
|
+
__name(postSpans, "postSpans");
|
|
122
|
+
async function postEvent(endpoint, projectId, accessKey, event, attachments, opts) {
|
|
123
|
+
const hasAttachments = !!attachments && attachments.size > 0;
|
|
124
|
+
const url = `${endpoint}/api/v1/sdk/events${hasAttachments ? "/multipart" : ""}`;
|
|
125
|
+
const headers = {
|
|
126
|
+
Authorization: `Bearer ${accessKey}`,
|
|
127
|
+
"X-Project-Id": projectId
|
|
128
|
+
};
|
|
129
|
+
let body;
|
|
130
|
+
let signedBody = null;
|
|
131
|
+
if (hasAttachments) {
|
|
132
|
+
const form = new FormData();
|
|
133
|
+
const eventJson = JSON.stringify(event);
|
|
134
|
+
if (eventJson.length > MAX_EVENT_BYTES) throw new PayloadTooLargeError(eventJson.length);
|
|
135
|
+
form.append("event", eventJson);
|
|
136
|
+
for (const [name, blob] of attachments) form.append(name, blob, name);
|
|
137
|
+
body = form;
|
|
138
|
+
} else {
|
|
139
|
+
headers["Content-Type"] = "application/json";
|
|
140
|
+
const json = JSON.stringify(event);
|
|
141
|
+
if (json.length > MAX_EVENT_BYTES) throw new PayloadTooLargeError(json.length);
|
|
142
|
+
signedBody = json;
|
|
143
|
+
body = json;
|
|
144
|
+
}
|
|
145
|
+
if (opts?.signingSecret && signedBody !== null) {
|
|
146
|
+
Object.assign(headers, await signRequest(signedBody, opts.signingSecret));
|
|
147
|
+
}
|
|
148
|
+
const f = pickFetch(opts);
|
|
149
|
+
const res = await f(url, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers,
|
|
152
|
+
body,
|
|
153
|
+
signal: timeoutSignal(REQUEST_TIMEOUT_MS)
|
|
154
|
+
});
|
|
155
|
+
if (!res.ok) {
|
|
156
|
+
const text = (await res.text().catch(() => "")).slice(0, 512);
|
|
157
|
+
const err = new TransportHttpError(res.status, text, res.headers.get("retry-after"));
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
return await res.json();
|
|
161
|
+
}
|
|
162
|
+
__name(postEvent, "postEvent");
|
|
163
|
+
var TransportHttpError = class extends Error {
|
|
164
|
+
static {
|
|
165
|
+
__name(this, "TransportHttpError");
|
|
166
|
+
}
|
|
167
|
+
status;
|
|
168
|
+
body;
|
|
169
|
+
retryAfter;
|
|
170
|
+
constructor(status, body, retryAfter) {
|
|
171
|
+
super(`InstantTasks SDK ingest failed: ${status} ${body}`), this.status = status, this.body = body, this.retryAfter = retryAfter;
|
|
172
|
+
this.name = "TransportHttpError";
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// src/scrubber.ts
|
|
177
|
+
var REDACTED = "[REDACTED]";
|
|
178
|
+
var MAX_DEPTH = 5;
|
|
179
|
+
var MAX_BYTES = 50 * 1024;
|
|
180
|
+
var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
181
|
+
"authorization",
|
|
182
|
+
"cookie",
|
|
183
|
+
"set-cookie",
|
|
184
|
+
"proxy-authorization"
|
|
185
|
+
]);
|
|
186
|
+
var SECRET_KEY_NAMES = /* @__PURE__ */ new Set([
|
|
187
|
+
"password",
|
|
188
|
+
"secret",
|
|
189
|
+
"token",
|
|
190
|
+
"apikey",
|
|
191
|
+
"api_key",
|
|
192
|
+
"auth",
|
|
193
|
+
"session"
|
|
194
|
+
]);
|
|
195
|
+
var SECRET_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
196
|
+
"token",
|
|
197
|
+
"access_token",
|
|
198
|
+
"refresh_token",
|
|
199
|
+
"password",
|
|
200
|
+
"api_key",
|
|
201
|
+
"apikey",
|
|
202
|
+
"secret",
|
|
203
|
+
"key",
|
|
204
|
+
"auth",
|
|
205
|
+
"session",
|
|
206
|
+
"code",
|
|
207
|
+
"state"
|
|
208
|
+
]);
|
|
209
|
+
function isSecretHeaderName(name) {
|
|
210
|
+
return SECRET_HEADER_NAMES.has(name.toLowerCase());
|
|
211
|
+
}
|
|
212
|
+
__name(isSecretHeaderName, "isSecretHeaderName");
|
|
213
|
+
function isSecretKeyName(name) {
|
|
214
|
+
return SECRET_KEY_NAMES.has(name.toLowerCase());
|
|
215
|
+
}
|
|
216
|
+
__name(isSecretKeyName, "isSecretKeyName");
|
|
217
|
+
function redactUrl(input) {
|
|
218
|
+
if (typeof input !== "string") return input;
|
|
219
|
+
if (!input.includes("?")) return input;
|
|
220
|
+
try {
|
|
221
|
+
const u = new URL(input, "http://_scrub_base_/");
|
|
222
|
+
let changed = false;
|
|
223
|
+
const params = u.searchParams;
|
|
224
|
+
const keys = [];
|
|
225
|
+
params.forEach((_v, k) => {
|
|
226
|
+
keys.push(k);
|
|
227
|
+
});
|
|
228
|
+
for (const k of keys) {
|
|
229
|
+
if (SECRET_QUERY_PARAMS.has(k.toLowerCase())) {
|
|
230
|
+
params.set(k, REDACTED);
|
|
231
|
+
changed = true;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!changed) return input;
|
|
235
|
+
if (input.startsWith("http://") || input.startsWith("https://")) return u.toString();
|
|
236
|
+
return u.pathname + (u.search ? u.search : "") + (u.hash || "");
|
|
237
|
+
} catch {
|
|
238
|
+
return input;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
__name(redactUrl, "redactUrl");
|
|
242
|
+
function cloneAndScrub(value, depth, seen, parentKey) {
|
|
243
|
+
if (depth > MAX_DEPTH) return value;
|
|
244
|
+
if (value === null || value === void 0) return value;
|
|
245
|
+
if (typeof value !== "object") {
|
|
246
|
+
if (typeof value === "string") return redactUrl(value);
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
if (seen.has(value)) return "[Circular]";
|
|
250
|
+
seen.add(value);
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return value.map((item) => cloneAndScrub(item, depth + 1, seen, parentKey));
|
|
253
|
+
}
|
|
254
|
+
const out = {};
|
|
255
|
+
for (const [k, v] of Object.entries(value)) {
|
|
256
|
+
if (isSecretKeyName(k)) {
|
|
257
|
+
out[k] = REDACTED;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (parentKey && parentKey.toLowerCase() === "headers" && isSecretHeaderName(k)) {
|
|
261
|
+
out[k] = REDACTED;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
if (typeof v === "string" && (k === "url" || k === "href" || k === "location")) {
|
|
265
|
+
out[k] = redactUrl(v);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
out[k] = cloneAndScrub(v, depth + 1, seen, k);
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
}
|
|
272
|
+
__name(cloneAndScrub, "cloneAndScrub");
|
|
273
|
+
function deepClone(value) {
|
|
274
|
+
if (typeof globalThis.structuredClone === "function") {
|
|
275
|
+
try {
|
|
276
|
+
return globalThis.structuredClone(value);
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return JSON.parse(JSON.stringify(value));
|
|
281
|
+
}
|
|
282
|
+
__name(deepClone, "deepClone");
|
|
283
|
+
function defaultScrubEvent(event) {
|
|
284
|
+
try {
|
|
285
|
+
const approxSize = JSON.stringify(event).length;
|
|
286
|
+
if (approxSize > MAX_BYTES) return event;
|
|
287
|
+
} catch {
|
|
288
|
+
return event;
|
|
289
|
+
}
|
|
290
|
+
const cloned = deepClone(event);
|
|
291
|
+
const scrubbed = cloneAndScrub(cloned, 0, /* @__PURE__ */ new WeakSet());
|
|
292
|
+
if (scrubbed && typeof scrubbed === "object" && scrubbed.url) {
|
|
293
|
+
scrubbed.url = redactUrl(scrubbed.url);
|
|
294
|
+
}
|
|
295
|
+
return scrubbed;
|
|
296
|
+
}
|
|
297
|
+
__name(defaultScrubEvent, "defaultScrubEvent");
|
|
298
|
+
|
|
299
|
+
// src/internal-logger.ts
|
|
300
|
+
function isProd() {
|
|
301
|
+
try {
|
|
302
|
+
const proc = globalThis.process;
|
|
303
|
+
return proc?.env?.NODE_ENV === "production";
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
__name(isProd, "isProd");
|
|
309
|
+
function emit(level, debug, args) {
|
|
310
|
+
if (!debug) return;
|
|
311
|
+
if (isProd()) return;
|
|
312
|
+
const fn = console[level];
|
|
313
|
+
if (typeof fn === "function") fn("[InstantTasks]", ...args);
|
|
314
|
+
}
|
|
315
|
+
__name(emit, "emit");
|
|
316
|
+
function debugLog(enabled, ...args) {
|
|
317
|
+
emit("log", enabled, args);
|
|
318
|
+
}
|
|
319
|
+
__name(debugLog, "debugLog");
|
|
320
|
+
function debugWarn(enabled, ...args) {
|
|
321
|
+
emit("warn", enabled, args);
|
|
322
|
+
}
|
|
323
|
+
__name(debugWarn, "debugWarn");
|
|
324
|
+
function internalError(message, err) {
|
|
325
|
+
try {
|
|
326
|
+
console.error("[InstantTasks]", message, err);
|
|
327
|
+
} catch {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
__name(internalError, "internalError");
|
|
331
|
+
|
|
332
|
+
// src/spans.ts
|
|
333
|
+
function randomHex(bytes) {
|
|
334
|
+
const arr = new Uint8Array(bytes);
|
|
335
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
336
|
+
crypto.getRandomValues(arr);
|
|
337
|
+
} else {
|
|
338
|
+
for (let i = 0; i < bytes; i++) arr[i] = Math.floor(Math.random() * 256);
|
|
339
|
+
}
|
|
340
|
+
return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
341
|
+
}
|
|
342
|
+
__name(randomHex, "randomHex");
|
|
343
|
+
function newTraceId() {
|
|
344
|
+
return randomHex(16);
|
|
345
|
+
}
|
|
346
|
+
__name(newTraceId, "newTraceId");
|
|
347
|
+
function newSpanId() {
|
|
348
|
+
return randomHex(8);
|
|
349
|
+
}
|
|
350
|
+
__name(newSpanId, "newSpanId");
|
|
351
|
+
var SpanExporter = class {
|
|
352
|
+
static {
|
|
353
|
+
__name(this, "SpanExporter");
|
|
354
|
+
}
|
|
355
|
+
endpoint;
|
|
356
|
+
projectId;
|
|
357
|
+
accessKey;
|
|
358
|
+
debug;
|
|
359
|
+
transportOpts;
|
|
360
|
+
queue = [];
|
|
361
|
+
timer = null;
|
|
362
|
+
FLUSH_MS = 5e3;
|
|
363
|
+
MAX_QUEUE = 50;
|
|
364
|
+
constructor(endpoint, projectId, accessKey, debug = false, transportOpts = {}) {
|
|
365
|
+
this.endpoint = endpoint;
|
|
366
|
+
this.projectId = projectId;
|
|
367
|
+
this.accessKey = accessKey;
|
|
368
|
+
this.debug = debug;
|
|
369
|
+
this.transportOpts = transportOpts;
|
|
370
|
+
}
|
|
371
|
+
start() {
|
|
372
|
+
if (this.timer) return;
|
|
373
|
+
this.timer = setInterval(() => {
|
|
374
|
+
void this.flush();
|
|
375
|
+
}, this.FLUSH_MS);
|
|
376
|
+
}
|
|
377
|
+
stop() {
|
|
378
|
+
if (this.timer) {
|
|
379
|
+
clearInterval(this.timer);
|
|
380
|
+
this.timer = null;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
enqueue(span) {
|
|
384
|
+
this.queue.push(span);
|
|
385
|
+
if (this.queue.length >= this.MAX_QUEUE) void this.flush();
|
|
386
|
+
}
|
|
387
|
+
async flush() {
|
|
388
|
+
if (!this.queue.length) return;
|
|
389
|
+
const batch = this.queue.splice(0, this.queue.length);
|
|
390
|
+
try {
|
|
391
|
+
await postSpans(this.endpoint, this.projectId, this.accessKey, batch, this.transportOpts);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
debugWarn(this.debug, "span flush failed", err);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
function startSpan(exporter, opts) {
|
|
398
|
+
const traceId = opts.traceId ?? newTraceId();
|
|
399
|
+
const spanId = newSpanId();
|
|
400
|
+
const startedAt = Date.now();
|
|
401
|
+
let attrs = {
|
|
402
|
+
...opts.attrs ?? {}
|
|
403
|
+
};
|
|
404
|
+
let ended = false;
|
|
405
|
+
return {
|
|
406
|
+
traceId,
|
|
407
|
+
spanId,
|
|
408
|
+
parentSpanId: opts.parentSpanId,
|
|
409
|
+
setAttr(key, value) {
|
|
410
|
+
attrs[key] = value;
|
|
411
|
+
},
|
|
412
|
+
end(closeOpts) {
|
|
413
|
+
if (ended) return;
|
|
414
|
+
ended = true;
|
|
415
|
+
const endedAt = Date.now();
|
|
416
|
+
if (closeOpts?.attrs) attrs = {
|
|
417
|
+
...attrs,
|
|
418
|
+
...closeOpts.attrs
|
|
419
|
+
};
|
|
420
|
+
exporter.enqueue({
|
|
421
|
+
traceId,
|
|
422
|
+
spanId,
|
|
423
|
+
parentSpanId: opts.parentSpanId,
|
|
424
|
+
name: opts.name,
|
|
425
|
+
kind: opts.kind ?? "internal",
|
|
426
|
+
status: closeOpts?.status ?? "ok",
|
|
427
|
+
startTime: new Date(startedAt).toISOString(),
|
|
428
|
+
endTime: new Date(endedAt).toISOString(),
|
|
429
|
+
durationMs: endedAt - startedAt,
|
|
430
|
+
attrs
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
__name(startSpan, "startSpan");
|
|
436
|
+
|
|
437
|
+
// src/validate.ts
|
|
438
|
+
var ID_RE = /^[A-Za-z0-9_.:\-]+$/;
|
|
439
|
+
var MAX_ID_LEN = 256;
|
|
440
|
+
function validateEndpoint(endpoint) {
|
|
441
|
+
if (typeof endpoint !== "string" || endpoint.length === 0) {
|
|
442
|
+
throw new Error("[InstantTasks] endpoint is required");
|
|
443
|
+
}
|
|
444
|
+
let url;
|
|
445
|
+
try {
|
|
446
|
+
url = new URL(endpoint);
|
|
447
|
+
} catch {
|
|
448
|
+
throw new Error(`[InstantTasks] endpoint must be a valid URL, got: ${endpoint.slice(0, 80)}`);
|
|
449
|
+
}
|
|
450
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname.endsWith(".localhost");
|
|
451
|
+
if (url.protocol === "http:" && !isLocalhost) {
|
|
452
|
+
throw new Error("[InstantTasks] endpoint must use https:// (http:// only allowed for localhost)");
|
|
453
|
+
}
|
|
454
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
455
|
+
throw new Error(`[InstantTasks] endpoint protocol must be http(s), got: ${url.protocol}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
__name(validateEndpoint, "validateEndpoint");
|
|
459
|
+
function validateIdentifier(name, value) {
|
|
460
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
461
|
+
throw new Error(`[InstantTasks] ${name} is required`);
|
|
462
|
+
}
|
|
463
|
+
if (value.length > MAX_ID_LEN) {
|
|
464
|
+
throw new Error(`[InstantTasks] ${name} exceeds ${MAX_ID_LEN} chars`);
|
|
465
|
+
}
|
|
466
|
+
if (!ID_RE.test(value)) {
|
|
467
|
+
throw new Error(`[InstantTasks] ${name} contains invalid characters`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
__name(validateIdentifier, "validateIdentifier");
|
|
471
|
+
|
|
472
|
+
// src/client.ts
|
|
473
|
+
var Client = class _Client {
|
|
474
|
+
static {
|
|
475
|
+
__name(this, "Client");
|
|
476
|
+
}
|
|
477
|
+
options;
|
|
478
|
+
listeners = /* @__PURE__ */ new Map();
|
|
479
|
+
spanExporter;
|
|
480
|
+
transportOpts;
|
|
481
|
+
constructor(opts) {
|
|
482
|
+
const endpoint = opts.endpoint ?? "https://tasks.koderlabs.net";
|
|
483
|
+
if (!opts.endpoint) warnDefaultEndpoint();
|
|
484
|
+
validateEndpoint(endpoint);
|
|
485
|
+
validateIdentifier("projectId", String(opts.projectId));
|
|
486
|
+
validateIdentifier("accessKey", String(opts.accessKey));
|
|
487
|
+
this.options = {
|
|
488
|
+
...opts,
|
|
489
|
+
endpoint
|
|
490
|
+
};
|
|
491
|
+
this.transportOpts = {
|
|
492
|
+
fetch: this.options.fetch,
|
|
493
|
+
signingSecret: this.options.signingSecret
|
|
494
|
+
};
|
|
495
|
+
this.spanExporter = new SpanExporter(this.options.endpoint, this.options.projectId, this.options.accessKey, !!this.options.debug, this.transportOpts);
|
|
496
|
+
this.spanExporter.start();
|
|
497
|
+
}
|
|
498
|
+
/** Start a span. Caller MUST call .end() on the returned handle. */
|
|
499
|
+
startSpan(opts) {
|
|
500
|
+
return startSpan(this.spanExporter, opts);
|
|
501
|
+
}
|
|
502
|
+
/** Flush any buffered spans. Call on app shutdown. */
|
|
503
|
+
flushSpans() {
|
|
504
|
+
return this.spanExporter.flush();
|
|
505
|
+
}
|
|
506
|
+
/** Stop background timers (span exporter interval). Called by init() on replace. */
|
|
507
|
+
stop() {
|
|
508
|
+
this.spanExporter.stop();
|
|
509
|
+
}
|
|
510
|
+
/** Apply configured scrubber. Returns the event (possibly transformed). */
|
|
511
|
+
applyScrub(event) {
|
|
512
|
+
const scrub = this.options.scrub;
|
|
513
|
+
if (scrub === false) return event;
|
|
514
|
+
if (typeof scrub === "function") {
|
|
515
|
+
try {
|
|
516
|
+
return scrub(event);
|
|
517
|
+
} catch (err) {
|
|
518
|
+
internalError("custom scrub threw", err);
|
|
519
|
+
return event;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
return defaultScrubEvent(event);
|
|
524
|
+
} catch (err) {
|
|
525
|
+
internalError("default scrub threw", err);
|
|
526
|
+
return event;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async send(event, attachments) {
|
|
530
|
+
const debug = !!this.options.debug;
|
|
531
|
+
debugLog(debug, "send", event.kind);
|
|
532
|
+
let outgoing = this.applyScrub(event);
|
|
533
|
+
if (this.options.beforeSend) {
|
|
534
|
+
let threw = false;
|
|
535
|
+
try {
|
|
536
|
+
outgoing = await this.options.beforeSend(outgoing);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
threw = true;
|
|
539
|
+
internalError("beforeSend threw \u2014 dropping event", err);
|
|
540
|
+
outgoing = null;
|
|
541
|
+
}
|
|
542
|
+
if (!outgoing) {
|
|
543
|
+
if (!threw) debugLog(debug, "event dropped by beforeSend");
|
|
544
|
+
const dropped = {
|
|
545
|
+
id: "dropped",
|
|
546
|
+
dropped: true
|
|
547
|
+
};
|
|
548
|
+
this.emit(threw ? "beforesend_error" : "send_dropped", {
|
|
549
|
+
event
|
|
550
|
+
});
|
|
551
|
+
return dropped;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
if (this.options.dryRun) {
|
|
555
|
+
debugLog(debug, "dryRun \u2014 not sending", outgoing.kind);
|
|
556
|
+
const result = {
|
|
557
|
+
id: "dryrun",
|
|
558
|
+
dryRun: true
|
|
559
|
+
};
|
|
560
|
+
this.emit("send_dryrun", {
|
|
561
|
+
event: outgoing,
|
|
562
|
+
result
|
|
563
|
+
});
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
if (Date.now() < this.rateLimitedUntil) {
|
|
567
|
+
const result = {
|
|
568
|
+
id: "rate-limited",
|
|
569
|
+
dropped: true
|
|
570
|
+
};
|
|
571
|
+
this.emit("send_rate_limited", {
|
|
572
|
+
event: outgoing
|
|
573
|
+
});
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
const result = await this.sendWithBackoff(outgoing, attachments, debug);
|
|
578
|
+
this.emit("send_success", {
|
|
579
|
+
event: outgoing,
|
|
580
|
+
result
|
|
581
|
+
});
|
|
582
|
+
return result;
|
|
583
|
+
} catch (err) {
|
|
584
|
+
this.emit("send_error", {
|
|
585
|
+
event: outgoing,
|
|
586
|
+
error: err
|
|
587
|
+
});
|
|
588
|
+
throw err;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
rateLimitedUntil = 0;
|
|
592
|
+
static MAX_RETRIES = 2;
|
|
593
|
+
static BASE_BACKOFF_MS = 1e3;
|
|
594
|
+
async sendWithBackoff(event, attachments, debug) {
|
|
595
|
+
let attempt = 0;
|
|
596
|
+
let lastErr;
|
|
597
|
+
while (attempt <= _Client.MAX_RETRIES) {
|
|
598
|
+
try {
|
|
599
|
+
return await postEvent(this.options.endpoint, this.options.projectId, this.options.accessKey, event, attachments, this.transportOpts);
|
|
600
|
+
} catch (err) {
|
|
601
|
+
lastErr = err;
|
|
602
|
+
if (err instanceof PayloadTooLargeError) {
|
|
603
|
+
debugWarn(debug, err.message);
|
|
604
|
+
throw err;
|
|
605
|
+
}
|
|
606
|
+
if (err instanceof TransportHttpError) {
|
|
607
|
+
if (err.status === 429) {
|
|
608
|
+
const cooldown = parseRetryAfter(err.retryAfter) ?? 3e4;
|
|
609
|
+
this.rateLimitedUntil = Date.now() + cooldown;
|
|
610
|
+
throw err;
|
|
611
|
+
}
|
|
612
|
+
if (err.status < 500 || err.status >= 600) throw err;
|
|
613
|
+
}
|
|
614
|
+
attempt++;
|
|
615
|
+
if (attempt > _Client.MAX_RETRIES) break;
|
|
616
|
+
const wait = _Client.BASE_BACKOFF_MS * Math.pow(2, attempt - 1);
|
|
617
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw lastErr;
|
|
621
|
+
}
|
|
622
|
+
on(event, listener) {
|
|
623
|
+
const arr = this.listeners.get(event) ?? [];
|
|
624
|
+
arr.push(listener);
|
|
625
|
+
this.listeners.set(event, arr);
|
|
626
|
+
}
|
|
627
|
+
emit(event, ...args) {
|
|
628
|
+
for (const l of this.listeners.get(event) ?? []) {
|
|
629
|
+
try {
|
|
630
|
+
l(...args);
|
|
631
|
+
} catch (e) {
|
|
632
|
+
internalError("listener threw", e);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
setUser(user) {
|
|
637
|
+
this.options.user = user;
|
|
638
|
+
}
|
|
639
|
+
setCustomData(data) {
|
|
640
|
+
this.options.customData = {
|
|
641
|
+
...this.options.customData,
|
|
642
|
+
...data
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
};
|
|
646
|
+
var warnedDefaultEndpoint = false;
|
|
647
|
+
function warnDefaultEndpoint() {
|
|
648
|
+
if (warnedDefaultEndpoint) return;
|
|
649
|
+
warnedDefaultEndpoint = true;
|
|
650
|
+
try {
|
|
651
|
+
const proc = globalThis.process;
|
|
652
|
+
if (proc?.env?.INSTANTTASKS_QUIET) return;
|
|
653
|
+
} catch {
|
|
654
|
+
}
|
|
655
|
+
internalError("init() called without an explicit `endpoint`. Defaulting to https://tasks.koderlabs.net is deprecated and will throw in the next major release. Set `endpoint: process.env.INSTANTTASKS_ENDPOINT` (or your self-hosted URL).");
|
|
656
|
+
}
|
|
657
|
+
__name(warnDefaultEndpoint, "warnDefaultEndpoint");
|
|
658
|
+
function parseRetryAfter(header) {
|
|
659
|
+
if (!header) return null;
|
|
660
|
+
const sec = Number(header);
|
|
661
|
+
if (Number.isFinite(sec) && sec >= 0) return Math.min(sec * 1e3, 5 * 6e4);
|
|
662
|
+
const date = Date.parse(header);
|
|
663
|
+
if (Number.isFinite(date)) {
|
|
664
|
+
const ms = date - Date.now();
|
|
665
|
+
if (ms > 0) return Math.min(ms, 5 * 6e4);
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
__name(parseRetryAfter, "parseRetryAfter");
|
|
670
|
+
|
|
671
|
+
// src/web-vitals.ts
|
|
672
|
+
var RATING_THRESHOLDS = {
|
|
673
|
+
// [good, poor] — between = needs-improvement
|
|
674
|
+
LCP: [
|
|
675
|
+
2500,
|
|
676
|
+
4e3
|
|
677
|
+
],
|
|
678
|
+
INP: [
|
|
679
|
+
200,
|
|
680
|
+
500
|
|
681
|
+
],
|
|
682
|
+
CLS: [
|
|
683
|
+
0.1,
|
|
684
|
+
0.25
|
|
685
|
+
],
|
|
686
|
+
FCP: [
|
|
687
|
+
1800,
|
|
688
|
+
3e3
|
|
689
|
+
],
|
|
690
|
+
TTFB: [
|
|
691
|
+
800,
|
|
692
|
+
1800
|
|
693
|
+
]
|
|
694
|
+
};
|
|
695
|
+
function rate(name, value) {
|
|
696
|
+
const [good, poor] = RATING_THRESHOLDS[name];
|
|
697
|
+
if (value <= good) return "good";
|
|
698
|
+
if (value >= poor) return "poor";
|
|
699
|
+
return "needs-improvement";
|
|
700
|
+
}
|
|
701
|
+
__name(rate, "rate");
|
|
702
|
+
function randomHexShort() {
|
|
703
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
704
|
+
const a = new Uint8Array(4);
|
|
705
|
+
crypto.getRandomValues(a);
|
|
706
|
+
return Array.from(a, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
707
|
+
}
|
|
708
|
+
return Math.random().toString(36).slice(2, 10);
|
|
709
|
+
}
|
|
710
|
+
__name(randomHexShort, "randomHexShort");
|
|
711
|
+
function buildReport(client, name, value) {
|
|
712
|
+
const env = client.options;
|
|
713
|
+
return client.send({
|
|
714
|
+
kind: "web_vital",
|
|
715
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
716
|
+
release: env.release,
|
|
717
|
+
environment: env.environment,
|
|
718
|
+
user: env.user,
|
|
719
|
+
url: typeof location !== "undefined" ? location.href : "",
|
|
720
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
721
|
+
viewport: typeof window !== "undefined" ? {
|
|
722
|
+
width: window.innerWidth,
|
|
723
|
+
height: window.innerHeight
|
|
724
|
+
} : {
|
|
725
|
+
width: 0,
|
|
726
|
+
height: 0
|
|
727
|
+
},
|
|
728
|
+
metric: name,
|
|
729
|
+
value: Math.round(value * 1e3) / 1e3,
|
|
730
|
+
rating: rate(name, value),
|
|
731
|
+
measurementId: `${name}-${Date.now()}-${randomHexShort()}`,
|
|
732
|
+
navigationType: performance.getEntriesByType?.("navigation")?.[0]?.type
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
__name(buildReport, "buildReport");
|
|
736
|
+
var MetricReporter = class MetricReporter2 {
|
|
737
|
+
static {
|
|
738
|
+
__name(this, "MetricReporter");
|
|
739
|
+
}
|
|
740
|
+
client;
|
|
741
|
+
name;
|
|
742
|
+
debounceMs;
|
|
743
|
+
reportOnHidden;
|
|
744
|
+
value = 0;
|
|
745
|
+
hasValue = false;
|
|
746
|
+
sent = false;
|
|
747
|
+
timer = null;
|
|
748
|
+
constructor(client, name, debounceMs, reportOnHidden) {
|
|
749
|
+
this.client = client;
|
|
750
|
+
this.name = name;
|
|
751
|
+
this.debounceMs = debounceMs;
|
|
752
|
+
this.reportOnHidden = reportOnHidden;
|
|
753
|
+
}
|
|
754
|
+
setValue(v) {
|
|
755
|
+
this.value = v;
|
|
756
|
+
this.hasValue = true;
|
|
757
|
+
if (this.reportOnHidden) return;
|
|
758
|
+
if (this.timer) clearTimeout(this.timer);
|
|
759
|
+
this.timer = setTimeout(() => this.flush(), this.debounceMs);
|
|
760
|
+
}
|
|
761
|
+
flush() {
|
|
762
|
+
if (this.sent || !this.hasValue) return;
|
|
763
|
+
this.sent = true;
|
|
764
|
+
if (this.timer) {
|
|
765
|
+
clearTimeout(this.timer);
|
|
766
|
+
this.timer = null;
|
|
767
|
+
}
|
|
768
|
+
void buildReport(this.client, this.name, this.value);
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
function captureWebVitals(client, opts = {}) {
|
|
772
|
+
if (typeof PerformanceObserver === "undefined") return;
|
|
773
|
+
const wanted = new Set(opts.metrics ?? [
|
|
774
|
+
"LCP",
|
|
775
|
+
"INP",
|
|
776
|
+
"CLS",
|
|
777
|
+
"FCP",
|
|
778
|
+
"TTFB"
|
|
779
|
+
]);
|
|
780
|
+
const reportOnHidden = opts.reportOnHidden !== false;
|
|
781
|
+
const debounceMs = opts.debounceMs ?? 2e3;
|
|
782
|
+
const reporters = [];
|
|
783
|
+
const make = /* @__PURE__ */ __name((name) => {
|
|
784
|
+
const r = new MetricReporter(client, name, debounceMs, reportOnHidden);
|
|
785
|
+
reporters.push(r);
|
|
786
|
+
return r;
|
|
787
|
+
}, "make");
|
|
788
|
+
if (typeof document !== "undefined") {
|
|
789
|
+
const onHide = /* @__PURE__ */ __name(() => {
|
|
790
|
+
if (document.visibilityState === "hidden") {
|
|
791
|
+
for (const r of reporters) r.flush();
|
|
792
|
+
}
|
|
793
|
+
}, "onHide");
|
|
794
|
+
document.addEventListener("visibilitychange", onHide);
|
|
795
|
+
}
|
|
796
|
+
if (wanted.has("LCP")) {
|
|
797
|
+
safeObserve("largest-contentful-paint", (entries) => {
|
|
798
|
+
const r = lcp ?? (lcp = make("LCP"));
|
|
799
|
+
const last = entries[entries.length - 1];
|
|
800
|
+
if (last) r.setValue(last.startTime ?? 0);
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
let lcp = null;
|
|
804
|
+
if (wanted.has("FCP")) {
|
|
805
|
+
const r = make("FCP");
|
|
806
|
+
safeObserve("paint", (entries) => {
|
|
807
|
+
for (const e of entries) {
|
|
808
|
+
if (e.name === "first-contentful-paint") {
|
|
809
|
+
r.setValue(e.startTime ?? 0);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (wanted.has("CLS")) {
|
|
815
|
+
const r = make("CLS");
|
|
816
|
+
let cls = 0;
|
|
817
|
+
safeObserve("layout-shift", (entries) => {
|
|
818
|
+
for (const e of entries) {
|
|
819
|
+
if (!e.hadRecentInput) cls += e.value;
|
|
820
|
+
}
|
|
821
|
+
r.setValue(cls);
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (wanted.has("INP")) {
|
|
825
|
+
const r = make("INP");
|
|
826
|
+
let maxInp = 0;
|
|
827
|
+
safeObserve("event", (entries) => {
|
|
828
|
+
for (const e of entries) {
|
|
829
|
+
if (e.duration > maxInp) maxInp = e.duration;
|
|
830
|
+
}
|
|
831
|
+
r.setValue(maxInp);
|
|
832
|
+
}, {
|
|
833
|
+
durationThreshold: 16
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
if (wanted.has("TTFB")) {
|
|
837
|
+
try {
|
|
838
|
+
const nav = performance.getEntriesByType?.("navigation")?.[0];
|
|
839
|
+
if (nav && typeof nav.responseStart === "number") {
|
|
840
|
+
const r = make("TTFB");
|
|
841
|
+
r.setValue(nav.responseStart);
|
|
842
|
+
r.flush();
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
__name(captureWebVitals, "captureWebVitals");
|
|
849
|
+
function safeObserve(type, cb, extra = {}) {
|
|
850
|
+
try {
|
|
851
|
+
const po = new PerformanceObserver((list) => cb(list.getEntries()));
|
|
852
|
+
po.observe({
|
|
853
|
+
type,
|
|
854
|
+
buffered: true,
|
|
855
|
+
...extra
|
|
856
|
+
});
|
|
857
|
+
} catch {
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
__name(safeObserve, "safeObserve");
|
|
861
|
+
|
|
862
|
+
// src/index.ts
|
|
863
|
+
var currentClient = null;
|
|
864
|
+
var currentIntegrations = [];
|
|
865
|
+
function init(opts) {
|
|
866
|
+
if (currentClient) {
|
|
867
|
+
currentClient.emit("replaced");
|
|
868
|
+
try {
|
|
869
|
+
void currentClient.flushSpans?.();
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
try {
|
|
873
|
+
currentClient.stop?.();
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
for (const integration of currentIntegrations) {
|
|
877
|
+
try {
|
|
878
|
+
integration.teardown?.();
|
|
879
|
+
} catch (e) {
|
|
880
|
+
internalError(`integration ${integration.name} teardown failed`, e);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
currentIntegrations = [];
|
|
884
|
+
}
|
|
885
|
+
const client = new Client(opts);
|
|
886
|
+
currentClient = client;
|
|
887
|
+
const integrations = opts.integrations ?? [];
|
|
888
|
+
for (const integration of integrations) integration.setup(client);
|
|
889
|
+
currentIntegrations = integrations;
|
|
890
|
+
return client;
|
|
891
|
+
}
|
|
892
|
+
__name(init, "init");
|
|
893
|
+
function getClient() {
|
|
894
|
+
return currentClient;
|
|
895
|
+
}
|
|
896
|
+
__name(getClient, "getClient");
|
|
897
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
898
|
+
0 && (module.exports = {
|
|
899
|
+
Client,
|
|
900
|
+
captureWebVitals,
|
|
901
|
+
defaultScrubEvent,
|
|
902
|
+
getClient,
|
|
903
|
+
init,
|
|
904
|
+
newSpanId,
|
|
905
|
+
newTraceId,
|
|
906
|
+
signRequest
|
|
907
|
+
});
|
|
908
|
+
//# sourceMappingURL=index.cjs.map
|