@runtime-digital-twin/sdk 1.0.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.
Files changed (56) hide show
  1. package/README.md +214 -0
  2. package/dist/constants.d.ts +11 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +13 -0
  5. package/dist/db-wrapper.d.ts +258 -0
  6. package/dist/db-wrapper.d.ts.map +1 -0
  7. package/dist/db-wrapper.js +636 -0
  8. package/dist/event-envelope.d.ts +35 -0
  9. package/dist/event-envelope.d.ts.map +1 -0
  10. package/dist/event-envelope.js +101 -0
  11. package/dist/fastify-plugin.d.ts +29 -0
  12. package/dist/fastify-plugin.d.ts.map +1 -0
  13. package/dist/fastify-plugin.js +243 -0
  14. package/dist/http-sentinels.d.ts +39 -0
  15. package/dist/http-sentinels.d.ts.map +1 -0
  16. package/dist/http-sentinels.js +169 -0
  17. package/dist/http-wrapper.d.ts +25 -0
  18. package/dist/http-wrapper.d.ts.map +1 -0
  19. package/dist/http-wrapper.js +477 -0
  20. package/dist/index.d.ts +19 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +93 -0
  23. package/dist/invariants.d.ts +58 -0
  24. package/dist/invariants.d.ts.map +1 -0
  25. package/dist/invariants.js +192 -0
  26. package/dist/multi-service-edge-builder.d.ts +80 -0
  27. package/dist/multi-service-edge-builder.d.ts.map +1 -0
  28. package/dist/multi-service-edge-builder.js +107 -0
  29. package/dist/outbound-matcher.d.ts +192 -0
  30. package/dist/outbound-matcher.d.ts.map +1 -0
  31. package/dist/outbound-matcher.js +457 -0
  32. package/dist/peer-service-resolver.d.ts +22 -0
  33. package/dist/peer-service-resolver.d.ts.map +1 -0
  34. package/dist/peer-service-resolver.js +85 -0
  35. package/dist/redaction.d.ts +111 -0
  36. package/dist/redaction.d.ts.map +1 -0
  37. package/dist/redaction.js +487 -0
  38. package/dist/replay-logger.d.ts +438 -0
  39. package/dist/replay-logger.d.ts.map +1 -0
  40. package/dist/replay-logger.js +434 -0
  41. package/dist/root-cause-analyzer.d.ts +45 -0
  42. package/dist/root-cause-analyzer.d.ts.map +1 -0
  43. package/dist/root-cause-analyzer.js +606 -0
  44. package/dist/shape-digest-utils.d.ts +45 -0
  45. package/dist/shape-digest-utils.d.ts.map +1 -0
  46. package/dist/shape-digest-utils.js +154 -0
  47. package/dist/trace-bundle-writer.d.ts +52 -0
  48. package/dist/trace-bundle-writer.d.ts.map +1 -0
  49. package/dist/trace-bundle-writer.js +267 -0
  50. package/dist/trace-loader.d.ts +69 -0
  51. package/dist/trace-loader.d.ts.map +1 -0
  52. package/dist/trace-loader.js +146 -0
  53. package/dist/trace-uploader.d.ts +25 -0
  54. package/dist/trace-uploader.d.ts.map +1 -0
  55. package/dist/trace-uploader.js +132 -0
  56. package/package.json +63 -0
