@runtimescope/collector 0.6.2 → 0.7.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/dist/index.js CHANGED
@@ -1,952 +1,25 @@
1
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
- }) : x)(function(x) {
4
- if (typeof require !== "undefined") return require.apply(this, arguments);
5
- throw Error('Dynamic require of "' + x + '" is not supported');
6
- });
7
-
8
- // src/server.ts
9
- import { createServer as createHttpsServer } from "https";
10
- import { WebSocketServer } from "ws";
11
-
12
- // src/ring-buffer.ts
13
- var RingBuffer = class {
14
- constructor(capacity) {
15
- this.capacity = capacity;
16
- this.buffer = new Array(capacity);
17
- }
18
- buffer;
19
- head = 0;
20
- _count = 0;
21
- get count() {
22
- return this._count;
23
- }
24
- push(item) {
25
- this.buffer[this.head] = item;
26
- this.head = (this.head + 1) % this.capacity;
27
- if (this._count < this.capacity) this._count++;
28
- }
29
- /** Returns all items from oldest to newest. */
30
- toArray() {
31
- if (this._count === 0) return [];
32
- const result = [];
33
- const start = this._count < this.capacity ? 0 : this.head;
34
- for (let i = 0; i < this._count; i++) {
35
- const idx = (start + i) % this.capacity;
36
- result.push(this.buffer[idx]);
37
- }
38
- return result;
39
- }
40
- /** Returns matching items from newest to oldest (most recent first). */
41
- query(predicate) {
42
- if (this._count === 0) return [];
43
- const result = [];
44
- const start = this._count < this.capacity ? 0 : this.head;
45
- for (let i = this._count - 1; i >= 0; i--) {
46
- const idx = (start + i) % this.capacity;
47
- const item = this.buffer[idx];
48
- if (predicate(item)) result.push(item);
49
- }
50
- return result;
51
- }
52
- clear() {
53
- this.buffer = new Array(this.capacity);
54
- this.head = 0;
55
- this._count = 0;
56
- }
57
- };
58
-
59
- // src/store.ts
60
- var EventStore = class {
61
- buffer;
62
- sessions = /* @__PURE__ */ new Map();
63
- sqliteStore = null;
64
- currentProject = null;
65
- onEventCallbacks = [];
66
- redactor = null;
67
- constructor(capacity = 1e4) {
68
- this.buffer = new RingBuffer(capacity);
69
- }
70
- setRedactor(redactor) {
71
- this.redactor = redactor;
72
- }
73
- get eventCount() {
74
- return this.buffer.count;
75
- }
76
- setSqliteStore(store, project) {
77
- this.sqliteStore = store;
78
- this.currentProject = project;
79
- }
80
- onEvent(callback) {
81
- this.onEventCallbacks.push(callback);
82
- }
83
- removeEventListener(callback) {
84
- const idx = this.onEventCallbacks.indexOf(callback);
85
- if (idx !== -1) this.onEventCallbacks.splice(idx, 1);
86
- }
87
- addEvent(event) {
88
- if (this.redactor?.isEnabled()) {
89
- event = this.redactor.redactEvent(event);
90
- }
91
- this.buffer.push(event);
92
- if (event.eventType === "session") {
93
- const se = event;
94
- this.sessions.set(se.sessionId, {
95
- sessionId: se.sessionId,
96
- appName: se.appName,
97
- connectedAt: se.connectedAt,
98
- sdkVersion: se.sdkVersion,
99
- eventCount: 0,
100
- isConnected: true
101
- });
102
- }
103
- const session = this.sessions.get(event.sessionId);
104
- if (session) session.eventCount++;
105
- if (this.sqliteStore && this.currentProject) {
106
- this.sqliteStore.addEvent(event, this.currentProject);
107
- }
108
- for (const cb of this.onEventCallbacks) {
109
- try {
110
- cb(event);
111
- } catch {
112
- }
113
- }
114
- }
115
- getNetworkRequests(filter = {}) {
116
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
117
- return this.buffer.query((e) => {
118
- if (e.eventType !== "network") return false;
119
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
120
- const ne = e;
121
- if (ne.timestamp < since) return false;
122
- if (filter.urlPattern && !ne.url.includes(filter.urlPattern)) return false;
123
- if (filter.status !== void 0 && ne.status !== filter.status) return false;
124
- if (filter.method && ne.method.toUpperCase() !== filter.method.toUpperCase())
125
- return false;
126
- return true;
127
- });
128
- }
129
- getConsoleMessages(filter = {}) {
130
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
131
- return this.buffer.query((e) => {
132
- if (e.eventType !== "console") return false;
133
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
134
- const ce = e;
135
- if (ce.timestamp < since) return false;
136
- if (filter.level && ce.level !== filter.level) return false;
137
- if (filter.search && !ce.message.toLowerCase().includes(filter.search.toLowerCase()))
138
- return false;
139
- return true;
140
- });
141
- }
142
- getSessionInfo() {
143
- return Array.from(this.sessions.values());
144
- }
145
- markDisconnected(sessionId) {
146
- const s = this.sessions.get(sessionId);
147
- if (s) s.isConnected = false;
148
- }
149
- getEventTimeline(filter = {}) {
150
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
151
- const typeSet = filter.eventTypes ? new Set(filter.eventTypes) : null;
152
- return this.buffer.toArray().filter((e) => {
153
- if (e.timestamp < since) return false;
154
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
155
- if (typeSet && !typeSet.has(e.eventType)) return false;
156
- return true;
157
- });
158
- }
159
- getAllEvents(sinceSeconds, sessionId) {
160
- const since = sinceSeconds ? Date.now() - sinceSeconds * 1e3 : 0;
161
- return this.buffer.toArray().filter((e) => {
162
- if (e.timestamp < since) return false;
163
- if (sessionId && e.sessionId !== sessionId) return false;
164
- return true;
165
- });
166
- }
167
- getSessionIdsForProject(appName) {
168
- return Array.from(this.sessions.values()).filter((s) => s.appName === appName).map((s) => s.sessionId);
169
- }
170
- getStateEvents(filter = {}) {
171
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
172
- return this.buffer.query((e) => {
173
- if (e.eventType !== "state") return false;
174
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
175
- const se = e;
176
- if (se.timestamp < since) return false;
177
- if (filter.storeId && se.storeId !== filter.storeId) return false;
178
- return true;
179
- });
180
- }
181
- getRenderEvents(filter = {}) {
182
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
183
- return this.buffer.query((e) => {
184
- if (e.eventType !== "render") return false;
185
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
186
- const re = e;
187
- if (re.timestamp < since) return false;
188
- if (filter.componentName) {
189
- const hasMatch = re.profiles.some(
190
- (p) => p.componentName.toLowerCase().includes(filter.componentName.toLowerCase())
191
- );
192
- if (!hasMatch) return false;
193
- }
194
- return true;
195
- });
196
- }
197
- getPerformanceMetrics(filter = {}) {
198
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
199
- return this.buffer.query((e) => {
200
- if (e.eventType !== "performance") return false;
201
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
202
- const pe = e;
203
- if (pe.timestamp < since) return false;
204
- if (filter.metricName && pe.metricName !== filter.metricName) return false;
205
- return true;
206
- });
207
- }
208
- getDatabaseEvents(filter = {}) {
209
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
210
- return this.buffer.query((e) => {
211
- if (e.eventType !== "database") return false;
212
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
213
- const de = e;
214
- if (de.timestamp < since) return false;
215
- if (filter.table) {
216
- const hasTable = de.tablesAccessed.some(
217
- (t) => t.toLowerCase() === filter.table.toLowerCase()
218
- );
219
- if (!hasTable) return false;
220
- }
221
- if (filter.minDurationMs !== void 0 && de.duration < filter.minDurationMs) return false;
222
- if (filter.search && !de.query.toLowerCase().includes(filter.search.toLowerCase()))
223
- return false;
224
- if (filter.operation && de.operation !== filter.operation) return false;
225
- if (filter.source && de.source !== filter.source) return false;
226
- return true;
227
- });
228
- }
229
- // ============================================================
230
- // Recon event queries — returns the most recent event of each type
231
- // ============================================================
232
- getLatestReconEvent(eventType, filter = {}) {
233
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
234
- const results = this.buffer.query((e) => {
235
- if (e.eventType !== eventType) return false;
236
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
237
- if (e.timestamp < since) return false;
238
- if (filter.url) {
239
- const re = e;
240
- if (re.url && !re.url.includes(filter.url)) return false;
241
- }
242
- return true;
243
- });
244
- return results[0] ?? null;
245
- }
246
- getReconEvents(eventType, filter = {}) {
247
- const since = filter.sinceSeconds ? Date.now() - filter.sinceSeconds * 1e3 : 0;
248
- return this.buffer.query((e) => {
249
- if (e.eventType !== eventType) return false;
250
- if (filter.sessionId && e.sessionId !== filter.sessionId) return false;
251
- if (e.timestamp < since) return false;
252
- if (filter.url) {
253
- const re = e;
254
- if (re.url && !re.url.includes(filter.url)) return false;
255
- }
256
- return true;
257
- });
258
- }
259
- getReconMetadata(filter = {}) {
260
- return this.getLatestReconEvent("recon_metadata", filter);
261
- }
262
- getReconDesignTokens(filter = {}) {
263
- return this.getLatestReconEvent("recon_design_tokens", filter);
264
- }
265
- getReconFonts(filter = {}) {
266
- return this.getLatestReconEvent("recon_fonts", filter);
267
- }
268
- getReconLayoutTree(filter = {}) {
269
- return this.getLatestReconEvent("recon_layout_tree", filter);
270
- }
271
- getReconAccessibility(filter = {}) {
272
- return this.getLatestReconEvent("recon_accessibility", filter);
273
- }
274
- getReconComputedStyles(filter = {}) {
275
- return this.getReconEvents("recon_computed_styles", filter);
276
- }
277
- getReconElementSnapshots(filter = {}) {
278
- return this.getReconEvents("recon_element_snapshot", filter);
279
- }
280
- getReconAssetInventory(filter = {}) {
281
- return this.getLatestReconEvent("recon_asset_inventory", filter);
282
- }
283
- clear() {
284
- const count = this.buffer.count;
285
- this.buffer.clear();
286
- this.sessions.clear();
287
- return { clearedCount: count };
288
- }
289
- };
290
-
291
- // src/sqlite-store.ts
292
- import Database from "better-sqlite3";
293
- var SqliteStore = class {
294
- db;
295
- writeBuffer = [];
296
- flushTimer = null;
297
- batchSize;
298
- insertEventStmt;
299
- insertSessionStmt;
300
- updateSessionDisconnectedStmt;
301
- constructor(options) {
302
- this.db = new Database(options.dbPath);
303
- this.batchSize = options.batchSize ?? 50;
304
- if (options.walMode !== false) {
305
- this.db.pragma("journal_mode = WAL");
306
- }
307
- this.db.pragma("synchronous = NORMAL");
308
- this.createSchema();
309
- this.insertEventStmt = this.db.prepare(`
310
- INSERT INTO events (event_id, session_id, project, event_type, timestamp, data)
311
- VALUES (?, ?, ?, ?, ?, ?)
312
- `);
313
- this.insertSessionStmt = this.db.prepare(`
314
- INSERT OR REPLACE INTO sessions (
315
- session_id, project, app_name, connected_at, sdk_version,
316
- event_count, is_connected, build_meta
317
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
318
- `);
319
- this.updateSessionDisconnectedStmt = this.db.prepare(`
320
- UPDATE sessions SET is_connected = 0, disconnected_at = ? WHERE session_id = ?
321
- `);
322
- const flushInterval = options.flushIntervalMs ?? 100;
323
- this.flushTimer = setInterval(() => this.flush(), flushInterval);
324
- }
325
- createSchema() {
326
- this.db.exec(`
327
- CREATE TABLE IF NOT EXISTS events (
328
- id INTEGER PRIMARY KEY AUTOINCREMENT,
329
- event_id TEXT NOT NULL UNIQUE,
330
- session_id TEXT NOT NULL,
331
- project TEXT NOT NULL,
332
- event_type TEXT NOT NULL,
333
- timestamp INTEGER NOT NULL,
334
- data TEXT NOT NULL
335
- );
336
-
337
- CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id);
338
- CREATE INDEX IF NOT EXISTS idx_events_type ON events(event_type);
339
- CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);
340
- CREATE INDEX IF NOT EXISTS idx_events_type_timestamp ON events(event_type, timestamp);
341
- CREATE INDEX IF NOT EXISTS idx_events_project ON events(project);
342
-
343
- CREATE TABLE IF NOT EXISTS sessions (
344
- session_id TEXT PRIMARY KEY,
345
- project TEXT NOT NULL,
346
- app_name TEXT NOT NULL,
347
- connected_at INTEGER NOT NULL,
348
- disconnected_at INTEGER,
349
- sdk_version TEXT NOT NULL,
350
- event_count INTEGER DEFAULT 0,
351
- is_connected INTEGER DEFAULT 1,
352
- build_meta TEXT
353
- );
354
-
355
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
356
-
357
- CREATE TABLE IF NOT EXISTS session_metrics (
358
- session_id TEXT PRIMARY KEY,
359
- project TEXT NOT NULL,
360
- metrics TEXT NOT NULL,
361
- created_at INTEGER NOT NULL,
362
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
363
- );
364
- `);
365
- }
366
- // --- Write Operations ---
367
- addEvent(event, project) {
368
- this.writeBuffer.push({ event, project });
369
- if (this.writeBuffer.length >= this.batchSize) {
370
- this.flush();
371
- }
372
- }
373
- flush() {
374
- if (this.writeBuffer.length === 0) return;
375
- const batch = this.writeBuffer.splice(0);
376
- const insertMany = this.db.transaction(() => {
377
- for (const { event, project } of batch) {
378
- try {
379
- this.insertEventStmt.run(
380
- event.eventId,
381
- event.sessionId,
382
- project,
383
- event.eventType,
384
- event.timestamp,
385
- JSON.stringify(event)
386
- );
387
- } catch {
388
- }
389
- }
390
- });
391
- try {
392
- insertMany();
393
- } catch (err) {
394
- console.error("[RuntimeScope] SQLite flush error:", err.message);
395
- }
396
- }
397
- saveSession(info) {
398
- this.insertSessionStmt.run(
399
- info.sessionId,
400
- info.project,
401
- info.appName,
402
- info.connectedAt,
403
- info.sdkVersion,
404
- info.eventCount,
405
- info.isConnected ? 1 : 0,
406
- info.buildMeta ? JSON.stringify(info.buildMeta) : null
407
- );
408
- }
409
- updateSessionDisconnected(sessionId, disconnectedAt) {
410
- this.updateSessionDisconnectedStmt.run(disconnectedAt, sessionId);
411
- }
412
- saveSessionMetrics(sessionId, project, metrics) {
413
- this.db.prepare(`
414
- INSERT OR REPLACE INTO session_metrics (session_id, project, metrics, created_at)
415
- VALUES (?, ?, ?, ?)
416
- `).run(sessionId, project, JSON.stringify(metrics), Date.now());
417
- }
418
- // --- Read Operations ---
419
- getEvents(filter) {
420
- const conditions = [];
421
- const params = [];
422
- if (filter.project) {
423
- conditions.push("project = ?");
424
- params.push(filter.project);
425
- }
426
- if (filter.sessionId) {
427
- conditions.push("session_id = ?");
428
- params.push(filter.sessionId);
429
- }
430
- if (filter.eventTypes && filter.eventTypes.length > 0) {
431
- const placeholders = filter.eventTypes.map(() => "?").join(", ");
432
- conditions.push(`event_type IN (${placeholders})`);
433
- params.push(...filter.eventTypes);
434
- }
435
- if (filter.since) {
436
- conditions.push("timestamp >= ?");
437
- params.push(filter.since);
438
- }
439
- if (filter.until) {
440
- conditions.push("timestamp <= ?");
441
- params.push(filter.until);
442
- }
443
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
444
- const limit = filter.limit ? `LIMIT ${filter.limit}` : "LIMIT 1000";
445
- const offset = filter.offset ? `OFFSET ${filter.offset}` : "";
446
- const rows = this.db.prepare(`SELECT data FROM events ${where} ORDER BY timestamp ASC ${limit} ${offset}`).all(...params);
447
- return rows.map((row) => JSON.parse(row.data));
448
- }
449
- getEventCount(filter) {
450
- const conditions = [];
451
- const params = [];
452
- if (filter.project) {
453
- conditions.push("project = ?");
454
- params.push(filter.project);
455
- }
456
- if (filter.sessionId) {
457
- conditions.push("session_id = ?");
458
- params.push(filter.sessionId);
459
- }
460
- if (filter.eventTypes && filter.eventTypes.length > 0) {
461
- const placeholders = filter.eventTypes.map(() => "?").join(", ");
462
- conditions.push(`event_type IN (${placeholders})`);
463
- params.push(...filter.eventTypes);
464
- }
465
- if (filter.since) {
466
- conditions.push("timestamp >= ?");
467
- params.push(filter.since);
468
- }
469
- if (filter.until) {
470
- conditions.push("timestamp <= ?");
471
- params.push(filter.until);
472
- }
473
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
474
- const row = this.db.prepare(`SELECT COUNT(*) as count FROM events ${where}`).get(...params);
475
- return row.count;
476
- }
477
- getSessions(project, limit = 50) {
478
- const rows = this.db.prepare(`
479
- SELECT session_id, project, app_name, connected_at, disconnected_at,
480
- sdk_version, event_count, is_connected, build_meta
481
- FROM sessions
482
- WHERE project = ?
483
- ORDER BY connected_at DESC
484
- LIMIT ?
485
- `).all(project, limit);
486
- return rows.map((row) => ({
487
- sessionId: row.session_id,
488
- project: row.project,
489
- appName: row.app_name,
490
- connectedAt: row.connected_at,
491
- disconnectedAt: row.disconnected_at ?? void 0,
492
- sdkVersion: row.sdk_version,
493
- eventCount: row.event_count,
494
- isConnected: row.is_connected === 1,
495
- buildMeta: row.build_meta ? JSON.parse(row.build_meta) : void 0
496
- }));
497
- }
498
- getSessionMetrics(sessionId) {
499
- const row = this.db.prepare("SELECT metrics FROM session_metrics WHERE session_id = ?").get(sessionId);
500
- return row ? JSON.parse(row.metrics) : null;
501
- }
502
- getEventsByType(project, eventType, sinceMs) {
503
- const conditions = ["project = ?", "event_type = ?"];
504
- const params = [project, eventType];
505
- if (sinceMs) {
506
- conditions.push("timestamp >= ?");
507
- params.push(sinceMs);
508
- }
509
- const where = conditions.join(" AND ");
510
- const rows = this.db.prepare(`SELECT data FROM events WHERE ${where} ORDER BY timestamp ASC LIMIT 1000`).all(...params);
511
- return rows.map((row) => JSON.parse(row.data));
512
- }
513
- // --- Maintenance ---
514
- deleteOldEvents(beforeTimestamp) {
515
- const result = this.db.prepare("DELETE FROM events WHERE timestamp < ?").run(beforeTimestamp);
516
- return result.changes;
517
- }
518
- vacuum() {
519
- this.db.exec("VACUUM");
520
- }
521
- close() {
522
- if (this.flushTimer) {
523
- clearInterval(this.flushTimer);
524
- this.flushTimer = null;
525
- }
526
- this.flush();
527
- this.db.close();
528
- }
529
- };
530
-
531
- // src/rate-limiter.ts
532
- var SessionRateLimiter = class {
533
- windows = /* @__PURE__ */ new Map();
534
- maxPerSecond;
535
- maxPerMinute;
536
- _droppedTotal = 0;
537
- constructor(config = {}) {
538
- this.maxPerSecond = config.maxEventsPerSecond ?? Infinity;
539
- this.maxPerMinute = config.maxEventsPerMinute ?? Infinity;
540
- }
541
- get droppedTotal() {
542
- return this._droppedTotal;
543
- }
544
- isEnabled() {
545
- return this.maxPerSecond !== Infinity || this.maxPerMinute !== Infinity;
546
- }
547
- /** Returns true if the event should be accepted, false if rate-limited. */
548
- allow(sessionId) {
549
- if (!this.isEnabled()) return true;
550
- const now = Date.now();
551
- let w = this.windows.get(sessionId);
552
- if (!w) {
553
- w = {
554
- secondCount: 0,
555
- secondStart: now,
556
- minuteCount: 0,
557
- minuteStart: now,
558
- lastWarning: 0
559
- };
560
- this.windows.set(sessionId, w);
561
- }
562
- if (now - w.secondStart >= 1e3) {
563
- w.secondCount = 0;
564
- w.secondStart = now;
565
- }
566
- if (now - w.minuteStart >= 6e4) {
567
- w.minuteCount = 0;
568
- w.minuteStart = now;
569
- }
570
- if (w.secondCount >= this.maxPerSecond) {
571
- this._droppedTotal++;
572
- this.maybeWarn(sessionId, w, now);
573
- return false;
574
- }
575
- if (w.minuteCount >= this.maxPerMinute) {
576
- this._droppedTotal++;
577
- this.maybeWarn(sessionId, w, now);
578
- return false;
579
- }
580
- w.secondCount++;
581
- w.minuteCount++;
582
- return true;
583
- }
584
- /** Allow a batch of N events. Returns the number accepted. */
585
- allowBatch(sessionId, count) {
586
- let accepted = 0;
587
- for (let i = 0; i < count; i++) {
588
- if (this.allow(sessionId)) accepted++;
589
- else break;
590
- }
591
- return accepted;
592
- }
593
- /** Remove tracking for sessions that haven't been seen in maxAgeMs. */
594
- prune(maxAgeMs = 3e5) {
595
- const cutoff = Date.now() - maxAgeMs;
596
- for (const [id, w] of this.windows) {
597
- if (w.minuteStart < cutoff) {
598
- this.windows.delete(id);
599
- }
600
- }
601
- }
602
- maybeWarn(sessionId, w, now) {
603
- if (now - w.lastWarning >= 6e4) {
604
- w.lastWarning = now;
605
- console.error(
606
- `[RuntimeScope] Rate limiting session ${sessionId.slice(0, 8)}... (dropped ${this._droppedTotal} total)`
607
- );
608
- }
609
- }
610
- };
611
-
612
- // src/tls.ts
613
- import { readFileSync } from "fs";
614
- function loadTlsOptions(config) {
615
- return {
616
- cert: readFileSync(config.certPath, "utf-8"),
617
- key: readFileSync(config.keyPath, "utf-8"),
618
- ...config.caPath ? { ca: readFileSync(config.caPath, "utf-8") } : {}
619
- };
620
- }
621
- function resolveTlsConfig() {
622
- const certPath = process.env.RUNTIMESCOPE_TLS_CERT;
623
- const keyPath = process.env.RUNTIMESCOPE_TLS_KEY;
624
- if (!certPath || !keyPath) return null;
625
- return {
626
- certPath,
627
- keyPath,
628
- caPath: process.env.RUNTIMESCOPE_TLS_CA
629
- };
630
- }
631
-
632
- // src/server.ts
633
- var CollectorServer = class {
634
- wss = null;
635
- store;
636
- projectManager;
637
- authManager = null;
638
- rateLimiter;
639
- clients = /* @__PURE__ */ new Map();
640
- pendingHandshakes = /* @__PURE__ */ new Set();
641
- pendingCommands = /* @__PURE__ */ new Map();
642
- sqliteStores = /* @__PURE__ */ new Map();
643
- disconnectCallbacks = [];
644
- pruneTimer = null;
645
- tlsConfig = null;
646
- constructor(options = {}) {
647
- this.store = new EventStore(options.bufferSize ?? 1e4);
648
- this.projectManager = options.projectManager ?? null;
649
- this.authManager = options.authManager ?? null;
650
- this.rateLimiter = new SessionRateLimiter(options.rateLimits ?? {});
651
- this.tlsConfig = options.tls ?? null;
652
- if (this.projectManager) {
653
- this.projectManager.ensureGlobalDir();
654
- }
655
- if (this.rateLimiter.isEnabled()) {
656
- this.pruneTimer = setInterval(() => this.rateLimiter.prune(), 6e4);
657
- }
658
- }
659
- getStore() {
660
- return this.store;
661
- }
662
- getPort() {
663
- const addr = this.wss?.address();
664
- return addr && typeof addr === "object" ? addr.port : null;
665
- }
666
- getClientCount() {
667
- return this.clients.size;
668
- }
669
- getProjectManager() {
670
- return this.projectManager;
671
- }
672
- getSqliteStore(projectName) {
673
- return this.sqliteStores.get(projectName);
674
- }
675
- getSqliteStores() {
676
- return this.sqliteStores;
677
- }
678
- getRateLimiter() {
679
- return this.rateLimiter;
680
- }
681
- onDisconnect(cb) {
682
- this.disconnectCallbacks.push(cb);
683
- }
684
- start(options = {}) {
685
- const port = options.port ?? 9090;
686
- const host = options.host ?? "127.0.0.1";
687
- const maxRetries = options.maxRetries ?? 5;
688
- const retryDelayMs = options.retryDelayMs ?? 1e3;
689
- const tls = options.tls ?? this.tlsConfig;
690
- return this.tryStart(port, host, maxRetries, retryDelayMs, tls);
691
- }
692
- tryStart(port, host, retriesLeft, retryDelayMs, tls) {
693
- return new Promise((resolve2, reject) => {
694
- let wss;
695
- if (tls) {
696
- const httpsServer = createHttpsServer(loadTlsOptions(tls));
697
- wss = new WebSocketServer({ server: httpsServer });
698
- httpsServer.on("listening", () => {
699
- this.wss = wss;
700
- this.setupConnectionHandler(wss);
701
- console.error(`[RuntimeScope] Collector listening on wss://${host}:${port}`);
702
- resolve2();
703
- });
704
- httpsServer.on("error", (err) => {
705
- httpsServer.close();
706
- this.handleStartError(err, port, host, retriesLeft, retryDelayMs, tls, resolve2, reject);
707
- });
708
- httpsServer.listen(port, host);
709
- } else {
710
- wss = new WebSocketServer({ port, host });
711
- wss.on("listening", () => {
712
- this.wss = wss;
713
- this.setupConnectionHandler(wss);
714
- console.error(`[RuntimeScope] Collector listening on ws://${host}:${port}`);
715
- resolve2();
716
- });
717
- wss.on("error", (err) => {
718
- wss.close();
719
- this.handleStartError(err, port, host, retriesLeft, retryDelayMs, tls, resolve2, reject);
720
- });
721
- }
722
- });
723
- }
724
- handleStartError(err, port, host, retriesLeft, retryDelayMs, tls, resolve2, reject) {
725
- if (err.code === "EADDRINUSE" && retriesLeft > 0) {
726
- console.error(
727
- `[RuntimeScope] Port ${port} in use, retrying in ${retryDelayMs}ms (${retriesLeft} attempts left)...`
728
- );
729
- setTimeout(() => {
730
- this.tryStart(port, host, retriesLeft - 1, retryDelayMs, tls).then(resolve2).catch(reject);
731
- }, retryDelayMs);
732
- } else {
733
- console.error("[RuntimeScope] WebSocket server error:", err.message);
734
- reject(err);
735
- }
736
- }
737
- ensureSqliteStore(projectName) {
738
- if (!this.projectManager) return null;
739
- let sqliteStore = this.sqliteStores.get(projectName);
740
- if (!sqliteStore) {
741
- try {
742
- this.projectManager.ensureProjectDir(projectName);
743
- const dbPath = this.projectManager.getProjectDbPath(projectName);
744
- sqliteStore = new SqliteStore({ dbPath });
745
- this.sqliteStores.set(projectName, sqliteStore);
746
- this.store.setSqliteStore(sqliteStore, projectName);
747
- console.error(`[RuntimeScope] SQLite store opened for project "${projectName}"`);
748
- } catch (err) {
749
- console.error(
750
- `[RuntimeScope] Failed to open SQLite for "${projectName}":`,
751
- err.message
752
- );
753
- return null;
754
- }
755
- }
756
- return sqliteStore;
757
- }
758
- setupConnectionHandler(wss) {
759
- wss.on("connection", (ws) => {
760
- if (this.authManager?.isEnabled()) {
761
- this.pendingHandshakes.add(ws);
762
- const authTimeout = setTimeout(() => {
763
- if (this.pendingHandshakes.has(ws)) {
764
- this.pendingHandshakes.delete(ws);
765
- try {
766
- ws.send(JSON.stringify({
767
- type: "error",
768
- payload: { code: "AUTH_TIMEOUT", message: "Handshake timeout" },
769
- timestamp: Date.now()
770
- }));
771
- } catch {
772
- }
773
- ws.close(4001, "Authentication timeout");
774
- }
775
- }, 5e3);
776
- ws.on("close", () => {
777
- clearTimeout(authTimeout);
778
- this.pendingHandshakes.delete(ws);
779
- });
780
- }
781
- ws.on("message", (data) => {
782
- try {
783
- const msg = JSON.parse(data.toString());
784
- this.handleMessage(ws, msg);
785
- } catch {
786
- console.error("[RuntimeScope] Malformed WebSocket message, ignoring");
787
- }
788
- });
789
- ws.on("close", () => {
790
- const clientInfo = this.clients.get(ws);
791
- if (clientInfo) {
792
- this.store.markDisconnected(clientInfo.sessionId);
793
- const sqliteStore = this.sqliteStores.get(clientInfo.projectName);
794
- if (sqliteStore) {
795
- sqliteStore.updateSessionDisconnected(clientInfo.sessionId, Date.now());
796
- }
797
- console.error(`[RuntimeScope] Session ${clientInfo.sessionId} disconnected`);
798
- for (const cb of this.disconnectCallbacks) {
799
- try {
800
- cb(clientInfo.sessionId, clientInfo.projectName);
801
- } catch {
802
- }
803
- }
804
- }
805
- this.clients.delete(ws);
806
- });
807
- ws.on("error", (err) => {
808
- console.error("[RuntimeScope] WebSocket client error:", err.message);
809
- });
810
- });
811
- }
812
- handleMessage(ws, msg) {
813
- switch (msg.type) {
814
- case "handshake": {
815
- const payload = msg.payload;
816
- if (this.authManager?.isEnabled()) {
817
- if (!this.authManager.isAuthorized(payload.authToken)) {
818
- try {
819
- ws.send(JSON.stringify({
820
- type: "error",
821
- payload: { code: "AUTH_FAILED", message: "Invalid or missing API key" },
822
- timestamp: Date.now()
823
- }));
824
- } catch {
825
- }
826
- ws.close(4001, "Authentication failed");
827
- return;
828
- }
829
- this.pendingHandshakes.delete(ws);
830
- }
831
- const projectName = payload.appName;
832
- this.clients.set(ws, {
833
- sessionId: payload.sessionId,
834
- projectName
835
- });
836
- const sqliteStore = this.ensureSqliteStore(projectName);
837
- if (sqliteStore) {
838
- const sessionInfo = {
839
- sessionId: payload.sessionId,
840
- project: projectName,
841
- appName: payload.appName,
842
- connectedAt: msg.timestamp,
843
- sdkVersion: payload.sdkVersion,
844
- eventCount: 0,
845
- isConnected: true
846
- };
847
- sqliteStore.saveSession(sessionInfo);
848
- }
849
- console.error(
850
- `[RuntimeScope] Session ${payload.sessionId} connected (${payload.appName} v${payload.sdkVersion})`
851
- );
852
- break;
853
- }
854
- case "event": {
855
- if (this.pendingHandshakes.has(ws)) return;
856
- const clientInfo = this.clients.get(ws);
857
- const payload = msg.payload;
858
- if (Array.isArray(payload.events)) {
859
- for (const event of payload.events) {
860
- if (clientInfo && !this.rateLimiter.allow(clientInfo.sessionId)) {
861
- break;
862
- }
863
- this.store.addEvent(event);
864
- }
865
- }
866
- break;
867
- }
868
- case "command_response": {
869
- const resp = msg;
870
- const pending = this.pendingCommands.get(resp.requestId);
871
- if (pending) {
872
- clearTimeout(pending.timer);
873
- this.pendingCommands.delete(resp.requestId);
874
- pending.resolve(resp.payload);
875
- }
876
- break;
877
- }
878
- case "heartbeat":
879
- break;
880
- }
881
- }
882
- /** Find the WebSocket for a given sessionId */
883
- findWsBySessionId(sessionId) {
884
- for (const [ws, info] of this.clients) {
885
- if (info.sessionId === sessionId) return ws;
886
- }
887
- return void 0;
888
- }
889
- /** Get the first connected session ID (for single-app use) */
890
- getFirstSessionId() {
891
- for (const [, info] of this.clients) {
892
- return info.sessionId;
893
- }
894
- return void 0;
895
- }
896
- /** Get the project name for a session */
897
- getProjectForSession(sessionId) {
898
- for (const [, info] of this.clients) {
899
- if (info.sessionId === sessionId) return info.projectName;
900
- }
901
- return void 0;
902
- }
903
- /** Send a command to the SDK and await the response */
904
- sendCommand(sessionId, command, timeoutMs = 1e4) {
905
- return new Promise((resolve2, reject) => {
906
- const ws = this.findWsBySessionId(sessionId);
907
- if (!ws || ws.readyState !== 1) {
908
- reject(new Error(`No active WebSocket for session ${sessionId}`));
909
- return;
910
- }
911
- const timer = setTimeout(() => {
912
- this.pendingCommands.delete(command.requestId);
913
- reject(new Error(`Command ${command.command} timed out after ${timeoutMs}ms`));
914
- }, timeoutMs);
915
- this.pendingCommands.set(command.requestId, { resolve: resolve2, reject, timer });
916
- try {
917
- ws.send(JSON.stringify({
918
- type: "command",
919
- payload: command,
920
- timestamp: Date.now(),
921
- sessionId
922
- }));
923
- } catch (err) {
924
- clearTimeout(timer);
925
- this.pendingCommands.delete(command.requestId);
926
- reject(err);
927
- }
928
- });
929
- }
930
- stop() {
931
- if (this.pruneTimer) {
932
- clearInterval(this.pruneTimer);
933
- this.pruneTimer = null;
934
- }
935
- for (const [name, sqliteStore] of this.sqliteStores) {
936
- try {
937
- sqliteStore.close();
938
- console.error(`[RuntimeScope] SQLite store closed for "${name}"`);
939
- } catch {
940
- }
941
- }
942
- this.sqliteStores.clear();
943
- if (this.wss) {
944
- this.wss.close();
945
- this.wss = null;
946
- console.error("[RuntimeScope] Collector stopped");
947
- }
948
- }
949
- };
1
+ import {
2
+ AuthManager,
3
+ BUILT_IN_RULES,
4
+ CollectorServer,
5
+ EventStore,
6
+ HttpServer,
7
+ PmStore,
8
+ ProjectDiscovery,
9
+ ProjectManager,
10
+ Redactor,
11
+ RingBuffer,
12
+ SessionManager,
13
+ SessionRateLimiter,
14
+ SqliteStore,
15
+ __require,
16
+ calculateActiveMinutes,
17
+ calculateCostMicrodollars,
18
+ generateApiKey,
19
+ loadTlsOptions,
20
+ parseSessionJsonl,
21
+ resolveTlsConfig
22
+ } from "./chunk-6JZXAFPC.js";
950
23
 
