@raindrop-ai/claude-code 0.0.13 → 0.0.15
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 +21 -0
- package/README.md +11 -0
- package/dist/cli.js +692 -109
- package/dist/index.cjs +642 -105
- package/dist/index.d.cts +181 -2
- package/dist/index.d.ts +181 -2
- package/dist/index.js +642 -105
- package/package.json +22 -13
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
7
7
|
import { tmpdir } from "os";
|
|
8
8
|
import { join } from "path";
|
|
9
9
|
|
|
10
|
-
// ../core/dist/chunk-
|
|
10
|
+
// ../core/dist/chunk-SK6EJEO7.js
|
|
11
11
|
function getCrypto() {
|
|
12
12
|
const c = globalThis.crypto;
|
|
13
13
|
return c;
|
|
@@ -51,6 +51,8 @@ function base64Encode(bytes) {
|
|
|
51
51
|
}
|
|
52
52
|
return out;
|
|
53
53
|
}
|
|
54
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
55
|
+
var MAX_RETRY_DELAY_MS = 3e4;
|
|
54
56
|
function wait(ms) {
|
|
55
57
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
56
58
|
}
|
|
@@ -58,6 +60,46 @@ function formatEndpoint(endpoint) {
|
|
|
58
60
|
if (!endpoint) return void 0;
|
|
59
61
|
return endpoint.endsWith("/") ? endpoint : `${endpoint}/`;
|
|
60
62
|
}
|
|
63
|
+
function redactUrlForLog(url) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(url);
|
|
66
|
+
parsed.username = "";
|
|
67
|
+
parsed.password = "";
|
|
68
|
+
parsed.search = "";
|
|
69
|
+
parsed.hash = "";
|
|
70
|
+
return parsed.toString();
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return "<unparseable-url>";
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
var RATE_LIMITED_LOG_INTERVAL_MS = 3e4;
|
|
76
|
+
var rateLimitedLogLast = /* @__PURE__ */ new Map();
|
|
77
|
+
function rateLimitedLog(key, log) {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
const last = rateLimitedLogLast.get(key);
|
|
80
|
+
if (last !== void 0 && now - last < RATE_LIMITED_LOG_INTERVAL_MS) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
rateLimitedLogLast.set(key, now);
|
|
84
|
+
log();
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
async function raceWithTimeout(promise, timeoutMs) {
|
|
88
|
+
let timer;
|
|
89
|
+
const settledInTime = await Promise.race([
|
|
90
|
+
promise.then(
|
|
91
|
+
() => true,
|
|
92
|
+
() => true
|
|
93
|
+
),
|
|
94
|
+
new Promise((resolve2) => {
|
|
95
|
+
var _a;
|
|
96
|
+
timer = setTimeout(() => resolve2(false), Math.max(0, timeoutMs));
|
|
97
|
+
(_a = timer.unref) == null ? void 0 : _a.call(timer);
|
|
98
|
+
})
|
|
99
|
+
]);
|
|
100
|
+
if (timer) clearTimeout(timer);
|
|
101
|
+
return settledInTime;
|
|
102
|
+
}
|
|
61
103
|
function parseRetryAfter(headers) {
|
|
62
104
|
var _a;
|
|
63
105
|
const value = (_a = headers.get("Retry-After")) != null ? _a : headers.get("retry-after");
|
|
@@ -74,12 +116,12 @@ function parseRetryAfter(headers) {
|
|
|
74
116
|
function getRetryDelayMs(attemptNumber, previousError) {
|
|
75
117
|
if (previousError && typeof previousError === "object" && previousError !== null && "retryAfterMs" in previousError) {
|
|
76
118
|
const v = previousError.retryAfterMs;
|
|
77
|
-
if (typeof v === "number") return Math.max(0, v);
|
|
119
|
+
if (typeof v === "number") return Math.min(Math.max(0, v), MAX_RETRY_DELAY_MS);
|
|
78
120
|
}
|
|
79
121
|
if (attemptNumber <= 1) return 0;
|
|
80
122
|
const base = 500;
|
|
81
123
|
const factor = Math.pow(2, attemptNumber - 2);
|
|
82
|
-
return base * factor;
|
|
124
|
+
return Math.min(base * factor, MAX_RETRY_DELAY_MS);
|
|
83
125
|
}
|
|
84
126
|
async function withRetry(operation, opName, opts) {
|
|
85
127
|
const prefix = opts.sdkName ? `[raindrop-ai/${opts.sdkName}]` : "[raindrop-ai/core]";
|
|
@@ -114,7 +156,9 @@ async function withRetry(operation, opName, opts) {
|
|
|
114
156
|
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
115
157
|
}
|
|
116
158
|
async function postJson(url, body, headers, opts) {
|
|
117
|
-
|
|
159
|
+
var _a;
|
|
160
|
+
const opName = `POST ${redactUrlForLog(url)}`;
|
|
161
|
+
const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
118
162
|
await withRetry(
|
|
119
163
|
async () => {
|
|
120
164
|
const resp = await fetch(url, {
|
|
@@ -123,7 +167,8 @@ async function postJson(url, body, headers, opts) {
|
|
|
123
167
|
"Content-Type": "application/json",
|
|
124
168
|
...headers
|
|
125
169
|
},
|
|
126
|
-
body: JSON.stringify(body)
|
|
170
|
+
body: JSON.stringify(body),
|
|
171
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
127
172
|
});
|
|
128
173
|
if (!resp.ok) {
|
|
129
174
|
const text = await resp.text().catch(() => "");
|
|
@@ -140,6 +185,27 @@ async function postJson(url, body, headers, opts) {
|
|
|
140
185
|
opts
|
|
141
186
|
);
|
|
142
187
|
}
|
|
188
|
+
var DEFAULT_MAX_TEXT_FIELD_CHARS = 1e6;
|
|
189
|
+
var TRUNCATION_MARKER = "...[truncated by raindrop]";
|
|
190
|
+
var currentDefaultMaxTextFieldChars = DEFAULT_MAX_TEXT_FIELD_CHARS;
|
|
191
|
+
function resolveMaxTextFieldChars(value) {
|
|
192
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
193
|
+
return Math.floor(value);
|
|
194
|
+
}
|
|
195
|
+
return currentDefaultMaxTextFieldChars;
|
|
196
|
+
}
|
|
197
|
+
function truncateToLimit(text, limit) {
|
|
198
|
+
if (limit > TRUNCATION_MARKER.length) {
|
|
199
|
+
return text.slice(0, limit - TRUNCATION_MARKER.length) + TRUNCATION_MARKER;
|
|
200
|
+
}
|
|
201
|
+
return text.slice(0, Math.max(0, limit));
|
|
202
|
+
}
|
|
203
|
+
function capText(value, limit) {
|
|
204
|
+
if (typeof value !== "string") return value;
|
|
205
|
+
const max = limit != null ? limit : currentDefaultMaxTextFieldChars;
|
|
206
|
+
if (value.length <= max) return value;
|
|
207
|
+
return truncateToLimit(value, max);
|
|
208
|
+
}
|
|
143
209
|
var SpanStatusCode = {
|
|
144
210
|
UNSET: 0,
|
|
145
211
|
OK: 1,
|
|
@@ -197,6 +263,112 @@ function buildExportTraceServiceRequest(spans, serviceName = "raindrop.core", se
|
|
|
197
263
|
]
|
|
198
264
|
};
|
|
199
265
|
}
|
|
266
|
+
var LOCAL_DEBUGGER_ENV_VAR = "RAINDROP_LOCAL_DEBUGGER";
|
|
267
|
+
var WORKSHOP_ENV_VAR = "RAINDROP_WORKSHOP";
|
|
268
|
+
var DEFAULT_LOCAL_WORKSHOP_URL = "http://localhost:5899/v1/";
|
|
269
|
+
function readEnvVar(name) {
|
|
270
|
+
var _a;
|
|
271
|
+
try {
|
|
272
|
+
const env = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env;
|
|
273
|
+
if (env && typeof env[name] === "string" && env[name].length > 0) {
|
|
274
|
+
return env[name];
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
}
|
|
278
|
+
return void 0;
|
|
279
|
+
}
|
|
280
|
+
function readWorkshopEnv() {
|
|
281
|
+
const raw = readEnvVar(WORKSHOP_ENV_VAR);
|
|
282
|
+
if (raw === void 0) return void 0;
|
|
283
|
+
const trimmed = raw.trim();
|
|
284
|
+
if (trimmed.length === 0) return void 0;
|
|
285
|
+
if (/^https?:\/\//i.test(trimmed)) return { url: trimmed };
|
|
286
|
+
if (/^(1|true|yes|on)$/i.test(trimmed)) return "enable";
|
|
287
|
+
if (/^(0|false|no|off)$/i.test(trimmed)) return "disable";
|
|
288
|
+
return void 0;
|
|
289
|
+
}
|
|
290
|
+
function isLocalDevHost(hostname) {
|
|
291
|
+
if (!hostname) return false;
|
|
292
|
+
if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
function readRuntimeHostname() {
|
|
299
|
+
try {
|
|
300
|
+
const loc = globalThis == null ? void 0 : globalThis.location;
|
|
301
|
+
if (loc && typeof loc.hostname === "string" && loc.hostname.length > 0) {
|
|
302
|
+
return loc.hostname;
|
|
303
|
+
}
|
|
304
|
+
} catch (e) {
|
|
305
|
+
}
|
|
306
|
+
return void 0;
|
|
307
|
+
}
|
|
308
|
+
function shouldAutoEnableLocalWorkshop() {
|
|
309
|
+
if (isLocalDevHost(readRuntimeHostname())) return true;
|
|
310
|
+
if (readEnvVar("NODE_ENV") === "development") return true;
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
function resolveLocalDebuggerBaseUrl(baseUrl) {
|
|
314
|
+
var _a, _b, _c;
|
|
315
|
+
if (baseUrl === null) return null;
|
|
316
|
+
if (typeof baseUrl === "string" && baseUrl.length > 0) {
|
|
317
|
+
return (_a = formatEndpoint(baseUrl)) != null ? _a : null;
|
|
318
|
+
}
|
|
319
|
+
const explicitUrlEnv = readEnvVar(LOCAL_DEBUGGER_ENV_VAR);
|
|
320
|
+
if (explicitUrlEnv) return (_b = formatEndpoint(explicitUrlEnv)) != null ? _b : null;
|
|
321
|
+
const workshopEnv = readWorkshopEnv();
|
|
322
|
+
if (workshopEnv === "disable") return null;
|
|
323
|
+
if (workshopEnv === "enable") return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
324
|
+
if (workshopEnv && "url" in workshopEnv) return (_c = formatEndpoint(workshopEnv.url)) != null ? _c : null;
|
|
325
|
+
if (shouldAutoEnableLocalWorkshop()) return DEFAULT_LOCAL_WORKSHOP_URL;
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
function mirrorTraceExportToLocalDebugger(body, options = {}) {
|
|
329
|
+
var _a;
|
|
330
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
331
|
+
if (!baseUrl) return;
|
|
332
|
+
void postJson(`${baseUrl}traces`, body, {}, {
|
|
333
|
+
maxAttempts: 1,
|
|
334
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
335
|
+
sdkName: options.sdkName
|
|
336
|
+
}).catch(() => {
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
function mirrorPartialEventToLocalDebugger(event, options = {}) {
|
|
340
|
+
var _a;
|
|
341
|
+
const baseUrl = resolveLocalDebuggerBaseUrl(options.baseUrl);
|
|
342
|
+
if (!baseUrl) return;
|
|
343
|
+
const headers = options.writeKey ? { Authorization: `Bearer ${options.writeKey}` } : {};
|
|
344
|
+
void postJson(`${baseUrl}events/track_partial`, event, headers, {
|
|
345
|
+
maxAttempts: 1,
|
|
346
|
+
debug: (_a = options.debug) != null ? _a : false,
|
|
347
|
+
sdkName: options.sdkName
|
|
348
|
+
}).catch(() => {
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
var PROJECT_ID_HEADER = "X-Raindrop-Project-Id";
|
|
352
|
+
var PROJECT_ID_SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
353
|
+
function isValidProjectIdSlug(value) {
|
|
354
|
+
return PROJECT_ID_SLUG_PATTERN.test(value);
|
|
355
|
+
}
|
|
356
|
+
function normalizeProjectId(raw, opts) {
|
|
357
|
+
if (typeof raw !== "string") return void 0;
|
|
358
|
+
const trimmed = raw.trim();
|
|
359
|
+
if (!trimmed) return void 0;
|
|
360
|
+
if (!isValidProjectIdSlug(trimmed) && opts.debug) {
|
|
361
|
+
console.warn(
|
|
362
|
+
`${opts.prefix} projectId "${trimmed}" does not match slug ${PROJECT_ID_SLUG_PATTERN.source}; sending anyway \u2014 backend may reject with HTTP 400`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return trimmed;
|
|
366
|
+
}
|
|
367
|
+
function projectIdHeaders(projectId) {
|
|
368
|
+
return projectId ? { [PROJECT_ID_HEADER]: projectId } : {};
|
|
369
|
+
}
|
|
370
|
+
var SHUTDOWN_DEADLINE_MS = 1e4;
|
|
371
|
+
var POST_SHUTDOWN_TIMEOUT_MS = 5e3;
|
|
200
372
|
function mergePatches(target, source) {
|
|
201
373
|
var _a, _b, _c, _d;
|
|
202
374
|
const out = { ...target, ...source };
|
|
@@ -214,7 +386,8 @@ var EventShipper = class {
|
|
|
214
386
|
this.sticky = /* @__PURE__ */ new Map();
|
|
215
387
|
this.timers = /* @__PURE__ */ new Map();
|
|
216
388
|
this.inFlight = /* @__PURE__ */ new Set();
|
|
217
|
-
|
|
389
|
+
this.hasShutdown = false;
|
|
390
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
218
391
|
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
219
392
|
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
220
393
|
this.enabled = opts.enabled !== false;
|
|
@@ -223,11 +396,20 @@ var EventShipper = class {
|
|
|
223
396
|
this.sdkName = (_d = opts.sdkName) != null ? _d : "core";
|
|
224
397
|
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
225
398
|
this.defaultEventName = (_e = opts.defaultEventName) != null ? _e : "ai_generation";
|
|
399
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
400
|
+
this.localDebuggerUrl = (_f = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _f : void 0;
|
|
401
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
402
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
403
|
+
}
|
|
404
|
+
this.projectId = normalizeProjectId(opts.projectId, {
|
|
405
|
+
debug: this.debug,
|
|
406
|
+
prefix: this.prefix
|
|
407
|
+
});
|
|
226
408
|
const isNode = typeof process !== "undefined" && typeof process.version === "string";
|
|
227
409
|
this.context = {
|
|
228
410
|
library: {
|
|
229
|
-
name: (
|
|
230
|
-
version: (
|
|
411
|
+
name: (_g = opts.libraryName) != null ? _g : "@raindrop-ai/core",
|
|
412
|
+
version: (_h = opts.libraryVersion) != null ? _h : "0.0.0"
|
|
231
413
|
},
|
|
232
414
|
metadata: {
|
|
233
415
|
jsRuntime: isNode ? "node" : "web",
|
|
@@ -241,10 +423,55 @@ var EventShipper = class {
|
|
|
241
423
|
authHeaders() {
|
|
242
424
|
return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
|
|
243
425
|
}
|
|
426
|
+
requestHeaders() {
|
|
427
|
+
return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Build the retry/timeout options for one POST, honoring the shutdown
|
|
431
|
+
* deadline. Returns `null` when the shutdown drain window is exhausted —
|
|
432
|
+
* the caller must drop the payload (with a rate-limited warning) instead
|
|
433
|
+
* of issuing a request that could outlive process exit.
|
|
434
|
+
*
|
|
435
|
+
* Checked fresh on EVERY send, so a shutdown that begins while the flush
|
|
436
|
+
* path is mid-drain takes effect immediately: no further retries, and the
|
|
437
|
+
* per-attempt timeout is clamped to the remaining window. After
|
|
438
|
+
* `shutdown()` returns (deadline cleared, `hasShutdown` still set),
|
|
439
|
+
* sends — late callers, or flush work the deadline abandoned mid-drain —
|
|
440
|
+
* run as a single short attempt rather than regaining the full retry
|
|
441
|
+
* schedule.
|
|
442
|
+
*/
|
|
443
|
+
requestOpts() {
|
|
444
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
445
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
446
|
+
if (remainingMs <= 0) return null;
|
|
447
|
+
return {
|
|
448
|
+
maxAttempts: 1,
|
|
449
|
+
debug: this.debug,
|
|
450
|
+
sdkName: this.sdkName,
|
|
451
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (this.hasShutdown) {
|
|
455
|
+
return {
|
|
456
|
+
maxAttempts: 1,
|
|
457
|
+
debug: this.debug,
|
|
458
|
+
sdkName: this.sdkName,
|
|
459
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
463
|
+
}
|
|
244
464
|
async patch(eventId, patch) {
|
|
245
465
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
246
466
|
if (!this.enabled) return;
|
|
247
467
|
if (!eventId || !eventId.trim()) return;
|
|
468
|
+
const maxChars = resolveMaxTextFieldChars(this.maxTextFieldCharsOpt);
|
|
469
|
+
if (typeof patch.input === "string" && patch.input.length > maxChars) {
|
|
470
|
+
patch = { ...patch, input: capText(patch.input, maxChars) };
|
|
471
|
+
}
|
|
472
|
+
if (typeof patch.output === "string" && patch.output.length > maxChars) {
|
|
473
|
+
patch = { ...patch, output: capText(patch.output, maxChars) };
|
|
474
|
+
}
|
|
248
475
|
if (this.debug) {
|
|
249
476
|
console.log(`${this.prefix} queue patch`, {
|
|
250
477
|
eventId,
|
|
@@ -291,9 +518,16 @@ var EventShipper = class {
|
|
|
291
518
|
})));
|
|
292
519
|
}
|
|
293
520
|
async shutdown() {
|
|
294
|
-
|
|
295
|
-
this.
|
|
296
|
-
|
|
521
|
+
this.hasShutdown = true;
|
|
522
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
523
|
+
try {
|
|
524
|
+
for (const t of this.timers.values()) clearTimeout(t);
|
|
525
|
+
this.timers.clear();
|
|
526
|
+
const settled = await raceWithTimeout(this.flush(), SHUTDOWN_DEADLINE_MS);
|
|
527
|
+
if (!settled) this.warnShutdownDrop("in-flight request(s) at shutdown");
|
|
528
|
+
} finally {
|
|
529
|
+
this.shutdownDeadlineAt = void 0;
|
|
530
|
+
}
|
|
297
531
|
}
|
|
298
532
|
async trackSignal(signal) {
|
|
299
533
|
var _a, _b;
|
|
@@ -313,16 +547,21 @@ var EventShipper = class {
|
|
|
313
547
|
}
|
|
314
548
|
}
|
|
315
549
|
];
|
|
550
|
+
if (!this.writeKey) return;
|
|
316
551
|
const url = `${this.baseUrl}signals/track`;
|
|
552
|
+
const opts = this.requestOpts();
|
|
553
|
+
if (!opts) {
|
|
554
|
+
this.warnShutdownDrop("signal");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
317
557
|
try {
|
|
318
|
-
await postJson(url, body, this.
|
|
319
|
-
maxAttempts: 3,
|
|
320
|
-
debug: this.debug,
|
|
321
|
-
sdkName: this.sdkName
|
|
322
|
-
});
|
|
558
|
+
await postJson(url, body, this.requestHeaders(), opts);
|
|
323
559
|
} catch (err) {
|
|
324
560
|
const msg = err instanceof Error ? err.message : String(err);
|
|
325
|
-
|
|
561
|
+
rateLimitedLog(
|
|
562
|
+
`${this.prefix}.send_signal_failed`,
|
|
563
|
+
() => console.warn(`${this.prefix} failed to send signal (dropping): ${msg}`)
|
|
564
|
+
);
|
|
326
565
|
}
|
|
327
566
|
}
|
|
328
567
|
async identify(users) {
|
|
@@ -343,19 +582,30 @@ var EventShipper = class {
|
|
|
343
582
|
traits: (_a = user.traits) != null ? _a : {}
|
|
344
583
|
};
|
|
345
584
|
});
|
|
585
|
+
if (!this.writeKey) return;
|
|
346
586
|
if (body.length === 0) return;
|
|
347
587
|
const url = `${this.baseUrl}users/identify`;
|
|
588
|
+
const opts = this.requestOpts();
|
|
589
|
+
if (!opts) {
|
|
590
|
+
this.warnShutdownDrop("identify");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
348
593
|
try {
|
|
349
|
-
await postJson(url, body, this.
|
|
350
|
-
maxAttempts: 3,
|
|
351
|
-
debug: this.debug,
|
|
352
|
-
sdkName: this.sdkName
|
|
353
|
-
});
|
|
594
|
+
await postJson(url, body, this.requestHeaders(), opts);
|
|
354
595
|
} catch (err) {
|
|
355
596
|
const msg = err instanceof Error ? err.message : String(err);
|
|
356
|
-
|
|
597
|
+
rateLimitedLog(
|
|
598
|
+
`${this.prefix}.send_identify_failed`,
|
|
599
|
+
() => console.warn(`${this.prefix} failed to send identify (dropping): ${msg}`)
|
|
600
|
+
);
|
|
357
601
|
}
|
|
358
602
|
}
|
|
603
|
+
warnShutdownDrop(what) {
|
|
604
|
+
rateLimitedLog(
|
|
605
|
+
`${this.prefix}.shutdown_deadline`,
|
|
606
|
+
() => console.warn(`${this.prefix} shutdown flush deadline exceeded; dropping ${what}`)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
359
609
|
async flushOne(eventId) {
|
|
360
610
|
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
361
611
|
if (!this.enabled) return;
|
|
@@ -419,11 +669,25 @@ var EventShipper = class {
|
|
|
419
669
|
endpoint: url
|
|
420
670
|
});
|
|
421
671
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
672
|
+
if (this.localDebuggerUrl) {
|
|
673
|
+
mirrorPartialEventToLocalDebugger(payload, {
|
|
674
|
+
baseUrl: this.localDebuggerUrl,
|
|
675
|
+
writeKey: this.writeKey,
|
|
676
|
+
debug: this.debug,
|
|
677
|
+
sdkName: this.sdkName
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
if (!this.writeKey) {
|
|
681
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const opts = this.requestOpts();
|
|
685
|
+
if (!opts) {
|
|
686
|
+
this.warnShutdownDrop(`track_partial ${eventId}`);
|
|
687
|
+
if (!isPending) this.sticky.delete(eventId);
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const p = postJson(url, payload, this.requestHeaders(), opts);
|
|
427
691
|
this.inFlight.add(p);
|
|
428
692
|
try {
|
|
429
693
|
try {
|
|
@@ -433,7 +697,10 @@ var EventShipper = class {
|
|
|
433
697
|
}
|
|
434
698
|
} catch (err) {
|
|
435
699
|
const msg = err instanceof Error ? err.message : String(err);
|
|
436
|
-
|
|
700
|
+
rateLimitedLog(
|
|
701
|
+
`${this.prefix}.send_track_partial_failed`,
|
|
702
|
+
() => console.warn(`${this.prefix} failed to send track_partial (dropping): ${msg}`)
|
|
703
|
+
);
|
|
437
704
|
}
|
|
438
705
|
} finally {
|
|
439
706
|
this.inFlight.delete(p);
|
|
@@ -443,28 +710,114 @@ var EventShipper = class {
|
|
|
443
710
|
}
|
|
444
711
|
}
|
|
445
712
|
};
|
|
446
|
-
var
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
713
|
+
var DEFAULT_SECRET_KEY_NAMES = [
|
|
714
|
+
"apikey",
|
|
715
|
+
"apisecret",
|
|
716
|
+
"apitoken",
|
|
717
|
+
"secretaccesskey",
|
|
718
|
+
"sessiontoken",
|
|
719
|
+
"privatekey",
|
|
720
|
+
"privatekeyid",
|
|
721
|
+
"clientsecret",
|
|
722
|
+
"accesstoken",
|
|
723
|
+
"refreshtoken",
|
|
724
|
+
"oauthtoken",
|
|
725
|
+
"bearertoken",
|
|
726
|
+
"authorization",
|
|
727
|
+
"password",
|
|
728
|
+
"passphrase"
|
|
729
|
+
];
|
|
730
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
731
|
+
function normalizeKeyName(name) {
|
|
732
|
+
return name.toLowerCase().replace(/[-_.]/g, "");
|
|
451
733
|
}
|
|
452
|
-
function
|
|
453
|
-
var _a;
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
734
|
+
function redactSecretsInObject(value, options) {
|
|
735
|
+
var _a, _b;
|
|
736
|
+
const normalizedSecretSet = buildSecretSet((_a = options == null ? void 0 : options.secretKeyNames) != null ? _a : DEFAULT_SECRET_KEY_NAMES);
|
|
737
|
+
const placeholder = (_b = options == null ? void 0 : options.placeholder) != null ? _b : REDACTED_PLACEHOLDER;
|
|
738
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
739
|
+
const walk = (node) => {
|
|
740
|
+
if (node === null || typeof node !== "object") return node;
|
|
741
|
+
if (seen.has(node)) return "[CIRCULAR]";
|
|
742
|
+
seen.add(node);
|
|
743
|
+
if (Array.isArray(node)) {
|
|
744
|
+
return node.map((item) => walk(item));
|
|
745
|
+
}
|
|
746
|
+
const out = {};
|
|
747
|
+
for (const [k, v] of Object.entries(node)) {
|
|
748
|
+
if (normalizedSecretSet.has(normalizeKeyName(k))) {
|
|
749
|
+
out[k] = placeholder;
|
|
750
|
+
} else {
|
|
751
|
+
out[k] = walk(v);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
};
|
|
756
|
+
return walk(value);
|
|
757
|
+
}
|
|
758
|
+
function buildSecretSet(names) {
|
|
759
|
+
const set = /* @__PURE__ */ new Set();
|
|
760
|
+
for (const name of names) set.add(normalizeKeyName(name));
|
|
761
|
+
return set;
|
|
762
|
+
}
|
|
763
|
+
var DEFAULT_REDACT_ATTRIBUTE_KEYS = [
|
|
764
|
+
"ai.request.providerOptions",
|
|
765
|
+
"ai.response.providerMetadata"
|
|
766
|
+
];
|
|
767
|
+
function defaultTransformSpan(span) {
|
|
768
|
+
const attrs = span.attributes;
|
|
769
|
+
if (!attrs || attrs.length === 0) return span;
|
|
770
|
+
let nextAttrs;
|
|
771
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
772
|
+
const attr = attrs[i];
|
|
773
|
+
const redacted = redactJsonAttributeValue(attr.key, attr.value);
|
|
774
|
+
if (redacted === void 0) continue;
|
|
775
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
776
|
+
nextAttrs[i] = { key: attr.key, value: redacted };
|
|
777
|
+
}
|
|
778
|
+
if (!nextAttrs) return span;
|
|
779
|
+
return { ...span, attributes: nextAttrs };
|
|
780
|
+
}
|
|
781
|
+
var REDACT_JSON_ATTRIBUTE_KEYS = new Set(DEFAULT_REDACT_ATTRIBUTE_KEYS);
|
|
782
|
+
function redactJsonAttributeValue(key, value) {
|
|
783
|
+
if (!REDACT_JSON_ATTRIBUTE_KEYS.has(key)) return void 0;
|
|
784
|
+
const json = value.stringValue;
|
|
785
|
+
if (typeof json !== "string" || json.length === 0) return void 0;
|
|
786
|
+
let parsed;
|
|
787
|
+
try {
|
|
788
|
+
parsed = JSON.parse(json);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
return void 0;
|
|
791
|
+
}
|
|
792
|
+
const scrubbed = redactSecretsInObject(parsed);
|
|
793
|
+
let scrubbedJson;
|
|
794
|
+
try {
|
|
795
|
+
scrubbedJson = JSON.stringify(scrubbed);
|
|
796
|
+
} catch (e) {
|
|
797
|
+
return void 0;
|
|
798
|
+
}
|
|
799
|
+
if (scrubbedJson === json) return void 0;
|
|
800
|
+
return { stringValue: scrubbedJson };
|
|
801
|
+
}
|
|
802
|
+
function applyOtelSpanAttributeLimit(limit) {
|
|
803
|
+
var _a, _b;
|
|
804
|
+
try {
|
|
805
|
+
const raw = (_b = (_a = globalThis == null ? void 0 : globalThis.process) == null ? void 0 : _a.env) == null ? void 0 : _b.OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT;
|
|
806
|
+
if (!raw) return limit;
|
|
807
|
+
const parsed = Number.parseInt(raw, 10);
|
|
808
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
809
|
+
return Math.min(limit, parsed);
|
|
810
|
+
}
|
|
811
|
+
} catch (e) {
|
|
812
|
+
}
|
|
813
|
+
return limit;
|
|
462
814
|
}
|
|
463
815
|
var TraceShipper = class {
|
|
464
816
|
constructor(opts) {
|
|
465
817
|
this.queue = [];
|
|
466
818
|
this.inFlight = /* @__PURE__ */ new Set();
|
|
467
|
-
|
|
819
|
+
this.hasShutdown = false;
|
|
820
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
468
821
|
this.writeKey = (_a = opts.writeKey) == null ? void 0 : _a.trim();
|
|
469
822
|
this.baseUrl = (_b = formatEndpoint(opts.endpoint)) != null ? _b : "https://api.raindrop.ai/v1/";
|
|
470
823
|
this.enabled = opts.enabled !== false;
|
|
@@ -477,13 +830,83 @@ var TraceShipper = class {
|
|
|
477
830
|
this.prefix = `[raindrop-ai/${this.sdkName}]`;
|
|
478
831
|
this.serviceName = (_g = opts.serviceName) != null ? _g : "raindrop.core";
|
|
479
832
|
this.serviceVersion = (_h = opts.serviceVersion) != null ? _h : "0.0.0";
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
this.
|
|
483
|
-
|
|
484
|
-
|
|
833
|
+
this.localDebuggerUrl = (_i = resolveLocalDebuggerBaseUrl(opts.localDebuggerUrl)) != null ? _i : void 0;
|
|
834
|
+
if (this.debug && this.localDebuggerUrl) {
|
|
835
|
+
console.log(`${this.prefix} Local debugger mirroring: ${this.localDebuggerUrl}`);
|
|
836
|
+
}
|
|
837
|
+
this.projectId = normalizeProjectId(opts.projectId, {
|
|
838
|
+
debug: this.debug,
|
|
839
|
+
prefix: this.prefix
|
|
840
|
+
});
|
|
841
|
+
this.transformSpanHook = opts.transformSpan;
|
|
842
|
+
this.disableDefaultRedaction = opts.disableDefaultRedaction === true;
|
|
843
|
+
this.maxTextFieldCharsOpt = opts.maxTextFieldChars;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Cap every string attribute value on the span. O(#attributes) length
|
|
847
|
+
* checks; only oversized values pay a slice. Runs AFTER the redaction
|
|
848
|
+
* pipeline so the default secret-scrub still sees parseable JSON in
|
|
849
|
+
* `ai.request.providerOptions` / `ai.response.providerMetadata` (capping
|
|
850
|
+
* first could cut a JSON blob mid-way, fail the parse, and ship secrets
|
|
851
|
+
* in the surviving prefix).
|
|
852
|
+
*
|
|
853
|
+
* A stricter `OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT` env var is honored
|
|
854
|
+
* for span content, matching the Python SDK and the OTel SDK convention.
|
|
855
|
+
*/
|
|
856
|
+
capSpanAttributes(span) {
|
|
857
|
+
var _a;
|
|
858
|
+
const maxChars = applyOtelSpanAttributeLimit(
|
|
859
|
+
resolveMaxTextFieldChars(this.maxTextFieldCharsOpt)
|
|
860
|
+
);
|
|
861
|
+
const attrs = span.attributes;
|
|
862
|
+
if (!attrs || attrs.length === 0) return span;
|
|
863
|
+
let nextAttrs;
|
|
864
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
865
|
+
const attr = attrs[i];
|
|
866
|
+
const value = (_a = attr.value) == null ? void 0 : _a.stringValue;
|
|
867
|
+
if (typeof value !== "string" || value.length <= maxChars) continue;
|
|
868
|
+
if (!nextAttrs) nextAttrs = attrs.slice();
|
|
869
|
+
nextAttrs[i] = {
|
|
870
|
+
key: attr.key,
|
|
871
|
+
value: { ...attr.value, stringValue: capText(value, maxChars) }
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
if (!nextAttrs) return span;
|
|
875
|
+
return { ...span, attributes: nextAttrs };
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Apply the user `transformSpan` hook (if any) followed by the default
|
|
879
|
+
* redactor (unless disabled). Returns either the (possibly new) span to
|
|
880
|
+
* ship, or `null` to drop the span entirely.
|
|
881
|
+
*
|
|
882
|
+
* Ordering: user hook runs first so callers can rewrite the span freely
|
|
883
|
+
* (rename attrs, add new ones, scrub things the default doesn't know
|
|
884
|
+
* about). The default redactor then runs on whatever the user produced,
|
|
885
|
+
* acting as the always-on floor for documented BYOK secrets. If the user
|
|
886
|
+
* sets `disableDefaultRedaction: true`, the floor is skipped.
|
|
887
|
+
*
|
|
888
|
+
* Fail-closed: if the user hook throws, the span is dropped — a buggy
|
|
889
|
+
* hook can never accidentally ship raw, un-redacted spans.
|
|
890
|
+
*/
|
|
891
|
+
redactSpan(span) {
|
|
892
|
+
let current = span;
|
|
893
|
+
if (this.transformSpanHook) {
|
|
894
|
+
try {
|
|
895
|
+
const result = this.transformSpanHook(current);
|
|
896
|
+
if (result === null) return null;
|
|
897
|
+
if (result !== void 0) current = result;
|
|
898
|
+
} catch (err) {
|
|
899
|
+
if (this.debug) {
|
|
900
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
901
|
+
console.warn(`${this.prefix} transformSpan hook threw: ${msg}`);
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
485
904
|
}
|
|
486
905
|
}
|
|
906
|
+
if (!this.disableDefaultRedaction) {
|
|
907
|
+
current = defaultTransformSpan(current);
|
|
908
|
+
}
|
|
909
|
+
return this.capSpanAttributes(current);
|
|
487
910
|
}
|
|
488
911
|
isDebugEnabled() {
|
|
489
912
|
return this.debug;
|
|
@@ -491,6 +914,9 @@ var TraceShipper = class {
|
|
|
491
914
|
authHeaders() {
|
|
492
915
|
return this.writeKey ? { Authorization: `Bearer ${this.writeKey}` } : {};
|
|
493
916
|
}
|
|
917
|
+
requestHeaders() {
|
|
918
|
+
return { ...this.authHeaders(), ...projectIdHeaders(this.projectId) };
|
|
919
|
+
}
|
|
494
920
|
startSpan(args2) {
|
|
495
921
|
var _a, _b;
|
|
496
922
|
const ids = createSpanIds(args2.parent);
|
|
@@ -501,8 +927,8 @@ var TraceShipper = class {
|
|
|
501
927
|
];
|
|
502
928
|
if ((_b = args2.attributes) == null ? void 0 : _b.length) attrs.push(...args2.attributes);
|
|
503
929
|
const span = { ids, name: args2.name, startTimeUnixNano: started, attributes: attrs };
|
|
504
|
-
|
|
505
|
-
|
|
930
|
+
this.mirrorToLocalDebugger(
|
|
931
|
+
buildOtlpSpan({
|
|
506
932
|
ids: span.ids,
|
|
507
933
|
name: span.name,
|
|
508
934
|
startTimeUnixNano: span.startTimeUnixNano,
|
|
@@ -510,16 +936,21 @@ var TraceShipper = class {
|
|
|
510
936
|
// placeholder — will be updated on endSpan
|
|
511
937
|
attributes: span.attributes,
|
|
512
938
|
status: { code: SpanStatusCode.UNSET }
|
|
513
|
-
})
|
|
514
|
-
|
|
515
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
516
|
-
baseUrl: this.localDebuggerUrl,
|
|
517
|
-
debug: false,
|
|
518
|
-
sdkName: this.sdkName
|
|
519
|
-
});
|
|
520
|
-
}
|
|
939
|
+
})
|
|
940
|
+
);
|
|
521
941
|
return span;
|
|
522
942
|
}
|
|
943
|
+
mirrorToLocalDebugger(span) {
|
|
944
|
+
if (!this.localDebuggerUrl) return;
|
|
945
|
+
const redacted = this.redactSpan(span);
|
|
946
|
+
if (redacted === null) return;
|
|
947
|
+
const body = buildExportTraceServiceRequest([redacted], this.serviceName, this.serviceVersion);
|
|
948
|
+
mirrorTraceExportToLocalDebugger(body, {
|
|
949
|
+
baseUrl: this.localDebuggerUrl,
|
|
950
|
+
debug: false,
|
|
951
|
+
sdkName: this.sdkName
|
|
952
|
+
});
|
|
953
|
+
}
|
|
523
954
|
endSpan(span, extra) {
|
|
524
955
|
var _a, _b;
|
|
525
956
|
if (span.endTimeUnixNano) return;
|
|
@@ -541,14 +972,7 @@ var TraceShipper = class {
|
|
|
541
972
|
status
|
|
542
973
|
});
|
|
543
974
|
this.enqueue(otlp);
|
|
544
|
-
|
|
545
|
-
const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
|
|
546
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
547
|
-
baseUrl: this.localDebuggerUrl,
|
|
548
|
-
debug: false,
|
|
549
|
-
sdkName: this.sdkName
|
|
550
|
-
});
|
|
551
|
-
}
|
|
975
|
+
this.mirrorToLocalDebugger(otlp);
|
|
552
976
|
}
|
|
553
977
|
createSpan(args2) {
|
|
554
978
|
var _a;
|
|
@@ -566,14 +990,7 @@ var TraceShipper = class {
|
|
|
566
990
|
status: args2.status
|
|
567
991
|
});
|
|
568
992
|
this.enqueue(otlp);
|
|
569
|
-
|
|
570
|
-
const body = buildExportTraceServiceRequest([otlp], this.serviceName, this.serviceVersion);
|
|
571
|
-
mirrorTraceExportToLocalDebugger(body, {
|
|
572
|
-
baseUrl: this.localDebuggerUrl,
|
|
573
|
-
debug: false,
|
|
574
|
-
sdkName: this.sdkName
|
|
575
|
-
});
|
|
576
|
-
}
|
|
993
|
+
this.mirrorToLocalDebugger(otlp);
|
|
577
994
|
}
|
|
578
995
|
enqueue(span) {
|
|
579
996
|
if (!this.enabled) return;
|
|
@@ -585,10 +1002,12 @@ var TraceShipper = class {
|
|
|
585
1002
|
)}`
|
|
586
1003
|
);
|
|
587
1004
|
}
|
|
1005
|
+
const redacted = this.redactSpan(span);
|
|
1006
|
+
if (redacted === null) return;
|
|
588
1007
|
if (this.queue.length >= this.maxQueueSize) {
|
|
589
1008
|
this.queue.shift();
|
|
590
1009
|
}
|
|
591
|
-
this.queue.push(
|
|
1010
|
+
this.queue.push(redacted);
|
|
592
1011
|
if (this.queue.length >= this.maxBatchSize) {
|
|
593
1012
|
void this.flush().catch(() => {
|
|
594
1013
|
});
|
|
@@ -610,6 +1029,17 @@ var TraceShipper = class {
|
|
|
610
1029
|
}
|
|
611
1030
|
while (this.queue.length > 0) {
|
|
612
1031
|
const batch = this.queue.splice(0, this.maxBatchSize);
|
|
1032
|
+
if (!this.writeKey) continue;
|
|
1033
|
+
const opts = this.requestOpts();
|
|
1034
|
+
if (!opts) {
|
|
1035
|
+
rateLimitedLog(
|
|
1036
|
+
`${this.prefix}.shutdown_deadline`,
|
|
1037
|
+
() => console.warn(
|
|
1038
|
+
`${this.prefix} shutdown flush deadline exceeded; dropping ${batch.length} spans`
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
613
1043
|
const body = buildExportTraceServiceRequest(batch, this.serviceName, this.serviceVersion);
|
|
614
1044
|
const url = `${this.baseUrl}traces`;
|
|
615
1045
|
if (this.debug) {
|
|
@@ -618,11 +1048,7 @@ var TraceShipper = class {
|
|
|
618
1048
|
endpoint: url
|
|
619
1049
|
});
|
|
620
1050
|
}
|
|
621
|
-
const p = postJson(url, body, this.
|
|
622
|
-
maxAttempts: 3,
|
|
623
|
-
debug: this.debug,
|
|
624
|
-
sdkName: this.sdkName
|
|
625
|
-
});
|
|
1051
|
+
const p = postJson(url, body, this.requestHeaders(), opts);
|
|
626
1052
|
this.inFlight.add(p);
|
|
627
1053
|
try {
|
|
628
1054
|
try {
|
|
@@ -630,21 +1056,61 @@ var TraceShipper = class {
|
|
|
630
1056
|
if (this.debug) console.log(`${this.prefix} sent ${batch.length} spans`);
|
|
631
1057
|
} catch (err) {
|
|
632
1058
|
const msg = err instanceof Error ? err.message : String(err);
|
|
633
|
-
|
|
1059
|
+
rateLimitedLog(
|
|
1060
|
+
`${this.prefix}.send_spans_failed`,
|
|
1061
|
+
() => console.warn(`${this.prefix} failed to send ${batch.length} spans: ${msg}`)
|
|
1062
|
+
);
|
|
634
1063
|
}
|
|
635
1064
|
} finally {
|
|
636
1065
|
this.inFlight.delete(p);
|
|
637
1066
|
}
|
|
638
1067
|
}
|
|
639
1068
|
}
|
|
1069
|
+
/** See EventShipper.requestOpts — same shutdown-budget semantics. */
|
|
1070
|
+
requestOpts() {
|
|
1071
|
+
if (this.shutdownDeadlineAt !== void 0) {
|
|
1072
|
+
const remainingMs = this.shutdownDeadlineAt - Date.now();
|
|
1073
|
+
if (remainingMs <= 0) return null;
|
|
1074
|
+
return {
|
|
1075
|
+
maxAttempts: 1,
|
|
1076
|
+
debug: this.debug,
|
|
1077
|
+
sdkName: this.sdkName,
|
|
1078
|
+
timeoutMs: Math.min(DEFAULT_REQUEST_TIMEOUT_MS, remainingMs)
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (this.hasShutdown) {
|
|
1082
|
+
return {
|
|
1083
|
+
maxAttempts: 1,
|
|
1084
|
+
debug: this.debug,
|
|
1085
|
+
sdkName: this.sdkName,
|
|
1086
|
+
timeoutMs: POST_SHUTDOWN_TIMEOUT_MS
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
return { maxAttempts: 3, debug: this.debug, sdkName: this.sdkName };
|
|
1090
|
+
}
|
|
640
1091
|
async shutdown() {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
1092
|
+
this.hasShutdown = true;
|
|
1093
|
+
this.shutdownDeadlineAt = Date.now() + SHUTDOWN_DEADLINE_MS;
|
|
1094
|
+
try {
|
|
1095
|
+
if (this.timer) {
|
|
1096
|
+
clearTimeout(this.timer);
|
|
1097
|
+
this.timer = void 0;
|
|
1098
|
+
}
|
|
1099
|
+
const drain = async () => {
|
|
1100
|
+
await this.flush();
|
|
1101
|
+
await Promise.all([...this.inFlight].map((p) => p.catch(() => {
|
|
1102
|
+
})));
|
|
1103
|
+
};
|
|
1104
|
+
const settled = await raceWithTimeout(drain(), SHUTDOWN_DEADLINE_MS);
|
|
1105
|
+
if (!settled) {
|
|
1106
|
+
rateLimitedLog(
|
|
1107
|
+
`${this.prefix}.shutdown_deadline`,
|
|
1108
|
+
() => console.warn(`${this.prefix} shutdown flush deadline exceeded; abandoning in-flight spans`)
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
} finally {
|
|
1112
|
+
this.shutdownDeadlineAt = void 0;
|
|
644
1113
|
}
|
|
645
|
-
await this.flush();
|
|
646
|
-
await Promise.all([...this.inFlight].map((p) => p.catch(() => {
|
|
647
|
-
})));
|
|
648
1114
|
}
|
|
649
1115
|
};
|
|
650
1116
|
|
|
@@ -909,18 +1375,85 @@ function transcriptToProperties(summary) {
|
|
|
909
1375
|
|
|
910
1376
|
// src/event-mapper.ts
|
|
911
1377
|
var MAX_ATTR_LENGTH = 32768;
|
|
1378
|
+
var TRUNCATION_MARKER2 = "...[truncated by raindrop]";
|
|
1379
|
+
var MAX_TEXT_FIELD_CHARS = 1e6;
|
|
1380
|
+
var MAX_BOUNDED_DEPTH = 12;
|
|
912
1381
|
function truncate(value) {
|
|
913
1382
|
if (value === void 0) return void 0;
|
|
914
1383
|
if (value.length <= MAX_ATTR_LENGTH) return value;
|
|
915
1384
|
const suffix = "\n...[truncated]";
|
|
916
1385
|
return value.slice(0, MAX_ATTR_LENGTH - suffix.length) + suffix;
|
|
917
1386
|
}
|
|
1387
|
+
function capText2(value, limit = MAX_TEXT_FIELD_CHARS) {
|
|
1388
|
+
if (value.length <= limit) return value;
|
|
1389
|
+
if (limit > TRUNCATION_MARKER2.length) {
|
|
1390
|
+
return value.slice(0, limit - TRUNCATION_MARKER2.length) + TRUNCATION_MARKER2;
|
|
1391
|
+
}
|
|
1392
|
+
return value.slice(0, limit);
|
|
1393
|
+
}
|
|
1394
|
+
function boundedClone2(value, budget, depth) {
|
|
1395
|
+
if (budget.remaining <= 0) return TRUNCATION_MARKER2;
|
|
1396
|
+
if (typeof value === "string") {
|
|
1397
|
+
if (value.length > budget.remaining) {
|
|
1398
|
+
const taken = value.slice(0, Math.max(0, budget.remaining)) + TRUNCATION_MARKER2;
|
|
1399
|
+
budget.remaining = 0;
|
|
1400
|
+
return taken;
|
|
1401
|
+
}
|
|
1402
|
+
budget.remaining -= Math.max(value.length, 1);
|
|
1403
|
+
return value;
|
|
1404
|
+
}
|
|
1405
|
+
if (value === null || typeof value === "number" || typeof value === "boolean") {
|
|
1406
|
+
budget.remaining -= 8;
|
|
1407
|
+
return value;
|
|
1408
|
+
}
|
|
1409
|
+
if (typeof value !== "object") {
|
|
1410
|
+
budget.remaining -= 8;
|
|
1411
|
+
return value;
|
|
1412
|
+
}
|
|
1413
|
+
if (depth >= MAX_BOUNDED_DEPTH) {
|
|
1414
|
+
budget.remaining -= 16;
|
|
1415
|
+
return `<max depth: ${TRUNCATION_MARKER2}>`;
|
|
1416
|
+
}
|
|
1417
|
+
const withToJson = value;
|
|
1418
|
+
if (typeof withToJson.toJSON === "function") {
|
|
1419
|
+
try {
|
|
1420
|
+
return boundedClone2(withToJson.toJSON(), budget, depth + 1);
|
|
1421
|
+
} catch (e) {
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (Array.isArray(value)) {
|
|
1425
|
+
const out2 = [];
|
|
1426
|
+
for (const item of value) {
|
|
1427
|
+
if (budget.remaining <= 0) {
|
|
1428
|
+
out2.push(TRUNCATION_MARKER2);
|
|
1429
|
+
break;
|
|
1430
|
+
}
|
|
1431
|
+
out2.push(boundedClone2(item, budget, depth + 1));
|
|
1432
|
+
}
|
|
1433
|
+
return out2;
|
|
1434
|
+
}
|
|
1435
|
+
const out = {};
|
|
1436
|
+
for (const key of Object.keys(value)) {
|
|
1437
|
+
if (budget.remaining <= 0) {
|
|
1438
|
+
out["..."] = TRUNCATION_MARKER2;
|
|
1439
|
+
break;
|
|
1440
|
+
}
|
|
1441
|
+
budget.remaining -= Math.max(key.length, 1);
|
|
1442
|
+
out[key] = boundedClone2(value[key], budget, depth + 1);
|
|
1443
|
+
}
|
|
1444
|
+
return out;
|
|
1445
|
+
}
|
|
918
1446
|
function safeStringify(value) {
|
|
919
1447
|
if (value === void 0 || value === null) return void 0;
|
|
920
1448
|
try {
|
|
921
|
-
|
|
1449
|
+
const pruned = boundedClone2(
|
|
1450
|
+
value,
|
|
1451
|
+
{ remaining: MAX_ATTR_LENGTH + TRUNCATION_MARKER2.length + 256 },
|
|
1452
|
+
0
|
|
1453
|
+
);
|
|
1454
|
+
return truncate(JSON.stringify(pruned));
|
|
922
1455
|
} catch (e) {
|
|
923
|
-
return truncate(String(value));
|
|
1456
|
+
return truncate(capText2(String(value), MAX_ATTR_LENGTH + 16));
|
|
924
1457
|
}
|
|
925
1458
|
}
|
|
926
1459
|
var STATE_DIR = join(tmpdir(), "raindrop-claude-code");
|
|
@@ -1101,7 +1634,9 @@ async function handleUserPromptSubmit(payload, convoId, config, properties, even
|
|
|
1101
1634
|
userId: config.userId,
|
|
1102
1635
|
convoId,
|
|
1103
1636
|
eventName: config.eventName,
|
|
1104
|
-
|
|
1637
|
+
// Capped so a pasted multi-MB prompt can't produce an event that the
|
|
1638
|
+
// ingest API rejects (silent data loss) — it lands truncated instead.
|
|
1639
|
+
input: payload.prompt !== void 0 ? capText2(payload.prompt) : void 0,
|
|
1105
1640
|
model,
|
|
1106
1641
|
properties
|
|
1107
1642
|
};
|
|
@@ -1224,7 +1759,7 @@ async function handlePostCompact(payload, eventId, convoId, config, properties,
|
|
|
1224
1759
|
properties: {
|
|
1225
1760
|
...properties,
|
|
1226
1761
|
compaction_trigger: payload.trigger,
|
|
1227
|
-
compact_summary: payload.compact_summary
|
|
1762
|
+
compact_summary: payload.compact_summary !== void 0 ? capText2(payload.compact_summary) : void 0
|
|
1228
1763
|
}
|
|
1229
1764
|
});
|
|
1230
1765
|
}
|
|
@@ -1459,7 +1994,7 @@ function enrichFromTranscript(payload, eventId, traceShipper) {
|
|
|
1459
1994
|
attrInt("gen_ai.usage.prompt_tokens", phase.inputTokens),
|
|
1460
1995
|
attrInt("gen_ai.usage.completion_tokens", phase.outputTokens),
|
|
1461
1996
|
...phase.cacheReadTokens > 0 ? [attrInt("gen_ai.usage.cache_read_tokens", phase.cacheReadTokens)] : [],
|
|
1462
|
-
attrString("ai.response.text", outputText),
|
|
1997
|
+
attrString("ai.response.text", truncate(outputText)),
|
|
1463
1998
|
...toolCallNames ? [attrString("ai.toolCall.names", toolCallNames)] : [],
|
|
1464
1999
|
...phase.hasThinking ? [attrString("ai.has_thinking", "true")] : [],
|
|
1465
2000
|
attrInt("ai.llm_call.index", emittedPhaseIndex)
|
|
@@ -1519,7 +2054,7 @@ async function handleStopOrFailure(payload, eventId, convoId, config, properties
|
|
|
1519
2054
|
userId: config.userId,
|
|
1520
2055
|
convoId,
|
|
1521
2056
|
eventName: config.eventName,
|
|
1522
|
-
output,
|
|
2057
|
+
output: output !== void 0 ? capText2(output) : void 0,
|
|
1523
2058
|
...(summary == null ? void 0 : summary.model) ? { model: summary.model } : {},
|
|
1524
2059
|
properties: {
|
|
1525
2060
|
...properties,
|
|
@@ -1549,7 +2084,7 @@ import { homedir, userInfo } from "os";
|
|
|
1549
2084
|
import { dirname, join as join2 } from "path";
|
|
1550
2085
|
var CONFIG_PATH = join2(homedir(), ".config", "raindrop", "config.json");
|
|
1551
2086
|
function loadConfig() {
|
|
1552
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
|
|
2087
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
|
|
1553
2088
|
let file = {};
|
|
1554
2089
|
try {
|
|
1555
2090
|
if (existsSync3(CONFIG_PATH)) {
|
|
@@ -1589,9 +2124,10 @@ function loadConfig() {
|
|
|
1589
2124
|
return {
|
|
1590
2125
|
writeKey: (_c = (_b = process.env["RAINDROP_WRITE_KEY"]) != null ? _b : file.write_key) != null ? _c : "",
|
|
1591
2126
|
endpoint: (_e = (_d = process.env["RAINDROP_API_URL"]) != null ? _d : file.api_url) != null ? _e : "https://api.raindrop.ai/v1",
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
2127
|
+
projectId: (_f = process.env["RAINDROP_PROJECT_ID"]) != null ? _f : file.project_id,
|
|
2128
|
+
userId: (_h = (_g = process.env["RAINDROP_USER_ID"]) != null ? _g : file.user_id) != null ? _h : systemUser,
|
|
2129
|
+
convoId: ((_i = process.env["RAINDROP_CONVO_ID"]) == null ? void 0 : _i.trim()) || void 0,
|
|
2130
|
+
debug: process.env["RAINDROP_DEBUG"] === "true" ? true : (_j = file.debug) != null ? _j : false,
|
|
1595
2131
|
enabled: (() => {
|
|
1596
2132
|
var _a2;
|
|
1597
2133
|
const env = process.env["RAINDROP_ENABLED"];
|
|
@@ -1599,7 +2135,7 @@ function loadConfig() {
|
|
|
1599
2135
|
const lower = env.toLowerCase();
|
|
1600
2136
|
return lower !== "false" && env !== "0";
|
|
1601
2137
|
})(),
|
|
1602
|
-
eventName: (
|
|
2138
|
+
eventName: (_l = (_k = process.env["RAINDROP_EVENT_NAME"]) != null ? _k : file.event_name) != null ? _l : "claude_code_session",
|
|
1603
2139
|
customProperties,
|
|
1604
2140
|
selfDiagnostics
|
|
1605
2141
|
};
|
|
@@ -1627,6 +2163,7 @@ import { join as join3 } from "path";
|
|
|
1627
2163
|
var DEFAULT_PORT = 5899;
|
|
1628
2164
|
var DEFAULT_URL = `http://localhost:${DEFAULT_PORT}/v1/`;
|
|
1629
2165
|
var HEALTH_TIMEOUT_MS = 300;
|
|
2166
|
+
var MIRROR_TIMEOUT_MS = 2e3;
|
|
1630
2167
|
var CACHE_DIR = join3(tmpdir2(), "raindrop-claude-code");
|
|
1631
2168
|
var CACHE_FILE = join3(CACHE_DIR, "debugger_cache");
|
|
1632
2169
|
var CACHE_TTL_MS = 5e3;
|
|
@@ -1650,10 +2187,8 @@ function writeCache(url) {
|
|
|
1650
2187
|
async function healthPing(baseUrl) {
|
|
1651
2188
|
const healthUrl = baseUrl.replace(/\/v1\/$/, "/health");
|
|
1652
2189
|
try {
|
|
1653
|
-
const
|
|
1654
|
-
const
|
|
1655
|
-
const resp = await fetch(healthUrl, { signal: controller.signal });
|
|
1656
|
-
clearTimeout(timeout);
|
|
2190
|
+
const signal = AbortSignal.timeout(HEALTH_TIMEOUT_MS);
|
|
2191
|
+
const resp = await fetch(healthUrl, { signal });
|
|
1657
2192
|
if (resp.ok) {
|
|
1658
2193
|
const body = await resp.json();
|
|
1659
2194
|
if (body.ok) return baseUrl;
|
|
@@ -1696,7 +2231,8 @@ function mirrorEventToLocalDebugger(baseUrl, payload, debug) {
|
|
|
1696
2231
|
void fetch(url, {
|
|
1697
2232
|
method: "POST",
|
|
1698
2233
|
headers: { "Content-Type": "application/json" },
|
|
1699
|
-
body: JSON.stringify(payload)
|
|
2234
|
+
body: JSON.stringify(payload),
|
|
2235
|
+
signal: AbortSignal.timeout(MIRROR_TIMEOUT_MS)
|
|
1700
2236
|
}).catch(() => {
|
|
1701
2237
|
});
|
|
1702
2238
|
}
|
|
@@ -1707,6 +2243,35 @@ import { join as join4 } from "path";
|
|
|
1707
2243
|
import { tmpdir as tmpdir3 } from "os";
|
|
1708
2244
|
var STDIN_TIMEOUT_MS = 5e3;
|
|
1709
2245
|
var STATE_DIR2 = join4(tmpdir3(), "raindrop-claude-code");
|
|
2246
|
+
var DEFAULT_HOOK_FLUSH_DEADLINE_MS = 8e3;
|
|
2247
|
+
var MIN_FLUSH_WINDOW_MS = 250;
|
|
2248
|
+
function hookFlushDeadlineMs() {
|
|
2249
|
+
const raw = process.env["RAINDROP_HOOK_FLUSH_DEADLINE_MS"];
|
|
2250
|
+
if (raw) {
|
|
2251
|
+
const parsed = parseInt(raw, 10);
|
|
2252
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
2253
|
+
}
|
|
2254
|
+
return DEFAULT_HOOK_FLUSH_DEADLINE_MS;
|
|
2255
|
+
}
|
|
2256
|
+
function withDeadline(promise, ms) {
|
|
2257
|
+
return new Promise((resolve2, reject) => {
|
|
2258
|
+
const timer = setTimeout(() => {
|
|
2259
|
+
promise.catch(() => {
|
|
2260
|
+
});
|
|
2261
|
+
resolve2("deadline");
|
|
2262
|
+
}, ms);
|
|
2263
|
+
promise.then(
|
|
2264
|
+
(value) => {
|
|
2265
|
+
clearTimeout(timer);
|
|
2266
|
+
resolve2(value);
|
|
2267
|
+
},
|
|
2268
|
+
(err) => {
|
|
2269
|
+
clearTimeout(timer);
|
|
2270
|
+
reject(err);
|
|
2271
|
+
}
|
|
2272
|
+
);
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
1710
2275
|
function getOtlpTraceId(sessionId) {
|
|
1711
2276
|
try {
|
|
1712
2277
|
const key = `rootspan_${sessionId}`.replace(/[^a-zA-Z0-9_\-]/g, "_");
|
|
@@ -1744,6 +2309,7 @@ function readStdin() {
|
|
|
1744
2309
|
});
|
|
1745
2310
|
}
|
|
1746
2311
|
async function handleHook() {
|
|
2312
|
+
const startedAtMs = Date.now();
|
|
1747
2313
|
const raw = await readStdin();
|
|
1748
2314
|
if (!raw.trim()) {
|
|
1749
2315
|
return;
|
|
@@ -1781,6 +2347,7 @@ async function handleHook() {
|
|
|
1781
2347
|
writeKey: config.writeKey,
|
|
1782
2348
|
endpoint: config.endpoint,
|
|
1783
2349
|
debug: config.debug,
|
|
2350
|
+
projectId: config.projectId,
|
|
1784
2351
|
// Only ship to the cloud when we have an API key. When only the local
|
|
1785
2352
|
// debugger is present the shipper still needs to be instantiated so the
|
|
1786
2353
|
// event mapper can call patch/finish, but cloud POSTs are skipped.
|
|
@@ -1790,6 +2357,7 @@ async function handleHook() {
|
|
|
1790
2357
|
writeKey: config.writeKey,
|
|
1791
2358
|
endpoint: config.endpoint,
|
|
1792
2359
|
debug: config.debug,
|
|
2360
|
+
projectId: config.projectId,
|
|
1793
2361
|
enabled: hasCloudKey
|
|
1794
2362
|
});
|
|
1795
2363
|
if (debugger_.url) {
|
|
@@ -1819,9 +2387,23 @@ async function handleHook() {
|
|
|
1819
2387
|
};
|
|
1820
2388
|
}
|
|
1821
2389
|
try {
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2390
|
+
const remainingMs = Math.max(
|
|
2391
|
+
MIN_FLUSH_WINDOW_MS,
|
|
2392
|
+
hookFlushDeadlineMs() - (Date.now() - startedAtMs)
|
|
2393
|
+
);
|
|
2394
|
+
const outcome = await withDeadline(
|
|
2395
|
+
(async () => {
|
|
2396
|
+
await mapHookToRaindrop(payload, mapperConfig, eventShipper, traceShipper);
|
|
2397
|
+
await Promise.all([eventShipper.flush(), traceShipper.flush()]);
|
|
2398
|
+
await Promise.all([eventShipper.shutdown(), traceShipper.shutdown()]);
|
|
2399
|
+
})(),
|
|
2400
|
+
remainingMs
|
|
2401
|
+
);
|
|
2402
|
+
if (outcome === "deadline" && config.debug) {
|
|
2403
|
+
console.error(
|
|
2404
|
+
`[raindrop-ai/claude-code] hook work exceeded the ${hookFlushDeadlineMs()}ms deadline; exiting without waiting on the network (pending telemetry dropped)`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
1825
2407
|
} catch (err) {
|
|
1826
2408
|
if (config.debug) {
|
|
1827
2409
|
console.error(
|
|
@@ -2294,6 +2876,7 @@ async function executeTool(args2) {
|
|
|
2294
2876
|
writeKey: config.writeKey,
|
|
2295
2877
|
endpoint: config.endpoint,
|
|
2296
2878
|
debug: false,
|
|
2879
|
+
projectId: config.projectId,
|
|
2297
2880
|
enabled: true
|
|
2298
2881
|
});
|
|
2299
2882
|
try {
|
|
@@ -2465,7 +3048,7 @@ async function main() {
|
|
|
2465
3048
|
}
|
|
2466
3049
|
case "hook": {
|
|
2467
3050
|
await handleHook();
|
|
2468
|
-
|
|
3051
|
+
process.exit(0);
|
|
2469
3052
|
}
|
|
2470
3053
|
case "mcp-serve": {
|
|
2471
3054
|
await startMcpServer();
|