@@ -0,0 +1,636 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DbQueryMatcher = void 0;
37
+ exports.normalizeSql = normalizeSql;
38
+ exports.computeParamsHash = computeParamsHash;
39
+ exports.extractOperation = extractOperation;
40
+ exports.compareDbQueries = compareDbQueries;
41
+ exports.parseRecordedDbQueries = parseRecordedDbQueries;
42
+ exports.wrapDbClient = wrapDbClient;
43
+ exports.formatDbQueryMismatch = formatDbQueryMismatch;
44
+ exports.formatUnexpectedDbQuery = formatUnexpectedDbQuery;
45
+ exports.formatMissingDbQuery = formatMissingDbQuery;
46
+ exports.formatBlockedDbAccess = formatBlockedDbAccess;
47
+ exports.formatDbQueryFailure = formatDbQueryFailure;
48
+ exports.createWrappedDbClient = createWrappedDbClient;
49
+ const crypto_1 = require("crypto");
50
+ const trace_bundle_writer_1 = require("./trace-bundle-writer");
51
+ const http_wrapper_1 = require("./http-wrapper");
52
+ /**
53
+ * Normalize SQL for matching: lowercase, trim whitespace, normalize spaces
54
+ */
55
+ function normalizeSql(sql) {
56
+ return sql
57
+ .toLowerCase()
58
+ .trim()
59
+ .replace(/\s+/g, " ")
60
+ .replace(/\(\s+/g, "(")
61
+ .replace(/\s+\)/g, ")");
62
+ }
63
+ /**
64
+ * Compute hash of query parameters
65
+ */
66
+ function computeParamsHash(params) {
67
+ const paramsStr = JSON.stringify(params);
68
+ const hash = (0, crypto_1.createHash)("sha256").update(paramsStr).digest("hex");
69
+ return `sha256:${hash}`;
70
+ }
71
+ /**
72
+ * Extract operation type from SQL
73
+ */
74
+ function extractOperation(sql) {
75
+ const normalized = sql.trim().toUpperCase();
76
+ if (normalized.startsWith("SELECT"))
77
+ return "SELECT";
78
+ if (normalized.startsWith("INSERT"))
79
+ return "INSERT";
80
+ if (normalized.startsWith("UPDATE"))
81
+ return "UPDATE";
82
+ if (normalized.startsWith("DELETE"))
83
+ return "DELETE";
84
+ if (normalized.startsWith("CREATE"))
85
+ return "CREATE";
86
+ if (normalized.startsWith("DROP"))
87
+ return "DROP";
88
+ if (normalized.startsWith("ALTER"))
89
+ return "ALTER";
90
+ return "UNKNOWN";
91
+ }
92
+ /**
93
+ * Compare two queries and return differences
94
+ */
95
+ function compareDbQueries(expected, actual) {
96
+ const diffs = [];
97
+ // SQL (normalized)
98
+ const expectedSql = expected.normalizedSql;
99
+ const actualSql = normalizeSql(actual.sql);
100
+ if (expectedSql !== actualSql) {
101
+ diffs.push({ field: "sql", expected: expectedSql, actual: actualSql });
102
+ }
103
+ // Params hash
104
+ const expectedParamsHash = expected.paramsHash;
105
+ const actualParamsHash = computeParamsHash(actual.params);
106
+ if (expectedParamsHash !== actualParamsHash) {
107
+ diffs.push({
108
+ field: "params",
109
+ expected: JSON.stringify(expected.params),
110
+ actual: JSON.stringify(actual.params),
111
+ });
112
+ }
113
+ return { matches: diffs.length === 0, diffs };
114
+ }
115
+ /**
116
+ * Ordered DB query matcher for replay
117
+ */
118
+ class DbQueryMatcher {
119
+ recordedQueries = [];
120
+ currentIndex = 0;
121
+ mode;
122
+ traceDir;
123
+ divergences = [];
124
+ logger = null;
125
+ constructor(recordedQueries, mode, traceDir, logger) {
126
+ this.recordedQueries = recordedQueries;
127
+ this.mode = mode;
128
+ this.traceDir = traceDir;
129
+ this.logger = logger || null;
130
+ }
131
+ /**
132
+ * Set the logger for structured replay logging
133
+ */
134
+ setLogger(logger) {
135
+ this.logger = logger;
136
+ }
137
+ /**
138
+ * Get all recorded divergences
139
+ */
140
+ getDivergences() {
141
+ return [...this.divergences];
142
+ }
143
+ /**
144
+ * Get current query index
145
+ */
146
+ getCurrentIndex() {
147
+ return this.currentIndex;
148
+ }
149
+ /**
150
+ * Get total recorded queries
151
+ */
152
+ getTotalQueries() {
153
+ return this.recordedQueries.length;
154
+ }
155
+ /**
156
+ * Check if only SELECT queries are in the recorded set
157
+ */
158
+ isReadOnly() {
159
+ return this.recordedQueries.every((q) => q.operation === "SELECT");
160
+ }
161
+ /**
162
+ * Match the next DB query against the recorded sequence
163
+ *
164
+ * In strict mode: throws DbQueryMismatch on any mismatch
165
+ * In explain mode: emits divergence and returns best-effort match
166
+ */
167
+ matchNext(actual) {
168
+ const queryIndex = this.currentIndex;
169
+ const operation = extractOperation(actual.sql);
170
+ const normalizedSql = normalizeSql(actual.sql);
171
+ const paramsHash = computeParamsHash(actual.params);
172
+ // Phase 1: Only support SELECT queries
173
+ if (operation !== "SELECT") {
174
+ const failure = {
175
+ type: "blocked_db_access",
176
+ location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
177
+ expected: "SELECT query only (Phase 1)",
178
+ actual: `${operation} query`,
179
+ hint: "DB replay Phase 1 only supports SELECT queries. Write operations are blocked.",
180
+ query: actual,
181
+ };
182
+ // Log blocked query
183
+ this.logger?.dbBlocked({
184
+ operation,
185
+ sql: actual.sql.substring(0, 100),
186
+ reason: "Phase 1 only supports SELECT queries",
187
+ });
188
+ throw failure;
189
+ }
190
+ // Check if we've exceeded recorded queries
191
+ if (queryIndex >= this.recordedQueries.length) {
192
+ const failure = {
193
+ type: "unexpected_db_query",
194
+ location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
195
+ expected: `<end of recorded queries after ${this.recordedQueries.length} query(s)>`,
196
+ actual: `${operation}: ${normalizedSql.substring(0, 50)}...`,
197
+ hint: "A DB query occurred during replay that was not recorded in the original trace.",
198
+ queryIndex,
199
+ service: this.traceDir,
200
+ query: actual,
201
+ };
202
+ // Log unexpected query
203
+ this.logger?.dbUnexpected({
204
+ queryIndex,
205
+ operation,
206
+ sql: normalizedSql,
207
+ paramsHash,
208
+ expectedQueryCount: this.recordedQueries.length,
209
+ });
210
+ if (this.mode === "strict") {
211
+ throw failure;
212
+ }
213
+ this.divergences.push({
214
+ type: "divergence",
215
+ severity: "error",
216
+ queryIndex,
217
+ message: `Unexpected DB query: ${actual.sql.substring(0, 50)}...`,
218
+ expected: null,
219
+ actual,
220
+ recoverable: false,
221
+ });
222
+ throw failure;
223
+ }
224
+ const expected = this.recordedQueries[queryIndex];
225
+ const { matches, diffs } = compareDbQueries(expected, actual);
226
+ if (matches) {
227
+ // Log successful match
228
+ this.logger?.dbMatch({
229
+ queryIndex,
230
+ operation,
231
+ sql: actual.sql,
232
+ normalizedSql,
233
+ paramsHash,
234
+ matchedSpanId: expected.spanId,
235
+ rowCount: expected.result.rowCount,
236
+ });
237
+ this.currentIndex++;
238
+ return expected;
239
+ }
240
+ // Explain mode: check if divergence is recoverable
241
+ // Only params difference is recoverable (same query structure)
242
+ const criticalDiffs = diffs.filter((d) => d.field === "sql");
243
+ const recoverable = criticalDiffs.length === 0;
244
+ // Log mismatch
245
+ this.logger?.dbMismatch({
246
+ queryIndex,
247
+ expected: {
248
+ operation: expected.operation,
249
+ sql: expected.normalizedSql,
250
+ paramsHash: expected.paramsHash,
251
+ },
252
+ actual: {
253
+ operation,
254
+ sql: normalizedSql,
255
+ paramsHash,
256
+ },
257
+ differences: diffs,
258
+ recoverable,
259
+ });
260
+ // Mismatch detected
261
+ const failure = {
262
+ type: "db_query_mismatch",
263
+ location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
264
+ expected: `${expected.operation}: ${expected.normalizedSql.substring(0, 50)}...`,
265
+ actual: `${operation}: ${normalizedSql.substring(0, 50)}...`,
266
+ hint: "DB query does not match recorded trace. Check for non-determinism or code changes.",
267
+ queryIndex,
268
+ service: this.traceDir,
269
+ diff: diffs,
270
+ };
271
+ if (this.mode === "strict") {
272
+ throw failure;
273
+ }
274
+ this.divergences.push({
275
+ type: "divergence",
276
+ severity: recoverable ? "warning" : "error",
277
+ queryIndex,
278
+ message: `DB query mismatch: ${diffs
279
+ .map((d) => `${d.field}`)
280
+ .join(", ")}`,
281
+ expected,
282
+ actual,
283
+ recoverable,
284
+ });
285
+ console.warn(`[Replay] DB query divergence at query ${queryIndex}: ${failure.expected} vs ${failure.actual}`);
286
+ for (const diff of diffs) {
287
+ console.warn(` ${diff.field}: expected="${diff.expected?.substring(0, 50)}" actual="${diff.actual?.substring(0, 50)}"`);
288
+ }
289
+ if (!recoverable) {
290
+ throw failure;
291
+ }
292
+ // Continue with the recorded response despite param differences
293
+ this.currentIndex++;
294
+ return expected;
295
+ }
296
+ /**
297
+ * Check if all recorded queries have been matched
298
+ */
299
+ isComplete() {
300
+ return this.currentIndex >= this.recordedQueries.length;
301
+ }
302
+ /**
303
+ * Get summary of unmatched queries
304
+ */
305
+ getUnmatchedQueries() {
306
+ return this.recordedQueries.slice(this.currentIndex);
307
+ }
308
+ /**
309
+ * Finalize replay and check for missing DB queries
310
+ */
311
+ finalize() {
312
+ const missingQueries = [];
313
+ const unmatchedQueries = this.getUnmatchedQueries();
314
+ for (let i = 0; i < unmatchedQueries.length; i++) {
315
+ const recordedQuery = unmatchedQueries[i];
316
+ const queryIndex = this.currentIndex + i;
317
+ const failure = {
318
+ type: "missing_db_query",
319
+ location: `${this.traceDir}/events.jsonl:query[${queryIndex}]`,
320
+ expected: `${recordedQuery.operation}: ${recordedQuery.normalizedSql.substring(0, 50)}...`,
321
+ actual: "<query never executed>",
322
+ hint: "A recorded DB query was never executed during replay.",
323
+ queryIndex,
324
+ service: this.traceDir,
325
+ spanId: recordedQuery.spanId,
326
+ recordedQuery,
327
+ };
328
+ missingQueries.push(failure);
329
+ // Log missing query
330
+ this.logger?.dbMissing({
331
+ queryIndex,
332
+ operation: recordedQuery.operation,
333
+ sql: recordedQuery.normalizedSql,
334
+ spanId: recordedQuery.spanId,
335
+ });
336
+ this.divergences.push({
337
+ type: "divergence",
338
+ severity: "error",
339
+ queryIndex,
340
+ message: `Missing DB query: ${recordedQuery.operation} ${recordedQuery.normalizedSql.substring(0, 50)}...`,
341
+ expected: recordedQuery,
342
+ actual: { sql: "", params: [] },
343
+ recoverable: false,
344
+ });
345
+ console.warn(`[Replay] Missing query ${queryIndex}: expected ${recordedQuery.operation} ${recordedQuery.normalizedSql.substring(0, 50)}...`);
346
+ }
347
+ if (missingQueries.length > 0 && this.mode === "strict") {
348
+ throw missingQueries[0];
349
+ }
350
+ return missingQueries;
351
+ }
352
+ }
353
+ exports.DbQueryMatcher = DbQueryMatcher;
354
+ /**
355
+ * Parse recorded DB queries from trace events
356
+ */
357
+ function parseRecordedDbQueries(events) {
358
+ const queries = [];
359
+ for (const event of events) {
360
+ if (event.type === "db.query") {
361
+ queries.push({
362
+ spanId: event.spanId,
363
+ operation: event.operation,
364
+ sql: event.sql,
365
+ normalizedSql: normalizeSql(event.sql),
366
+ params: event.params || [],
367
+ paramsHash: computeParamsHash(event.params || []),
368
+ result: {
369
+ rows: event.rows || [],
370
+ rowCount: event.rowCount || 0,
371
+ resultHash: event.resultHash || null,
372
+ resultBlob: event.resultBlob || null,
373
+ },
374
+ durationMs: event.durationMs || 0,
375
+ });
376
+ }
377
+ }
378
+ return queries;
379
+ }
380
+ /**
381
+ * Wrap a DB client for recording or replay
382
+ *
383
+ * During recording: captures queries and results
384
+ * During replay: returns recorded results, blocks live DB access
385
+ */
386
+ function wrapDbClient(client, options) {
387
+ const { mode, matcher, readBlob } = options;
388
+ let queryCount = 0;
389
+ if (mode === "replay") {
390
+ if (!matcher) {
391
+ throw new Error("DbQueryMatcher is required for replay mode");
392
+ }
393
+ // Replay mode: return recorded results, never touch live DB
394
+ return {
395
+ async query(sql, params = []) {
396
+ queryCount++;
397
+ // Match against recorded queries
398
+ const recorded = matcher.matchNext({ sql, params });
399
+ // Return recorded result
400
+ let rows = recorded.result.rows;
401
+ // If rows are stored in blob, read from blob
402
+ if (recorded.result.resultBlob && readBlob && rows.length === 0) {
403
+ const blobRows = await readBlob(recorded.result.resultBlob);
404
+ if (blobRows) {
405
+ rows = blobRows;
406
+ }
407
+ }
408
+ return {
409
+ rows,
410
+ rowCount: recorded.result.rowCount,
411
+ };
412
+ },
413
+ getQueryCount() {
414
+ return queryCount;
415
+ },
416
+ };
417
+ }
418
+ // Record mode: capture queries and forward to real client
419
+ if (!client) {
420
+ throw new Error("DbClient is required for record mode");
421
+ }
422
+ return {
423
+ async query(sql, params = []) {
424
+ queryCount++;
425
+ const { bundle, spanId } = (0, http_wrapper_1.getTraceContext)();
426
+ const operation = extractOperation(sql);
427
+ const startTime = Date.now();
428
+ const querySpanId = (0, trace_bundle_writer_1.generateSpanId)();
429
+ try {
430
+ // Execute the real query
431
+ const result = await client.query(sql, params);
432
+ const endTime = Date.now();
433
+ const durationMs = endTime - startTime;
434
+ // Record the query if we have a trace context
435
+ if (bundle) {
436
+ // Store result rows (potentially as blob if large)
437
+ const { bodyHash: resultHash, bodyBlob: resultBlob } = await (0, trace_bundle_writer_1.processBody)(result.rows, bundle.writeBlob);
438
+ await bundle.writeEvent({
439
+ type: "db.query",
440
+ timestamp: startTime,
441
+ spanId: querySpanId,
442
+ parentSpanId: spanId,
443
+ operation,
444
+ sql,
445
+ params,
446
+ rows: resultBlob ? [] : result.rows, // Inline if small, empty if in blob
447
+ resultHash,
448
+ resultBlob,
449
+ rowCount: result.rowCount,
450
+ durationMs,
451
+ });
452
+ }
453
+ return result;
454
+ }
455
+ catch (error) {
456
+ const endTime = Date.now();
457
+ // Record error if we have a trace context
458
+ if (bundle) {
459
+ const err = error;
460
+ await bundle.writeEvent({
461
+ type: "error",
462
+ timestamp: endTime,
463
+ spanId: querySpanId,
464
+ parentSpanId: spanId,
465
+ error: {
466
+ name: err.name || "Error",
467
+ message: err.message || String(error),
468
+ stack: err.stack || undefined,
469
+ },
470
+ });
471
+ }
472
+ throw error;
473
+ }
474
+ },
475
+ getQueryCount() {
476
+ return queryCount;
477
+ },
478
+ };
479
+ }
480
+ /**
481
+ * Format DbQueryMismatch for CLI output
482
+ */
483
+ function formatDbQueryMismatch(failure) {
484
+ const lines = [
485
+ `[Replay Error] ${failure.type}`,
486
+ ` Location: ${failure.location}`,
487
+ ` Service: ${failure.service}`,
488
+ ` Query #${failure.queryIndex}`,
489
+ ` Expected: ${failure.expected}`,
490
+ ` Actual: ${failure.actual}`,
491
+ ` Differences:`,
492
+ ];
493
+ for (const diff of failure.diff) {
494
+ lines.push(` ${diff.field}: "${diff.expected?.substring(0, 50)}" → "${diff.actual?.substring(0, 50)}"`);
495
+ }
496
+ lines.push(` Hint: ${failure.hint}`);
497
+ return lines.join("\n");
498
+ }
499
+ /**
500
+ * Format UnexpectedDbQuery for CLI output
501
+ */
502
+ function formatUnexpectedDbQuery(failure) {
503
+ const lines = [
504
+ `[Replay Error] ${failure.type}`,
505
+ ` Location: ${failure.location}`,
506
+ ` Service: ${failure.service}`,
507
+ ` Query #${failure.queryIndex}`,
508
+ ` Expected: ${failure.expected}`,
509
+ ` Actual: ${failure.actual}`,
510
+ ` Query: ${failure.query.sql.substring(0, 100)}...`,
511
+ ` Hint: ${failure.hint}`,
512
+ ];
513
+ return lines.join("\n");
514
+ }
515
+ /**
516
+ * Format MissingDbQuery for CLI output
517
+ */
518
+ function formatMissingDbQuery(failure) {
519
+ const lines = [
520
+ `[Replay Error] ${failure.type}`,
521
+ ` Location: ${failure.location}`,
522
+ ` Service: ${failure.service}`,
523
+ ` Span ID: ${failure.spanId}`,
524
+ ` Query #${failure.queryIndex}`,
525
+ ` Expected: ${failure.expected}`,
526
+ ` Actual: ${failure.actual}`,
527
+ ` Hint: ${failure.hint}`,
528
+ ];
529
+ return lines.join("\n");
530
+ }
531
+ /**
532
+ * Format BlockedDbAccess for CLI output
533
+ */
534
+ function formatBlockedDbAccess(failure) {
535
+ const lines = [
536
+ `[Replay Error] ${failure.type}`,
537
+ ` Location: ${failure.location}`,
538
+ ` Expected: ${failure.expected}`,
539
+ ` Actual: ${failure.actual}`,
540
+ ` Query: ${failure.query.sql.substring(0, 100)}...`,
541
+ ` Hint: ${failure.hint}`,
542
+ ];
543
+ return lines.join("\n");
544
+ }
545
+ /**
546
+ * Format any DB query failure for CLI output
547
+ */
548
+ function formatDbQueryFailure(failure) {
549
+ switch (failure.type) {
550
+ case "db_query_mismatch":
551
+ return formatDbQueryMismatch(failure);
552
+ case "unexpected_db_query":
553
+ return formatUnexpectedDbQuery(failure);
554
+ case "missing_db_query":
555
+ return formatMissingDbQuery(failure);
556
+ case "blocked_db_access":
557
+ return formatBlockedDbAccess(failure);
558
+ default:
559
+ return `[Replay Error] Unknown DB failure type`;
560
+ }
561
+ }
562
+ async function createWrappedDbClient(options = {}) {
563
+ const { realClient, traceDir, replayMode, readBlob } = options;
564
+ // Detect replay mode
565
+ const mode = process.env.RDT_MODE || process.env.RDT_REPLAY === "1" || process.env.RDT_REPLAY === "true"
566
+ ? "replay"
567
+ : "record";
568
+ if (mode === "replay") {
569
+ // Replay mode: create matcher from trace
570
+ const actualTraceDir = traceDir || process.env.RDT_TRACE_DIR || process.env.TRACE_DIR;
571
+ if (!actualTraceDir) {
572
+ console.warn("[DB Wrapper] Replay mode detected but RDT_TRACE_DIR not set. DB queries will be disabled.");
573
+ return null;
574
+ }
575
+ try {
576
+ // Dynamically import fs/path modules to avoid issues in browser environments
577
+ const { readFile } = await Promise.resolve().then(() => __importStar(require("fs/promises")));
578
+ const { createReadStream } = await Promise.resolve().then(() => __importStar(require("fs")));
579
+ const { createInterface } = await Promise.resolve().then(() => __importStar(require("readline")));
580
+ const { join } = await Promise.resolve().then(() => __importStar(require("path")));
581
+ // Load events and create matcher
582
+ const eventsPath = join(actualTraceDir, "events.jsonl");
583
+ const events = [];
584
+ try {
585
+ const fileStream = createReadStream(eventsPath);
586
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
587
+ for await (const line of rl) {
588
+ const trimmed = line.trim();
589
+ if (trimmed) {
590
+ try {
591
+ events.push(JSON.parse(trimmed));
592
+ }
593
+ catch {
594
+ // Skip malformed lines
595
+ }
596
+ }
597
+ }
598
+ }
599
+ catch (error) {
600
+ console.warn(`[DB Wrapper] Failed to load trace events from ${eventsPath}: ${error}`);
601
+ return null;
602
+ }
603
+ const recordedQueries = parseRecordedDbQueries(events);
604
+ if (recordedQueries.length === 0) {
605
+ // No DB queries in trace - return null (service can handle this)
606
+ return null;
607
+ }
608
+ const actualReplayMode = (replayMode || process.env.RDT_REPLAY_MODE || "strict");
609
+ const matcher = new DbQueryMatcher(recordedQueries, actualReplayMode, actualTraceDir);
610
+ // Default blob reader if not provided
611
+ const actualReadBlob = readBlob || (async (blobRef) => {
612
+ const hash = blobRef.startsWith("sha256:") ? blobRef.substring(7) : blobRef;
613
+ const blobPath = join(actualTraceDir, "blobs", `${hash}.json`);
614
+ try {
615
+ return JSON.parse(await readFile(blobPath, "utf8"));
616
+ }
617
+ catch {
618
+ return null;
619
+ }
620
+ });
621
+ return wrapDbClient(null, { mode: "replay", matcher, readBlob: actualReadBlob });
622
+ }
623
+ catch (error) {
624
+ console.warn(`[DB Wrapper] Failed to create replay DB client: ${error}`);
625
+ return null;
626
+ }
627
+ }
628
+ else {
629
+ // Record mode: wrap real client
630
+ if (!realClient) {
631
+ console.warn("[DB Wrapper] Record mode but no realClient provided. DB queries will be disabled.");
632
+ return null;
633
+ }
634
+ return wrapDbClient(realClient, { mode: "record" });
635
+ }
636
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Base event envelope - all trace events must include these fields
3
+ */
4
+ /**
5
+ * Base event envelope interface
6
+ */
7
+ export interface BaseEventEnvelope {
8
+ type: string;
9
+ timestamp: number;
10
+ traceId: string;
11
+ spanId: string;
12
+ parentSpanId: string | null;
13
+ serviceName: string;
14
+ }
15
+ /**
16
+ * Create a base event envelope with required fields
17
+ *
18
+ * @param type - Event type (e.g., 'http.request.inbound')
19
+ * @param timestamp - Event timestamp (milliseconds since epoch)
20
+ * @param traceId - Trace ID
21
+ * @param spanId - Span ID
22
+ * @param parentSpanId - Parent span ID (null for root spans)
23
+ * @param serviceName - Optional service name (will be resolved if not provided)
24
+ * @param additionalFields - Additional event-specific fields
25
+ * @returns Complete event object with base envelope + additional fields
26
+ */
27
+ export declare function createBaseEvent(type: string, timestamp: number, traceId: string, spanId: string, parentSpanId: string | null, serviceName?: string, additionalFields?: Record<string, any>): BaseEventEnvelope & Record<string, any>;
28
+ /**
29
+ * Validate that an event has all required base envelope fields
30
+ */
31
+ export declare function validateBaseEnvelope(event: any): {
32
+ valid: boolean;
33
+ errors: string[];
34
+ };
35
+ //# sourceMappingURL=event-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"event-envelope.d.ts","sourceRoot":"","sources":["../src/event-envelope.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,WAAW,EAAE,MAAM,CAAC;CACrB;AAwCD;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,IAAI,EAC3B,WAAW,CAAC,EAAE,MAAM,EACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACrC,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAiBzC;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA0BrF"}