951
24
  // src/issue-detector.ts
952
25
  function detectIssues(events) {
@@ -1249,326 +322,6 @@ function detectPoorWebVitals(events) {
1249
322
  return issues;
1250
323
  }
1251
324
 
1252
- // src/project-manager.ts
1253
- import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync, readdirSync } from "fs";
1254
- import { join } from "path";
1255
- import { homedir } from "os";
1256
- var DEFAULT_GLOBAL_CONFIG = {
1257
- defaultPort: 9090,
1258
- bufferSize: 1e4,
1259
- httpPort: 9091
1260
- };
1261
- var ProjectManager = class {
1262
- baseDir;
1263
- constructor(baseDir) {
1264
- this.baseDir = baseDir ?? join(homedir(), ".runtimescope");
1265
- }
1266
- get rootDir() {
1267
- return this.baseDir;
1268
- }
1269
- // --- Directory helpers ---
1270
- getProjectDir(projectName) {
1271
- return join(this.baseDir, "projects", projectName);
1272
- }
1273
- getProjectDbPath(projectName) {
1274
- return join(this.getProjectDir(projectName), "events.db");
1275
- }
1276
- getSessionsDir(projectName) {
1277
- return join(this.getProjectDir(projectName), "sessions");
1278
- }
1279
- getSessionSnapshotPath(projectName, sessionId, timestamp) {
1280
- const date = new Date(timestamp);
1281
- const dateStr = date.toISOString().replace(/[:.]/g, "-").slice(0, 19);
1282
- const shortId = sessionId.slice(0, 8);
1283
- return join(this.getSessionsDir(projectName), `${dateStr}_${shortId}.db`);
1284
- }
1285
- // --- Lifecycle (idempotent) ---
1286
- ensureGlobalDir() {
1287
- this.mkdirp(this.baseDir);
1288
- this.mkdirp(join(this.baseDir, "projects"));
1289
- const configPath = join(this.baseDir, "config.json");
1290
- if (!existsSync(configPath)) {
1291
- this.writeJson(configPath, DEFAULT_GLOBAL_CONFIG);
1292
- }
1293
- }
1294
- ensureProjectDir(projectName) {
1295
- const projectDir = this.getProjectDir(projectName);
1296
- this.mkdirp(projectDir);
1297
- this.mkdirp(this.getSessionsDir(projectName));
1298
- const configPath = join(projectDir, "config.json");
1299
- if (!existsSync(configPath)) {
1300
- const config = {
1301
- name: projectName,
1302
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1303
- settings: {
1304
- retentionDays: 30
1305
- }
1306
- };
1307
- this.writeJson(configPath, config);
1308
- }
1309
- }
1310
- // --- Config ---
1311
- getGlobalConfig() {
1312
- const configPath = join(this.baseDir, "config.json");
1313
- if (!existsSync(configPath)) return { ...DEFAULT_GLOBAL_CONFIG };
1314
- return { ...DEFAULT_GLOBAL_CONFIG, ...this.readJson(configPath) };
1315
- }
1316
- saveGlobalConfig(config) {
1317
- this.writeJson(join(this.baseDir, "config.json"), config);
1318
- }
1319
- getProjectConfig(projectName) {
1320
- const configPath = join(this.getProjectDir(projectName), "config.json");
1321
- if (!existsSync(configPath)) return null;
1322
- return this.readJson(configPath);
1323
- }
1324
- saveProjectConfig(projectName, config) {
1325
- this.writeJson(join(this.getProjectDir(projectName), "config.json"), config);
1326
- }
1327
- getInfrastructureConfig(projectName) {
1328
- const jsonPath = join(this.getProjectDir(projectName), "infrastructure.json");
1329
- if (existsSync(jsonPath)) {
1330
- const config = this.readJson(jsonPath);
1331
- return this.resolveConfigEnvVars(config);
1332
- }
1333
- const yamlPath = join(this.getProjectDir(projectName), "infrastructure.yaml");
1334
- if (existsSync(yamlPath)) {
1335
- try {
1336
- const content = readFileSync2(yamlPath, "utf-8");
1337
- return this.resolveConfigEnvVars(this.parseSimpleYaml(content));
1338
- } catch {
1339
- return null;
1340
- }
1341
- }
1342
- return null;
1343
- }
1344
- getClaudeInstructions(projectName) {
1345
- const filePath = join(this.getProjectDir(projectName), "claude-instructions.md");
1346
- if (!existsSync(filePath)) return null;
1347
- return readFileSync2(filePath, "utf-8");
1348
- }
1349
- // --- Discovery ---
1350
- listProjects() {
1351
- const projectsDir = join(this.baseDir, "projects");
1352
- if (!existsSync(projectsDir)) return [];
1353
- return readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
1354
- }
1355
- projectExists(projectName) {
1356
- return existsSync(this.getProjectDir(projectName));
1357
- }
1358
- // --- Environment variable resolution ---
1359
- resolveEnvVars(value) {
1360
- return value.replace(/\$\{([^}]+)\}/g, (_match, varName) => {
1361
- return process.env[varName] ?? "";
1362
- });
1363
- }
1364
- // --- Private helpers ---
1365
- mkdirp(dir) {
1366
- if (!existsSync(dir)) {
1367
- mkdirSync(dir, { recursive: true });
1368
- }
1369
- }
1370
- readJson(path) {
1371
- const content = readFileSync2(path, "utf-8");
1372
- return JSON.parse(content);
1373
- }
1374
- writeJson(path, data) {
1375
- writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
1376
- }
1377
- resolveConfigEnvVars(config) {
1378
- const resolve2 = (obj) => {
1379
- if (typeof obj === "string") return this.resolveEnvVars(obj);
1380
- if (Array.isArray(obj)) return obj.map(resolve2);
1381
- if (obj && typeof obj === "object") {
1382
- const result = {};
1383
- for (const [key, value] of Object.entries(obj)) {
1384
- result[key] = resolve2(value);
1385
- }
1386
- return result;
1387
- }
1388
- return obj;
1389
- };
1390
- return resolve2(config);
1391
- }
1392
- /**
1393
- * Minimal YAML parser for simple infrastructure config files.
1394
- * Handles flat key-value pairs and one level of nesting.
1395
- * For full YAML support, install js-yaml.
1396
- */
1397
- parseSimpleYaml(content) {
1398
- try {
1399
- const yaml = __require("js-yaml");
1400
- return yaml.load(content);
1401
- } catch {
1402
- try {
1403
- return JSON.parse(content);
1404
- } catch {
1405
- return {};
1406
- }
1407
- }
1408
- }
1409
- };
1410
-
1411
- // src/auth.ts
1412
- import { randomBytes, timingSafeEqual } from "crypto";
1413
- var AuthManager = class {
1414
- keys = /* @__PURE__ */ new Map();
1415
- enabled;
1416
- constructor(config = {}) {
1417
- this.enabled = config.enabled ?? false;
1418
- for (const entry of config.apiKeys ?? []) {
1419
- this.keys.set(entry.key, entry);
1420
- }
1421
- }
1422
- isEnabled() {
1423
- return this.enabled;
1424
- }
1425
- /** Validate an API key. Returns the entry if valid, null if invalid. */
1426
- validate(key) {
1427
- if (!this.enabled) return null;
1428
- if (!key) return null;
1429
- for (const [storedKey, entry] of this.keys) {
1430
- if (this.safeCompare(key, storedKey)) {
1431
- return entry;
1432
- }
1433
- }
1434
- return null;
1435
- }
1436
- /** Check if request is authorized. Returns true if auth is disabled or key is valid. */
1437
- isAuthorized(key) {
1438
- if (!this.enabled) return true;
1439
- return this.validate(key) !== null;
1440
- }
1441
- /** Extract bearer token from Authorization header value. */
1442
- static extractBearer(header) {
1443
- if (!header) return void 0;
1444
- const match = header.match(/^Bearer\s+(\S+)$/i);
1445
- return match?.[1];
1446
- }
1447
- /** Constant-time string comparison to prevent timing attacks. */
1448
- safeCompare(a, b) {
1449
- if (a.length !== b.length) return false;
1450
- try {
1451
- return timingSafeEqual(Buffer.from(a, "utf-8"), Buffer.from(b, "utf-8"));
1452
- } catch {
1453
- return false;
1454
- }
1455
- }
1456
- };
1457
- function generateApiKey(label, project) {
1458
- return {
1459
- key: randomBytes(32).toString("hex"),
1460
- label,
1461
- project,
1462
- createdAt: Date.now()
1463
- };
1464
- }
1465
-
1466
- // src/redactor.ts
1467
- var BUILT_IN_RULES = [
1468
- {
1469
- name: "jwt",
1470
- pattern: /eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+/g,
1471
- replacement: "[REDACTED:jwt]"
1472
- },
1473
- {
1474
- name: "credit_card",
1475
- pattern: /\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
1476
- replacement: "[REDACTED:cc]"
1477
- },
1478
- {
1479
- name: "ssn",
1480
- pattern: /\b\d{3}-\d{2}-\d{4}\b/g,
1481
- replacement: "[REDACTED:ssn]"
1482
- },
1483
- {
1484
- name: "email",
1485
- pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z]{2,}\b/gi,
1486
- replacement: "[REDACTED:email]"
1487
- },
1488
- {
1489
- name: "bearer_token",
1490
- pattern: /Bearer\s+[A-Za-z0-9._~+/=-]+/gi,
1491
- replacement: "Bearer [REDACTED]"
1492
- },
1493
- {
1494
- name: "api_key_param",
1495
- pattern: /(?:api[_-]?key|apikey|secret|token|password|passwd|authorization)=[^&\s"']+/gi,
1496
- replacement: "[REDACTED:param]"
1497
- }
1498
- ];
1499
- var DEFAULT_SENSITIVE_KEYS = [
1500
- "password",
1501
- "passwd",
1502
- "secret",
1503
- "token",
1504
- "accessToken",
1505
- "refreshToken",
1506
- "apiKey",
1507
- "api_key",
1508
- "authorization",
1509
- "credit_card",
1510
- "creditCard",
1511
- "ssn",
1512
- "socialSecurity"
1513
- ];
1514
- var Redactor = class {
1515
- rules;
1516
- sensitiveKeyPattern;
1517
- enabled;
1518
- constructor(config = {}) {
1519
- this.enabled = config.enabled ?? true;
1520
- this.rules = [];
1521
- if (config.useBuiltIn !== false) {
1522
- this.rules.push(...BUILT_IN_RULES);
1523
- }
1524
- if (config.rules) {
1525
- this.rules.push(...config.rules);
1526
- }
1527
- const keys = config.sensitiveKeys ?? DEFAULT_SENSITIVE_KEYS;
1528
- this.sensitiveKeyPattern = keys.length > 0 ? new RegExp(`^(${keys.join("|")})$`, "i") : null;
1529
- }
1530
- isEnabled() {
1531
- return this.enabled;
1532
- }
1533
- /** Apply all redaction rules to a string value. */
1534
- redactString(value) {
1535
- let result = value;
1536
- for (const rule of this.rules) {
1537
- rule.pattern.lastIndex = 0;
1538
- result = result.replace(rule.pattern, rule.replacement);
1539
- }
1540
- return result;
1541
- }
1542
- /**
1543
- * Deep-walk an event and redact all string fields.
1544
- * Returns a new event object (does not mutate the original).
1545
- */
1546
- redactEvent(event) {
1547
- if (!this.enabled) return event;
1548
- return this.deepRedact(event);
1549
- }
1550
- deepRedact(value, key) {
1551
- if (value === null || value === void 0) return value;
1552
- if (key && this.sensitiveKeyPattern?.test(key)) {
1553
- return "[REDACTED]";
1554
- }
1555
- if (typeof value === "string") {
1556
- return this.redactString(value);
1557
- }
1558
- if (Array.isArray(value)) {
1559
- return value.map((item) => this.deepRedact(item));
1560
- }
1561
- if (typeof value === "object") {
1562
- const result = {};
1563
- for (const [k, v] of Object.entries(value)) {
1564
- result[k] = this.deepRedact(v, k);
1565
- }
1566
- return result;
1567
- }
1568
- return value;
1569
- }
1570
- };
1571
-
1572
325
  // src/engines/api-discovery.ts
