@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,58 @@
1
+ /**
2
+ * State Invariant Checking
3
+ *
4
+ * Lightweight contract checking that emits state.invariant events
5
+ * without throwing by default (observe mode).
6
+ */
7
+ export interface InvariantDetails {
8
+ paths?: string[];
9
+ expected?: string;
10
+ observed?: string;
11
+ }
12
+ export interface EmitInvariantOptions {
13
+ name: string;
14
+ passed: boolean;
15
+ severity: "warn" | "error";
16
+ details?: InvariantDetails;
17
+ }
18
+ /**
19
+ * Emit a state.invariant event
20
+ *
21
+ * Uses the current trace context if available. If no trace context is available,
22
+ * the event is silently dropped (fail-open behavior).
23
+ *
24
+ * @param options - Invariant options
25
+ * @param explicitSpanId - Optional explicit spanId (overrides trace context)
26
+ * @param explicitParentSpanId - Optional explicit parentSpanId (overrides trace context)
27
+ */
28
+ export declare function emitInvariant(options: EmitInvariantOptions, explicitSpanId?: string, explicitParentSpanId?: string | null): Promise<void>;
29
+ /**
30
+ * Assert that a value is not null/undefined
31
+ *
32
+ * @param path - JSONPath-like path to the value (e.g., "pricing.price")
33
+ * @param value - Value to check
34
+ * @param options - Optional details
35
+ */
36
+ export declare function assertNonNull(path: string, value: unknown, options?: {
37
+ expected?: string;
38
+ observed?: string;
39
+ }): Promise<void>;
40
+ /**
41
+ * Assert that an object has all required keys
42
+ *
43
+ * @param keys - Array of required key names
44
+ * @param obj - Object to check
45
+ * @param path - Optional JSONPath-like path prefix (e.g., "response")
46
+ */
47
+ export declare function assertHasKeys(keys: string[], obj: Record<string, unknown> | null | undefined, path?: string): Promise<void>;
48
+ /**
49
+ * Assert that a numeric value is within a range
50
+ *
51
+ * @param name - Name of the invariant (e.g., "pricing.price_range")
52
+ * @param value - Value to check
53
+ * @param min - Minimum value (inclusive)
54
+ * @param max - Maximum value (inclusive)
55
+ * @param path - Optional JSONPath-like path to the value
56
+ */
57
+ export declare function assertInRange(name: string, value: number | null | undefined, min: number, max: number, path?: string): Promise<void>;
58
+ //# sourceMappingURL=invariants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invariants.d.ts","sourceRoot":"","sources":["../src/invariants.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,EAC7B,cAAc,CAAC,EAAE,MAAM,EACvB,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,GACnC,OAAO,CAAC,IAAI,CAAC,CA0Df;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,OAAO,EACd,OAAO,CAAC,EAAE;IACR,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,OAAO,CAAC,IAAI,CAAC,CAyBf;AAED;;;;;;GAMG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EAC/C,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAyCf"}
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /**
3
+ * State Invariant Checking
4
+ *
5
+ * Lightweight contract checking that emits state.invariant events
6
+ * without throwing by default (observe mode).
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.emitInvariant = emitInvariant;
10
+ exports.assertNonNull = assertNonNull;
11
+ exports.assertHasKeys = assertHasKeys;
12
+ exports.assertInRange = assertInRange;
13
+ const http_wrapper_1 = require("./http-wrapper");
14
+ /**
15
+ * Emit a state.invariant event
16
+ *
17
+ * Uses the current trace context if available. If no trace context is available,
18
+ * the event is silently dropped (fail-open behavior).
19
+ *
20
+ * @param options - Invariant options
21
+ * @param explicitSpanId - Optional explicit spanId (overrides trace context)
22
+ * @param explicitParentSpanId - Optional explicit parentSpanId (overrides trace context)
23
+ */
24
+ async function emitInvariant(options, explicitSpanId, explicitParentSpanId) {
25
+ const { name, passed, severity, details } = options;
26
+ const { bundle, spanId: contextSpanId } = (0, http_wrapper_1.getTraceContext)();
27
+ // Use explicit spanId if provided, otherwise use context
28
+ const spanId = explicitSpanId || contextSpanId;
29
+ if (process.env.DEBUG_HTTP_SENTINELS) {
30
+ console.log('[INVARIANTS] emitInvariant called:', {
31
+ name,
32
+ passed,
33
+ severity,
34
+ explicitSpanId,
35
+ contextSpanId,
36
+ finalSpanId: spanId,
37
+ hasBundle: !!bundle,
38
+ });
39
+ }
40
+ // If no trace context and no explicit spanId, silently skip (fail-open)
41
+ if (!bundle || !spanId) {
42
+ if (process.env.DEBUG_HTTP_SENTINELS) {
43
+ console.warn('[INVARIANTS] No trace context or spanId, skipping invariant');
44
+ }
45
+ return;
46
+ }
47
+ // Use explicit parentSpanId if provided, otherwise null
48
+ const parentSpanId = explicitParentSpanId !== undefined ? explicitParentSpanId : null;
49
+ try {
50
+ const event = {
51
+ type: "state.invariant",
52
+ timestamp: Date.now(),
53
+ spanId,
54
+ parentSpanId,
55
+ name,
56
+ passed,
57
+ severity,
58
+ ...(details && { details }),
59
+ };
60
+ if (process.env.DEBUG_HTTP_SENTINELS) {
61
+ console.log('[INVARIANTS] Writing invariant event:', event);
62
+ }
63
+ await bundle.writeEvent(event);
64
+ if (process.env.DEBUG_HTTP_SENTINELS) {
65
+ console.log('[INVARIANTS] Invariant event written successfully');
66
+ }
67
+ }
68
+ catch (error) {
69
+ // Fail-open: log error but don't throw
70
+ console.error(`[SDK] Failed to emit invariant event: ${error}`);
71
+ if (process.env.DEBUG_HTTP_SENTINELS) {
72
+ console.error('[INVARIANTS] Error details:', error);
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Assert that a value is not null/undefined
78
+ *
79
+ * @param path - JSONPath-like path to the value (e.g., "pricing.price")
80
+ * @param value - Value to check
81
+ * @param options - Optional details
82
+ */
83
+ async function assertNonNull(path, value, options) {
84
+ const passed = value !== null && value !== undefined;
85
+ if (!passed) {
86
+ await emitInvariant({
87
+ name: `${path}.non_null`,
88
+ passed: false,
89
+ severity: "error",
90
+ details: {
91
+ paths: [path],
92
+ expected: options?.expected || "non-null value",
93
+ observed: options?.observed || "null or undefined",
94
+ },
95
+ });
96
+ }
97
+ else {
98
+ // Also emit passed invariants for completeness (optional, can be filtered)
99
+ await emitInvariant({
100
+ name: `${path}.non_null`,
101
+ passed: true,
102
+ severity: "warn", // Lower severity for passed checks
103
+ details: {
104
+ paths: [path],
105
+ },
106
+ });
107
+ }
108
+ }
109
+ /**
110
+ * Assert that an object has all required keys
111
+ *
112
+ * @param keys - Array of required key names
113
+ * @param obj - Object to check
114
+ * @param path - Optional JSONPath-like path prefix (e.g., "response")
115
+ */
116
+ async function assertHasKeys(keys, obj, path) {
117
+ if (!obj || typeof obj !== "object") {
118
+ await emitInvariant({
119
+ name: path ? `${path}.has_keys` : "object.has_keys",
120
+ passed: false,
121
+ severity: "error",
122
+ details: {
123
+ paths: path ? [path] : undefined,
124
+ expected: `object with keys: ${keys.join(", ")}`,
125
+ observed: obj === null ? "null" : obj === undefined ? "undefined" : typeof obj,
126
+ },
127
+ });
128
+ return;
129
+ }
130
+ const missingKeys = keys.filter((key) => !(key in obj));
131
+ const passed = missingKeys.length === 0;
132
+ await emitInvariant({
133
+ name: path ? `${path}.has_keys` : "object.has_keys",
134
+ passed,
135
+ severity: passed ? "warn" : "error",
136
+ details: {
137
+ paths: path ? missingKeys.map((key) => `${path}.${key}`) : missingKeys,
138
+ expected: `object with keys: ${keys.join(", ")}`,
139
+ observed: passed
140
+ ? `object has all required keys`
141
+ : `missing keys: ${missingKeys.join(", ")}`,
142
+ },
143
+ });
144
+ }
145
+ /**
146
+ * Assert that a numeric value is within a range
147
+ *
148
+ * @param name - Name of the invariant (e.g., "pricing.price_range")
149
+ * @param value - Value to check
150
+ * @param min - Minimum value (inclusive)
151
+ * @param max - Maximum value (inclusive)
152
+ * @param path - Optional JSONPath-like path to the value
153
+ */
154
+ async function assertInRange(name, value, min, max, path) {
155
+ if (value === null || value === undefined) {
156
+ await emitInvariant({
157
+ name,
158
+ passed: false,
159
+ severity: "error",
160
+ details: {
161
+ paths: path ? [path] : undefined,
162
+ expected: `number in range [${min}, ${max}]`,
163
+ observed: value === null ? "null" : "undefined",
164
+ },
165
+ });
166
+ return;
167
+ }
168
+ if (typeof value !== "number" || isNaN(value)) {
169
+ await emitInvariant({
170
+ name,
171
+ passed: false,
172
+ severity: "error",
173
+ details: {
174
+ paths: path ? [path] : undefined,
175
+ expected: `number in range [${min}, ${max}]`,
176
+ observed: `not a number: ${typeof value}`,
177
+ },
178
+ });
179
+ return;
180
+ }
181
+ const passed = value >= min && value <= max;
182
+ await emitInvariant({
183
+ name,
184
+ passed,
185
+ severity: passed ? "warn" : "error",
186
+ details: {
187
+ paths: path ? [path] : undefined,
188
+ expected: `value in range [${min}, ${max}]`,
189
+ observed: passed ? `value ${value} is in range` : `value ${value} is out of range`,
190
+ },
191
+ });
192
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Multi-service edge builder
3
+ *
4
+ * Analyzes trace events to build edges between services for distributed tracing.
5
+ * An edge represents a call from one service to another.
6
+ */
7
+ export interface TraceEvent {
8
+ type: string;
9
+ timestamp: number;
10
+ traceId: string;
11
+ spanId: string;
12
+ parentSpanId: string | null;
13
+ serviceName: string;
14
+ method?: string;
15
+ url?: string;
16
+ urlTemplate?: string | null;
17
+ peerHost?: string | null;
18
+ peerService?: string | null;
19
+ statusCode?: number | null;
20
+ path?: string;
21
+ route?: string;
22
+ error?: {
23
+ name?: string;
24
+ message?: string;
25
+ stack?: string;
26
+ };
27
+ responseShapeDigest?: {
28
+ kind: string;
29
+ hash?: string;
30
+ nullPaths?: string[];
31
+ keys?: string[];
32
+ topLevelType?: string;
33
+ };
34
+ name?: string;
35
+ passed?: boolean;
36
+ severity?: "warn" | "error";
37
+ details?: {
38
+ paths?: string[];
39
+ expected?: string;
40
+ observed?: string;
41
+ };
42
+ }
43
+ export interface ServiceEdge {
44
+ traceId: string;
45
+ fromService: string;
46
+ fromSpanId: string;
47
+ fromOperation: string;
48
+ toService: string;
49
+ toSpanId: string;
50
+ toOperation: string;
51
+ statusCode: number | null;
52
+ timestampOut: number;
53
+ timestampIn: number;
54
+ }
55
+ /**
56
+ * Build service edges from trace events
57
+ *
58
+ * An edge is formed when:
59
+ * - outbound.type == "http.request.outbound"
60
+ * - inbound.type == "http.request.inbound"
61
+ * - inbound.traceId == outbound.traceId
62
+ * - inbound.parentSpanId == outbound.spanId
63
+ *
64
+ * @param events - Array of trace events (all from the same traceId)
65
+ * @returns Array of service edges, sorted by timestampOut
66
+ */
67
+ export declare function buildEdges(events: TraceEvent[]): ServiceEdge[];
68
+ /**
69
+ * Build edges from a traceId by loading events
70
+ *
71
+ * This is a convenience function that loads events from a trace directory
72
+ * and calls buildEdges. For now, this requires the caller to provide events.
73
+ * In the future, this could load from file system or storage.
74
+ *
75
+ * @param traceId - The trace ID
76
+ * @param events - Array of trace events for this traceId
77
+ * @returns Array of service edges
78
+ */
79
+ export declare function buildEdgesForTrace(traceId: string, events: TraceEvent[]): ServiceEdge[];
80
+ //# sourceMappingURL=multi-service-edge-builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"multi-service-edge-builder.d.ts","sourceRoot":"","sources":["../src/multi-service-edge-builder.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,UAAU;IACzB,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;IAEpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAE3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF,mBAAmB,CAAC,EAAE;QACpB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;IAEF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,OAAO,CAAC,EAAE;QACR,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AA6BD;;;;;;;;;;;GAWG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,CAuD9D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,WAAW,EAAE,CAIvF"}
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-service edge builder
4
+ *
5
+ * Analyzes trace events to build edges between services for distributed tracing.
6
+ * An edge represents a call from one service to another.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.buildEdges = buildEdges;
10
+ exports.buildEdgesForTrace = buildEdgesForTrace;
11
+ /**
12
+ * Build operation string for inbound event
13
+ */
14
+ function buildInboundOperation(event) {
15
+ const method = event.method || 'UNKNOWN';
16
+ const path = event.route || event.path || 'unknown';
17
+ return `${method} ${path}`;
18
+ }
19
+ /**
20
+ * Build operation string for outbound event
21
+ */
22
+ function buildOutboundOperation(event) {
23
+ const method = event.method || 'UNKNOWN';
24
+ // Prefer urlTemplate, fallback to peerHost, then "unknown"
25
+ if (event.urlTemplate) {
26
+ return `${method} ${event.urlTemplate}`;
27
+ }
28
+ if (event.peerHost) {
29
+ return `${method} ${event.peerHost}`;
30
+ }
31
+ return `${method} unknown`;
32
+ }
33
+ /**
34
+ * Build service edges from trace events
35
+ *
36
+ * An edge is formed when:
37
+ * - outbound.type == "http.request.outbound"
38
+ * - inbound.type == "http.request.inbound"
39
+ * - inbound.traceId == outbound.traceId
40
+ * - inbound.parentSpanId == outbound.spanId
41
+ *
42
+ * @param events - Array of trace events (all from the same traceId)
43
+ * @returns Array of service edges, sorted by timestampOut
44
+ */
45
+ function buildEdges(events) {
46
+ const edges = [];
47
+ // Index outbound events by spanId for fast lookup
48
+ const outboundBySpanId = new Map();
49
+ for (const event of events) {
50
+ if (event.type === 'http.request.outbound') {
51
+ outboundBySpanId.set(event.spanId, event);
52
+ }
53
+ }
54
+ // Find inbound events and match them with outbound events
55
+ for (const inboundEvent of events) {
56
+ if (inboundEvent.type !== 'http.request.inbound') {
57
+ continue;
58
+ }
59
+ // Check if this inbound event has a parentSpanId that matches an outbound event
60
+ if (!inboundEvent.parentSpanId) {
61
+ // Root span - no parent outbound event
62
+ continue;
63
+ }
64
+ const outboundEvent = outboundBySpanId.get(inboundEvent.parentSpanId);
65
+ if (!outboundEvent) {
66
+ // No matching outbound event found - downstream service not instrumented
67
+ continue;
68
+ }
69
+ // Verify traceIds match (should always be true if events are from same trace)
70
+ if (inboundEvent.traceId !== outboundEvent.traceId) {
71
+ continue;
72
+ }
73
+ // Build edge
74
+ const edge = {
75
+ traceId: inboundEvent.traceId,
76
+ fromService: outboundEvent.serviceName,
77
+ fromSpanId: outboundEvent.spanId,
78
+ fromOperation: buildOutboundOperation(outboundEvent),
79
+ toService: inboundEvent.serviceName,
80
+ toSpanId: inboundEvent.spanId,
81
+ toOperation: buildInboundOperation(inboundEvent),
82
+ statusCode: outboundEvent.statusCode ?? null,
83
+ timestampOut: outboundEvent.timestamp,
84
+ timestampIn: inboundEvent.timestamp,
85
+ };
86
+ edges.push(edge);
87
+ }
88
+ // Sort edges by timestampOut (chronological order)
89
+ edges.sort((a, b) => a.timestampOut - b.timestampOut);
90
+ return edges;
91
+ }
92
+ /**
93
+ * Build edges from a traceId by loading events
94
+ *
95
+ * This is a convenience function that loads events from a trace directory
96
+ * and calls buildEdges. For now, this requires the caller to provide events.
97
+ * In the future, this could load from file system or storage.
98
+ *
99
+ * @param traceId - The trace ID
100
+ * @param events - Array of trace events for this traceId
101
+ * @returns Array of service edges
102
+ */
103
+ function buildEdgesForTrace(traceId, events) {
104
+ // Filter events to only include those for this traceId
105
+ const traceEvents = events.filter(e => e.traceId === traceId);
106
+ return buildEdges(traceEvents);
107
+ }
@@ -0,0 +1,192 @@
1
+ import type { ReplayLogger } from "./replay-logger";
2
+ /**
3
+ * Replay modes for outbound call matching
4
+ */
5
+ export type ReplayMode = "strict" | "explain";
6
+ /**
7
+ * Structured failure for outbound request mismatch
8
+ */
9
+ export interface OutboundRequestMismatch {
10
+ type: "outbound_request_mismatch";
11
+ location: string;
12
+ expected: string;
13
+ actual: string;
14
+ hint: string;
15
+ callIndex: number;
16
+ diff: {
17
+ field: string;
18
+ expected: string | null;
19
+ actual: string | null;
20
+ }[];
21
+ }
22
+ /**
23
+ * Structured failure for unexpected outbound call (call occurred but no recorded match)
24
+ */
25
+ export interface UnexpectedOutboundCall {
26
+ type: "unexpected_outbound_call";
27
+ location: string;
28
+ expected: string;
29
+ actual: string;
30
+ hint: string;
31
+ callIndex: number;
32
+ service: string;
33
+ spanId: string | null;
34
+ call: OutboundCallSignature;
35
+ }
36
+ /**
37
+ * Structured failure for missing outbound call (recorded call never happened)
38
+ */
39
+ export interface MissingOutboundCall {
40
+ type: "missing_outbound_call";
41
+ location: string;
42
+ expected: string;
43
+ actual: string;
44
+ hint: string;
45
+ callIndex: number;
46
+ service: string;
47
+ spanId: string;
48
+ recordedCall: RecordedOutboundCall;
49
+ }
50
+ /**
51
+ * Union type for all outbound call failures
52
+ */
53
+ export type OutboundCallFailure = OutboundRequestMismatch | UnexpectedOutboundCall | MissingOutboundCall;
54
+ /**
55
+ * Divergence event emitted in explain mode
56
+ */
57
+ export interface OutboundDivergence {
58
+ type: "divergence";
59
+ severity: "warning" | "error";
60
+ callIndex: number;
61
+ message: string;
62
+ expected: RecordedOutboundCall | null;
63
+ actual: OutboundCallSignature;
64
+ recoverable: boolean;
65
+ }
66
+ /**
67
+ * Recorded outbound HTTP call from trace
68
+ */
69
+ export interface RecordedOutboundCall {
70
+ spanId: string;
71
+ method: string;
72
+ url: string;
73
+ normalizedUrl: string;
74
+ headers: Record<string, string>;
75
+ bodyHash: string | null;
76
+ response: {
77
+ statusCode: number;
78
+ headers: Record<string, string>;
79
+ bodyHash: string | null;
80
+ bodyBlob: string | null;
81
+ } | null;
82
+ }
83
+ /**
84
+ * Signature used to match an outbound call
85
+ */
86
+ export interface OutboundCallSignature {
87
+ method: string;
88
+ url: string;
89
+ headers: Record<string, string>;
90
+ bodyHash: string | null;
91
+ }
92
+ /**
93
+ * Normalize URL for matching: pathname + sorted query params
94
+ */
95
+ export declare function normalizeUrl(url: string): string;
96
+ /**
97
+ * Normalize headers for matching: lowercase keys, filter to matching headers
98
+ */
99
+ export declare function normalizeHeaders(headers: Record<string, string>): Record<string, string>;
100
+ /**
101
+ * Compute body hash from content
102
+ */
103
+ export declare function computeBodyHash(body: string | Buffer | object | null | undefined): string | null;
104
+ /**
105
+ * Compute match signature for comparison
106
+ */
107
+ export declare function computeMatchSignature(call: OutboundCallSignature): string;
108
+ /**
109
+ * Compare two calls and return differences
110
+ */
111
+ export declare function compareOutboundCalls(expected: RecordedOutboundCall, actual: OutboundCallSignature): {
112
+ matches: boolean;
113
+ diffs: {
114
+ field: string;
115
+ expected: string | null;
116
+ actual: string | null;
117
+ }[];
118
+ };
119
+ /**
120
+ * Ordered outbound call matcher for replay
121
+ */
122
+ export declare class OutboundMatcher {
123
+ private recordedCalls;
124
+ private currentIndex;
125
+ private mode;
126
+ private traceDir;
127
+ private divergences;
128
+ private logger;
129
+ constructor(recordedCalls: RecordedOutboundCall[], mode: ReplayMode, traceDir: string, logger?: ReplayLogger);
130
+ /**
131
+ * Set the logger for structured replay logging
132
+ */
133
+ setLogger(logger: ReplayLogger): void;
134
+ /**
135
+ * Get all recorded divergences (for explain mode)
136
+ */
137
+ getDivergences(): OutboundDivergence[];
138
+ /**
139
+ * Get current call index
140
+ */
141
+ getCurrentIndex(): number;
142
+ /**
143
+ * Get total recorded calls
144
+ */
145
+ getTotalCalls(): number;
146
+ /**
147
+ * Match the next outbound call against the recorded sequence
148
+ *
149
+ * In strict mode: throws OutboundRequestMismatch on any mismatch
150
+ * In explain mode: emits divergence and returns best-effort match
151
+ */
152
+ matchNext(actual: OutboundCallSignature): RecordedOutboundCall;
153
+ /**
154
+ * Check if all recorded calls have been matched
155
+ */
156
+ isComplete(): boolean;
157
+ /**
158
+ * Get summary of unmatched calls (calls that were recorded but not replayed)
159
+ */
160
+ getUnmatchedCalls(): RecordedOutboundCall[];
161
+ /**
162
+ * Finalize replay and check for missing outbound calls.
163
+ * Should be called at the end of replay to detect any recorded calls that never happened.
164
+ *
165
+ * In strict mode: throws MissingOutboundCall[] if any calls are missing
166
+ * In explain mode: emits divergences and returns the missing calls
167
+ *
168
+ * @returns Array of MissingOutboundCall failures (empty if all calls matched)
169
+ */
170
+ finalize(): MissingOutboundCall[];
171
+ }
172
+ /**
173
+ * Parse recorded outbound calls from trace events
174
+ */
175
+ export declare function parseRecordedOutboundCalls(events: any[]): RecordedOutboundCall[];
176
+ /**
177
+ * Format OutboundRequestMismatch for CLI output
178
+ */
179
+ export declare function formatOutboundMismatch(failure: OutboundRequestMismatch): string;
180
+ /**
181
+ * Format UnexpectedOutboundCall for CLI output
182
+ */
183
+ export declare function formatUnexpectedOutboundCall(failure: UnexpectedOutboundCall): string;
184
+ /**
185
+ * Format MissingOutboundCall for CLI output
186
+ */
187
+ export declare function formatMissingOutboundCall(failure: MissingOutboundCall): string;
188
+ /**
189
+ * Format any outbound call failure for CLI output
190
+ */
191
+ export declare function formatOutboundCallFailure(failure: OutboundCallFailure): string;
192
+ //# sourceMappingURL=outbound-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbound-matcher.d.ts","sourceRoot":"","sources":["../src/outbound-matcher.ts"],"names":[],"mappings":"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"}