1573
326
  var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1574
327
  var NUMERIC_RE = /^\d+$/;
@@ -3228,126 +1981,6 @@ var InfraConnector = class {
3228
1981
  }
3229
1982
  };
3230
1983
 
3231
- // src/session-manager.ts
3232
- var SessionManager = class {
3233
- projectManager;
3234
- sqliteStores;
3235
- store;
3236
- constructor(projectManager, sqliteStores, store) {
3237
- this.projectManager = projectManager;
3238
- this.sqliteStores = sqliteStores;
3239
- this.store = store;
3240
- }
3241
- computeMetrics(sessionId, project, events) {
3242
- const sessionEvents = events.filter((e) => e.sessionId === sessionId);
3243
- const networkEvents = sessionEvents.filter((e) => e.eventType === "network");
3244
- const renderEvents = sessionEvents.filter((e) => e.eventType === "render");
3245
- const stateEvents = sessionEvents.filter((e) => e.eventType === "state");
3246
- const performanceEvents = sessionEvents.filter((e) => e.eventType === "performance");
3247
- const databaseEvents = sessionEvents.filter((e) => e.eventType === "database");
3248
- const consoleEvents = sessionEvents.filter((e) => e.eventType === "console");
3249
- const endpoints = {};
3250
- const endpointGroups = /* @__PURE__ */ new Map();
3251
- for (const e of networkEvents) {
3252
- const key = `${e.method} ${e.url}`;
3253
- const group = endpointGroups.get(key) ?? [];
3254
- group.push(e);
3255
- endpointGroups.set(key, group);
3256
- }
3257
- for (const [key, group] of endpointGroups) {
3258
- const avgLatency = group.reduce((s, e) => s + e.duration, 0) / group.length;
3259
- const errorCount2 = group.filter((e) => e.status >= 400).length;
3260
- endpoints[key] = { avgLatency, errorRate: errorCount2 / group.length, callCount: group.length };
3261
- }
3262
- const components = {};
3263
- for (const re of renderEvents) {
3264
- for (const p of re.profiles) {
3265
- const existing = components[p.componentName];
3266
- if (existing) {
3267
- existing.renderCount += p.renderCount;
3268
- existing.avgDuration = (existing.avgDuration + p.avgDuration) / 2;
3269
- } else {
3270
- components[p.componentName] = { renderCount: p.renderCount, avgDuration: p.avgDuration };
3271
- }
3272
- }
3273
- }
3274
- const stores = {};
3275
- for (const se of stateEvents) {
3276
- const existing = stores[se.storeId];
3277
- if (existing) {
3278
- existing.updateCount++;
3279
- } else {
3280
- stores[se.storeId] = { updateCount: 1 };
3281
- }
3282
- }
3283
- const webVitals = {};
3284
- for (const pe of performanceEvents) {
3285
- if (pe.rating) {
3286
- webVitals[pe.metricName] = { value: pe.value, rating: pe.rating };
3287
- }
3288
- }
3289
- const queries = {};
3290
- const queryGroups = /* @__PURE__ */ new Map();
3291
- for (const de of databaseEvents) {
3292
- const group = queryGroups.get(de.normalizedQuery) ?? [];
3293
- group.push(de);
3294
- queryGroups.set(de.normalizedQuery, group);
3295
- }
3296
- for (const [key, group] of queryGroups) {
3297
- const avgDuration = group.reduce((s, e) => s + e.duration, 0) / group.length;
3298
- queries[key] = { avgDuration, callCount: group.length };
3299
- }
3300
- const timestamps = sessionEvents.map((e) => e.timestamp);
3301
- const errorCount = consoleEvents.filter((e) => e.level === "error").length + networkEvents.filter((e) => e.status >= 400).length;
3302
- return {
3303
- sessionId,
3304
- project,
3305
- connectedAt: timestamps.length > 0 ? Math.min(...timestamps) : Date.now(),
3306
- disconnectedAt: timestamps.length > 0 ? Math.max(...timestamps) : Date.now(),
3307
- totalEvents: sessionEvents.length,
3308
- errorCount,
3309
- endpoints,
3310
- components,
3311
- stores,
3312
- webVitals,
3313
- queries
3314
- };
3315
- }
3316
- createSnapshot(sessionId, project) {
3317
- const events = this.store.getAllEvents();
3318
- const metrics = this.computeMetrics(sessionId, project, events);
3319
- const sqliteStore = this.sqliteStores.get(project);
3320
- if (sqliteStore) {
3321
- sqliteStore.saveSessionMetrics(sessionId, project, metrics);
3322
- }
3323
- return {
3324
- sessionId,
3325
- project,
3326
- metrics,
3327
- createdAt: Date.now()
3328
- };
3329
- }
3330
- getSessionHistory(project, limit = 20) {
3331
- const sqliteStore = this.sqliteStores.get(project);
3332
- if (!sqliteStore) return [];
3333
- const sessions = sqliteStore.getSessions(project, limit);
3334
- const snapshots = [];
3335
- for (const session of sessions) {
3336
- const metricsData = sqliteStore.getSessionMetrics(session.sessionId);
3337
- if (metricsData) {
3338
- snapshots.push({
3339
- sessionId: session.sessionId,
3340
- project,
3341
- metrics: metricsData,
3342
- buildMeta: session.buildMeta,
3343
- createdAt: session.disconnectedAt ?? session.connectedAt
3344
- });
3345
- }
3346
- }
3347
- return snapshots;
3348
- }
3349
- };
3350
-
3351
1984
  // src/session-differ.ts
3352
1985
  var CHANGE_THRESHOLD = 0.1;
3353
1986
  function classifyDelta(percentChange) {
@@ -3429,352 +2062,6 @@ function compareSessions(metricsA, metricsB) {
3429
2062
  }
3430
2063
  };
3431
2064
  }
3432
-
3433
- // src/http-server.ts
3434
- import { createServer } from "http";
3435
- import { createServer as createHttpsServer2 } from "https";
3436
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
3437
- import { resolve, dirname } from "path";
3438
- import { fileURLToPath } from "url";
3439
- import { WebSocketServer as WebSocketServer2 } from "ws";
3440
- var HttpServer = class {
3441
- server = null;
3442
- wss = null;
3443
- store;
3444
- processMonitor;
3445
- authManager;
3446
- allowedOrigins;
3447
- dashboardClients = /* @__PURE__ */ new Set();
3448
- eventListener = null;
3449
- routes = /* @__PURE__ */ new Map();
3450
- sdkBundlePath = null;
3451
- activePort = 9091;
3452
- startedAt = Date.now();
3453
- constructor(store, processMonitor, options) {
3454
- this.store = store;
3455
- this.processMonitor = processMonitor ?? null;
3456
- this.authManager = options?.authManager ?? null;
3457
- this.allowedOrigins = options?.allowedOrigins ?? null;
3458
- this.registerRoutes();
3459
- }
3460
- registerRoutes() {
3461
- this.routes.set("GET /api/health", (_req, res) => {
3462
- this.json(res, {
3463
- status: "ok",
3464
- timestamp: Date.now(),
3465
- uptime: Math.floor((Date.now() - this.startedAt) / 1e3),
3466
- sessions: this.store.getSessionInfo().filter((s) => s.isConnected).length,
3467
- authEnabled: this.authManager?.isEnabled() ?? false
3468
- });
3469
- });
3470
- this.routes.set("GET /api/sessions", (_req, res) => {
3471
- const sessions = this.store.getSessionInfo();
3472
- this.json(res, { data: sessions, count: sessions.length });
3473
- });
3474
- this.routes.set("GET /api/projects", (_req, res) => {
3475
- const sessions = this.store.getSessionInfo();
3476
- const projectMap = /* @__PURE__ */ new Map();
3477
- for (const s of sessions) {
3478
- const existing = projectMap.get(s.appName);
3479
- if (existing) {
3480
- existing.sessions.push(s.sessionId);
3481
- existing.eventCount += s.eventCount;
3482
- if (s.isConnected) existing.isConnected = true;
3483
- } else {
3484
- projectMap.set(s.appName, {
3485
- appName: s.appName,
3486
- sessions: [s.sessionId],
3487
- isConnected: s.isConnected,
3488
- eventCount: s.eventCount
3489
- });
3490
- }
3491
- }
3492
- const projects = Array.from(projectMap.values());
3493
- this.json(res, { data: projects, count: projects.length });
3494
- });
3495
- this.routes.set("GET /api/processes", (_req, res, params) => {
3496
- if (!this.processMonitor) {
3497
- this.json(res, { data: [], count: 0 });
3498
- return;
3499
- }
3500
- const type = params.get("type") ?? void 0;
3501
- const project = params.get("project") ?? void 0;
3502
- const processes = this.processMonitor.getProcesses({ type, project });
3503
- this.json(res, { data: processes, count: processes.length });
3504
- });
3505
- this.routes.set("GET /api/ports", (_req, res, params) => {
3506
- if (!this.processMonitor) {
3507
- this.json(res, { data: [], count: 0 });
3508
- return;
3509
- }
3510
- const port = numParam(params, "port");
3511
- const ports = this.processMonitor.getPortUsage(port);
3512
- this.json(res, { data: ports, count: ports.length });
3513
- });
3514
- this.routes.set("GET /api/events/network", (_req, res, params) => {
3515
- const events = this.store.getNetworkRequests({
3516
- sinceSeconds: numParam(params, "since_seconds"),
3517
- urlPattern: params.get("url_pattern") ?? void 0,
3518
- method: params.get("method") ?? void 0,
3519
- sessionId: params.get("session_id") ?? void 0
3520
- });
3521
- this.json(res, { data: events, count: events.length });
3522
- });
3523
- this.routes.set("GET /api/events/console", (_req, res, params) => {
3524
- const events = this.store.getConsoleMessages({
3525
- sinceSeconds: numParam(params, "since_seconds"),
3526
- level: params.get("level") ?? void 0,
3527
- search: params.get("search") ?? void 0,
3528
- sessionId: params.get("session_id") ?? void 0
3529
- });
3530
- this.json(res, { data: events, count: events.length });
3531
- });
3532
- this.routes.set("GET /api/events/state", (_req, res, params) => {
3533
- const events = this.store.getStateEvents({
3534
- sinceSeconds: numParam(params, "since_seconds"),
3535
- storeId: params.get("store_id") ?? void 0,
3536
- sessionId: params.get("session_id") ?? void 0
3537
- });
3538
- this.json(res, { data: events, count: events.length });
3539
- });
3540
- this.routes.set("GET /api/events/renders", (_req, res, params) => {
3541
- const events = this.store.getRenderEvents({
3542
- sinceSeconds: numParam(params, "since_seconds"),
3543
- componentName: params.get("component") ?? void 0,
3544
- sessionId: params.get("session_id") ?? void 0
3545
- });
3546
- this.json(res, { data: events, count: events.length });
3547
- });
3548
- this.routes.set("GET /api/events/performance", (_req, res, params) => {
3549
- const events = this.store.getPerformanceMetrics({
3550
- sinceSeconds: numParam(params, "since_seconds"),
3551
- metricName: params.get("metric") ?? void 0,
3552
- sessionId: params.get("session_id") ?? void 0
3553
- });
3554
- this.json(res, { data: events, count: events.length });
3555
- });
3556
- this.routes.set("GET /api/events/database", (_req, res, params) => {
3557
- const events = this.store.getDatabaseEvents({
3558
- sinceSeconds: numParam(params, "since_seconds"),
3559
- table: params.get("table") ?? void 0,
3560
- minDurationMs: numParam(params, "min_duration_ms"),
3561
- search: params.get("search") ?? void 0,
3562
- sessionId: params.get("session_id") ?? void 0
3563
- });
3564
- this.json(res, { data: events, count: events.length });
3565
- });
3566
- this.routes.set("GET /api/events/timeline", (_req, res, params) => {
3567
- const eventTypes = params.get("event_types")?.split(",") ?? void 0;
3568
- const events = this.store.getEventTimeline({
3569
- sinceSeconds: numParam(params, "since_seconds"),
3570
- eventTypes,
3571
- sessionId: params.get("session_id") ?? void 0
3572
- });
3573
- this.json(res, { data: events, count: events.length });
3574
- });
3575
- this.routes.set("DELETE /api/events", (_req, res) => {
3576
- const result = this.store.clear();
3577
- this.json(res, result);
3578
- });
3579
- }
3580
- /**
3581
- * Resolve the SDK IIFE bundle path.
3582
- * Tries multiple locations for monorepo and installed-package scenarios.
3583
- */
3584
- resolveSdkPath() {
3585
- if (this.sdkBundlePath) return this.sdkBundlePath;
3586
- const __dir = dirname(fileURLToPath(import.meta.url));
3587
- const candidates = [
3588
- resolve(__dir, "../../sdk/dist/index.global.js"),
3589
- // monorepo: packages/collector/dist -> packages/sdk/dist
3590
- resolve(__dir, "../../../node_modules/@runtimescope/sdk/dist/index.global.js")
3591
- // npm installed
3592
- ];
3593
- for (const p of candidates) {
3594
- if (existsSync2(p)) {
3595
- this.sdkBundlePath = p;
3596
- return p;
3597
- }
3598
- }
3599
- return null;
3600
- }
3601
- async start(options = {}) {
3602
- const basePort = options.port ?? parseInt(process.env.RUNTIMESCOPE_HTTP_PORT ?? "9091", 10);
3603
- const host = options.host ?? "127.0.0.1";
3604
- const tls = options.tls;
3605
- const maxRetries = 5;
3606
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
3607
- const port = basePort + attempt;
3608
- try {
3609
- await this.tryStart(port, host, tls);
3610
- return;
3611
- } catch (err) {
3612
- const isAddrInUse = err.code === "EADDRINUSE";
3613
- if (isAddrInUse && attempt < maxRetries) {
3614
- console.error(`[RuntimeScope] HTTP port ${port} in use, trying ${port + 1}...`);
3615
- continue;
3616
- }
3617
- throw err;
3618
- }
3619
- }
3620
- }
3621
- tryStart(port, host, tls) {
3622
- return new Promise((resolve2, reject) => {
3623
- const handler = (req, res) => this.handleRequest(req, res);
3624
- const server = tls ? createHttpsServer2(loadTlsOptions(tls), handler) : createServer(handler);
3625
- this.wss = new WebSocketServer2({ server, path: "/api/ws/events" });
3626
- this.wss.on("connection", (ws) => {
3627
- this.dashboardClients.add(ws);
3628
- ws.on("close", () => this.dashboardClients.delete(ws));
3629
- ws.on("error", () => this.dashboardClients.delete(ws));
3630
- });
3631
- this.eventListener = (event) => this.broadcastEvent(event);
3632
- this.store.onEvent(this.eventListener);
3633
- server.on("listening", () => {
3634
- this.server = server;
3635
- this.activePort = port;
3636
- this.startedAt = Date.now();
3637
- const proto = tls ? "https" : "http";
3638
- console.error(`[RuntimeScope] HTTP API listening on ${proto}://${host}:${port}`);
3639
- resolve2();
3640
- });
3641
- server.on("error", (err) => {
3642
- this.wss?.close();
3643
- this.wss = null;
3644
- if (this.eventListener) {
3645
- this.store.removeEventListener(this.eventListener);
3646
- this.eventListener = null;
3647
- }
3648
- reject(err);
3649
- });
3650
- server.listen(port, host);
3651
- });
3652
- }
3653
- async stop() {
3654
- if (this.eventListener) {
3655
- this.store.removeEventListener(this.eventListener);
3656
- this.eventListener = null;
3657
- }
3658
- for (const ws of this.dashboardClients) {
3659
- ws.close();
3660
- }
3661
- this.dashboardClients.clear();
3662
- if (this.wss) {
3663
- this.wss.close();
3664
- this.wss = null;
3665
- }
3666
- if (this.server) {
3667
- return new Promise((resolve2) => {
3668
- this.server.close(() => {
3669
- this.server = null;
3670
- console.error("[RuntimeScope] HTTP API stopped");
3671
- resolve2();
3672
- });
3673
- });
3674
- }
3675
- }
3676
- broadcastEvent(event) {
3677
- if (this.dashboardClients.size === 0) return;
3678
- const message = JSON.stringify({ type: "event", data: event });
3679
- for (const ws of this.dashboardClients) {
3680
- if (ws.readyState === 1) {
3681
- try {
3682
- ws.send(message);
3683
- } catch {
3684
- this.dashboardClients.delete(ws);
3685
- }
3686
- }
3687
- }
3688
- }
3689
- handleRequest(req, res) {
3690
- const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
3691
- this.setCorsHeaders(req, res);
3692
- if (req.method === "OPTIONS") {
3693
- res.writeHead(204);
3694
- res.end();
3695
- return;
3696
- }
3697
- const isPublic = url.pathname === "/api/health" || url.pathname === "/runtimescope.js" || url.pathname === "/snippet";
3698
- if (!isPublic && this.authManager?.isEnabled()) {
3699
- const token = AuthManager.extractBearer(req.headers.authorization);
3700
- if (!this.authManager.isAuthorized(token)) {
3701
- this.json(res, { error: "Unauthorized", code: "AUTH_FAILED" }, 401);
3702
- return;
3703
- }
3704
- }
3705
- if (req.method === "GET" && url.pathname === "/runtimescope.js") {
3706
- const sdkPath = this.resolveSdkPath();
3707
- if (sdkPath) {
3708
- const bundle = readFileSync3(sdkPath, "utf-8");
3709
- res.writeHead(200, {
3710
- "Content-Type": "application/javascript",
3711
- "Cache-Control": "no-cache"
3712
- });
3713
- res.end(bundle);
3714
- } else {
3715
- res.writeHead(404, { "Content-Type": "text/plain" });
3716
- res.end("SDK bundle not found. Run: npm run build -w packages/sdk");
3717
- }
3718
- return;
3719
- }
3720
- if (req.method === "GET" && url.pathname === "/snippet") {
3721
- const appName = url.searchParams.get("app") || "my-app";
3722
- const wsPort = process.env.RUNTIMESCOPE_PORT ?? "9090";
3723
- const snippet = `<!-- RuntimeScope SDK \u2014 paste before </body> -->
3724
- <script src="http://localhost:${this.activePort}/runtimescope.js"></script>
3725
- <script>
3726
- RuntimeScope.init({
3727
- appName: '${appName}',
3728
- endpoint: 'ws://localhost:${wsPort}',
3729
- });
3730
- </script>`;
3731
- res.writeHead(200, {
3732
- "Content-Type": "text/plain"
3733
- });
3734
- res.end(snippet);
3735
- return;
3736
- }
3737
- const routeKey = `${req.method} ${url.pathname}`;
3738
- const handler = this.routes.get(routeKey);
3739
- if (handler) {
3740
- try {
3741
- const result = handler(req, res, url.searchParams);
3742
- if (result instanceof Promise) {
3743
- result.catch((err) => {
3744
- this.json(res, { error: err.message }, 500);
3745
- });
3746
- }
3747
- } catch (err) {
3748
- this.json(res, { error: err.message }, 500);
3749
- }
3750
- } else {
3751
- this.json(res, { error: "Not found", path: url.pathname }, 404);
3752
- }
3753
- }
3754
- setCorsHeaders(req, res) {
3755
- const origin = req.headers.origin;
3756
- if (this.allowedOrigins && this.allowedOrigins.length > 0) {
3757
- if (origin && this.allowedOrigins.includes(origin)) {
3758
- res.setHeader("Access-Control-Allow-Origin", origin);
3759
- res.setHeader("Vary", "Origin");
3760
- }
3761
- } else {
3762
- res.setHeader("Access-Control-Allow-Origin", "*");
3763
- }
3764
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
3765
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
3766
- }
3767
- json(res, data, status = 200) {
3768
- res.writeHead(status, { "Content-Type": "application/json" });
3769
- res.end(JSON.stringify(data));
3770
- }
3771
- };
3772
- function numParam(params, key) {
3773
- const val = params.get(key);
3774
- if (!val) return void 0;
3775
- const num = parseInt(val, 10);
3776
- return isNaN(num) ? void 0 : num;
3777
- }
3778
2065
  export {
3779
2066
  ApiDiscoveryEngine,
3780
2067
  AuthManager,
@@ -3785,7 +2072,9 @@ export {
3785
2072
  EventStore,
3786
2073
  HttpServer,
3787
2074
  InfraConnector,
2075
+ PmStore,
3788
2076
  ProcessMonitor,
2077
+ ProjectDiscovery,
3789
2078
  ProjectManager,
3790
2079
  Redactor,
3791
2080
  RingBuffer,
@@ -3794,6 +2083,8 @@ export {
3794
2083
  SessionRateLimiter,
3795
2084
  SqliteStore,
3796
2085
  aggregateQueryStats,
2086
+ calculateActiveMinutes,
2087
+ calculateCostMicrodollars,
3797
2088
  compareSessions,
3798
2089
  detectIssues,
3799
2090
  detectN1Queries,
@@ -3801,6 +2092,7 @@ export {
3801
2092
  detectSlowQueries,
3802
2093
  generateApiKey,
3803
2094
  loadTlsOptions,
2095
+ parseSessionJsonl,
3804
2096
  resolveTlsConfig,
3805
2097
  suggestIndexes
3806
2098
  };