@mastra/observability 1.0.0 → 1.1.0-alpha.1
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/CHANGELOG.md +36 -0
- package/dist/exporters/index.d.ts +1 -0
- package/dist/exporters/index.d.ts.map +1 -1
- package/dist/exporters/json.d.ts +386 -0
- package/dist/exporters/json.d.ts.map +1 -0
- package/dist/index.cjs +953 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +952 -92
- package/dist/index.js.map +1 -1
- package/dist/model-tracing.d.ts +6 -0
- package/dist/model-tracing.d.ts.map +1 -1
- package/dist/spans/base.d.ts +2 -0
- package/dist/spans/base.d.ts.map +1 -1
- package/dist/spans/serialization.d.ts +20 -0
- package/dist/spans/serialization.d.ts.map +1 -1
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -5,8 +5,12 @@ var error = require('@mastra/core/error');
|
|
|
5
5
|
var logger = require('@mastra/core/logger');
|
|
6
6
|
var observability = require('@mastra/core/observability');
|
|
7
7
|
var utils = require('@mastra/core/utils');
|
|
8
|
+
var promises = require('fs/promises');
|
|
9
|
+
var path = require('path');
|
|
10
|
+
var url = require('url');
|
|
8
11
|
var web = require('stream/web');
|
|
9
12
|
|
|
13
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
10
14
|
var __defProp = Object.defineProperty;
|
|
11
15
|
var __export = (target, all) => {
|
|
12
16
|
for (var name in all)
|
|
@@ -6065,6 +6069,822 @@ var TestExporter = class extends BaseExporter {
|
|
|
6065
6069
|
this.logger.info("TestExporter shutdown");
|
|
6066
6070
|
}
|
|
6067
6071
|
};
|
|
6072
|
+
var __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
6073
|
+
var __dirname$1 = path.dirname(__filename$1);
|
|
6074
|
+
var SNAPSHOTS_DIR = path.join(__dirname$1, "..", "__snapshots__");
|
|
6075
|
+
var JsonExporter = class extends BaseExporter {
|
|
6076
|
+
name = "json-exporter";
|
|
6077
|
+
/** All collected events */
|
|
6078
|
+
#events = [];
|
|
6079
|
+
/** Per-span state tracking */
|
|
6080
|
+
#spanStates = /* @__PURE__ */ new Map();
|
|
6081
|
+
/** Logs for debugging */
|
|
6082
|
+
#logs = [];
|
|
6083
|
+
/** Configuration */
|
|
6084
|
+
#config;
|
|
6085
|
+
constructor(config = {}) {
|
|
6086
|
+
super(config);
|
|
6087
|
+
this.#config = {
|
|
6088
|
+
validateLifecycle: true,
|
|
6089
|
+
storeLogs: true,
|
|
6090
|
+
jsonIndent: 2,
|
|
6091
|
+
...config
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
/**
|
|
6095
|
+
* Process incoming tracing events with lifecycle tracking
|
|
6096
|
+
*/
|
|
6097
|
+
async _exportTracingEvent(event) {
|
|
6098
|
+
const span = event.exportedSpan;
|
|
6099
|
+
const spanId = span.id;
|
|
6100
|
+
const logMessage = `[JsonExporter] ${event.type}: ${span.type} "${span.name}" (entity: ${span.entityName ?? span.entityId ?? "unknown"}, trace: ${span.traceId.slice(-8)}, span: ${spanId.slice(-8)})`;
|
|
6101
|
+
if (this.#config.storeLogs) {
|
|
6102
|
+
this.#logs.push(logMessage);
|
|
6103
|
+
}
|
|
6104
|
+
const state = this.#spanStates.get(spanId) || {
|
|
6105
|
+
hasStart: false,
|
|
6106
|
+
hasEnd: false,
|
|
6107
|
+
hasUpdate: false,
|
|
6108
|
+
events: []
|
|
6109
|
+
};
|
|
6110
|
+
if (this.#config.validateLifecycle) {
|
|
6111
|
+
this.#validateLifecycle(event, state, spanId);
|
|
6112
|
+
}
|
|
6113
|
+
if (event.type === observability.TracingEventType.SPAN_STARTED) {
|
|
6114
|
+
state.hasStart = true;
|
|
6115
|
+
} else if (event.type === observability.TracingEventType.SPAN_ENDED) {
|
|
6116
|
+
state.hasEnd = true;
|
|
6117
|
+
if (span.isEvent) {
|
|
6118
|
+
state.isEventSpan = true;
|
|
6119
|
+
}
|
|
6120
|
+
} else if (event.type === observability.TracingEventType.SPAN_UPDATED) {
|
|
6121
|
+
state.hasUpdate = true;
|
|
6122
|
+
}
|
|
6123
|
+
state.events.push(event);
|
|
6124
|
+
this.#spanStates.set(spanId, state);
|
|
6125
|
+
this.#events.push(event);
|
|
6126
|
+
}
|
|
6127
|
+
/**
|
|
6128
|
+
* Validate span lifecycle rules
|
|
6129
|
+
*/
|
|
6130
|
+
#validateLifecycle(event, state, spanId) {
|
|
6131
|
+
const span = event.exportedSpan;
|
|
6132
|
+
if (event.type === observability.TracingEventType.SPAN_STARTED) {
|
|
6133
|
+
if (state.hasStart) {
|
|
6134
|
+
this.logger.warn(`Span ${spanId} (${span.type} "${span.name}") started twice`);
|
|
6135
|
+
}
|
|
6136
|
+
} else if (event.type === observability.TracingEventType.SPAN_ENDED) {
|
|
6137
|
+
if (span.isEvent) {
|
|
6138
|
+
if (state.hasStart) {
|
|
6139
|
+
this.logger.warn(`Event span ${spanId} (${span.type} "${span.name}") incorrectly received SPAN_STARTED`);
|
|
6140
|
+
}
|
|
6141
|
+
if (state.hasUpdate) {
|
|
6142
|
+
this.logger.warn(`Event span ${spanId} (${span.type} "${span.name}") incorrectly received SPAN_UPDATED`);
|
|
6143
|
+
}
|
|
6144
|
+
} else {
|
|
6145
|
+
if (!state.hasStart) {
|
|
6146
|
+
this.logger.warn(`Normal span ${spanId} (${span.type} "${span.name}") ended without starting`);
|
|
6147
|
+
}
|
|
6148
|
+
}
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
// ============================================================================
|
|
6152
|
+
// Query Methods
|
|
6153
|
+
// ============================================================================
|
|
6154
|
+
/**
|
|
6155
|
+
* Get all collected events
|
|
6156
|
+
*/
|
|
6157
|
+
get events() {
|
|
6158
|
+
return [...this.#events];
|
|
6159
|
+
}
|
|
6160
|
+
/**
|
|
6161
|
+
* Get completed spans by SpanType (e.g., 'agent_run', 'tool_call')
|
|
6162
|
+
*
|
|
6163
|
+
* @param type - The SpanType to filter by
|
|
6164
|
+
* @returns Array of completed exported spans of the specified type
|
|
6165
|
+
*/
|
|
6166
|
+
getSpansByType(type) {
|
|
6167
|
+
return Array.from(this.#spanStates.values()).filter((state) => {
|
|
6168
|
+
if (!state.hasEnd) return false;
|
|
6169
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6170
|
+
return endEvent?.exportedSpan.type === type;
|
|
6171
|
+
}).map((state) => {
|
|
6172
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6173
|
+
return endEvent?.exportedSpan;
|
|
6174
|
+
}).filter((span) => span !== void 0);
|
|
6175
|
+
}
|
|
6176
|
+
/**
|
|
6177
|
+
* Get events by TracingEventType (SPAN_STARTED, SPAN_UPDATED, SPAN_ENDED)
|
|
6178
|
+
*
|
|
6179
|
+
* @param type - The TracingEventType to filter by
|
|
6180
|
+
* @returns Array of events of the specified type
|
|
6181
|
+
*/
|
|
6182
|
+
getByEventType(type) {
|
|
6183
|
+
return this.#events.filter((e) => e.type === type);
|
|
6184
|
+
}
|
|
6185
|
+
/**
|
|
6186
|
+
* Get all events and spans for a specific trace
|
|
6187
|
+
*
|
|
6188
|
+
* @param traceId - The trace ID to filter by
|
|
6189
|
+
* @returns Object containing events and final spans for the trace
|
|
6190
|
+
*/
|
|
6191
|
+
getByTraceId(traceId) {
|
|
6192
|
+
const events = this.#events.filter((e) => e.exportedSpan.traceId === traceId);
|
|
6193
|
+
const spans = this.#getUniqueSpansFromEvents(events);
|
|
6194
|
+
return { events, spans };
|
|
6195
|
+
}
|
|
6196
|
+
/**
|
|
6197
|
+
* Get all events for a specific span
|
|
6198
|
+
*
|
|
6199
|
+
* @param spanId - The span ID to filter by
|
|
6200
|
+
* @returns Object containing events and final span state
|
|
6201
|
+
*/
|
|
6202
|
+
getBySpanId(spanId) {
|
|
6203
|
+
const state = this.#spanStates.get(spanId);
|
|
6204
|
+
if (!state) {
|
|
6205
|
+
return { events: [], span: void 0, state: void 0 };
|
|
6206
|
+
}
|
|
6207
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6208
|
+
const span = endEvent?.exportedSpan ?? state.events[state.events.length - 1]?.exportedSpan;
|
|
6209
|
+
return { events: state.events, span, state };
|
|
6210
|
+
}
|
|
6211
|
+
/**
|
|
6212
|
+
* Get all unique spans (returns the final state of each span)
|
|
6213
|
+
*/
|
|
6214
|
+
getAllSpans() {
|
|
6215
|
+
return Array.from(this.#spanStates.values()).map((state) => {
|
|
6216
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6217
|
+
return endEvent?.exportedSpan ?? state.events[state.events.length - 1]?.exportedSpan;
|
|
6218
|
+
}).filter((span) => span !== void 0);
|
|
6219
|
+
}
|
|
6220
|
+
/**
|
|
6221
|
+
* Get only completed spans (those that have received SPAN_ENDED)
|
|
6222
|
+
*/
|
|
6223
|
+
getCompletedSpans() {
|
|
6224
|
+
return Array.from(this.#spanStates.values()).filter((state) => state.hasEnd).map((state) => {
|
|
6225
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6226
|
+
return endEvent.exportedSpan;
|
|
6227
|
+
});
|
|
6228
|
+
}
|
|
6229
|
+
/**
|
|
6230
|
+
* Get root spans only (spans with no parent)
|
|
6231
|
+
*/
|
|
6232
|
+
getRootSpans() {
|
|
6233
|
+
return this.getAllSpans().filter((span) => span.isRootSpan);
|
|
6234
|
+
}
|
|
6235
|
+
/**
|
|
6236
|
+
* Get incomplete spans (started but not yet ended)
|
|
6237
|
+
*/
|
|
6238
|
+
getIncompleteSpans() {
|
|
6239
|
+
return Array.from(this.#spanStates.entries()).filter(([_, state]) => !state.hasEnd).map(([spanId, state]) => ({
|
|
6240
|
+
spanId,
|
|
6241
|
+
span: state.events[0]?.exportedSpan,
|
|
6242
|
+
state: {
|
|
6243
|
+
hasStart: state.hasStart,
|
|
6244
|
+
hasUpdate: state.hasUpdate,
|
|
6245
|
+
hasEnd: state.hasEnd
|
|
6246
|
+
}
|
|
6247
|
+
}));
|
|
6248
|
+
}
|
|
6249
|
+
/**
|
|
6250
|
+
* Get unique trace IDs from all collected spans
|
|
6251
|
+
*/
|
|
6252
|
+
getTraceIds() {
|
|
6253
|
+
const traceIds = /* @__PURE__ */ new Set();
|
|
6254
|
+
for (const event of this.#events) {
|
|
6255
|
+
traceIds.add(event.exportedSpan.traceId);
|
|
6256
|
+
}
|
|
6257
|
+
return Array.from(traceIds);
|
|
6258
|
+
}
|
|
6259
|
+
// ============================================================================
|
|
6260
|
+
// Statistics
|
|
6261
|
+
// ============================================================================
|
|
6262
|
+
/**
|
|
6263
|
+
* Get comprehensive statistics about collected spans
|
|
6264
|
+
*/
|
|
6265
|
+
getStatistics() {
|
|
6266
|
+
const bySpanType = {};
|
|
6267
|
+
let completedSpans = 0;
|
|
6268
|
+
let incompleteSpans = 0;
|
|
6269
|
+
for (const state of this.#spanStates.values()) {
|
|
6270
|
+
if (state.hasEnd) {
|
|
6271
|
+
completedSpans++;
|
|
6272
|
+
const endEvent = state.events.find((e) => e.type === observability.TracingEventType.SPAN_ENDED);
|
|
6273
|
+
const spanType = endEvent?.exportedSpan.type;
|
|
6274
|
+
if (spanType) {
|
|
6275
|
+
bySpanType[spanType] = (bySpanType[spanType] || 0) + 1;
|
|
6276
|
+
}
|
|
6277
|
+
} else {
|
|
6278
|
+
incompleteSpans++;
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
return {
|
|
6282
|
+
totalEvents: this.#events.length,
|
|
6283
|
+
totalSpans: this.#spanStates.size,
|
|
6284
|
+
totalTraces: this.getTraceIds().length,
|
|
6285
|
+
completedSpans,
|
|
6286
|
+
incompleteSpans,
|
|
6287
|
+
byEventType: {
|
|
6288
|
+
started: this.#events.filter((e) => e.type === observability.TracingEventType.SPAN_STARTED).length,
|
|
6289
|
+
updated: this.#events.filter((e) => e.type === observability.TracingEventType.SPAN_UPDATED).length,
|
|
6290
|
+
ended: this.#events.filter((e) => e.type === observability.TracingEventType.SPAN_ENDED).length
|
|
6291
|
+
},
|
|
6292
|
+
bySpanType
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
// ============================================================================
|
|
6296
|
+
// JSON Output
|
|
6297
|
+
// ============================================================================
|
|
6298
|
+
/**
|
|
6299
|
+
* Serialize all collected data to JSON string
|
|
6300
|
+
*
|
|
6301
|
+
* @param options - Serialization options
|
|
6302
|
+
* @returns JSON string of all collected data
|
|
6303
|
+
*/
|
|
6304
|
+
toJSON(options) {
|
|
6305
|
+
const indent = options?.indent ?? this.#config.jsonIndent;
|
|
6306
|
+
const includeEvents = options?.includeEvents ?? true;
|
|
6307
|
+
const includeStats = options?.includeStats ?? true;
|
|
6308
|
+
const data = {
|
|
6309
|
+
spans: this.getAllSpans()
|
|
6310
|
+
};
|
|
6311
|
+
if (includeEvents) {
|
|
6312
|
+
data.events = this.#events;
|
|
6313
|
+
}
|
|
6314
|
+
if (includeStats) {
|
|
6315
|
+
data.statistics = this.getStatistics();
|
|
6316
|
+
}
|
|
6317
|
+
return JSON.stringify(data, this.#jsonReplacer, indent);
|
|
6318
|
+
}
|
|
6319
|
+
/**
|
|
6320
|
+
* Build a tree structure from spans, nesting children under their parents
|
|
6321
|
+
*
|
|
6322
|
+
* @returns Array of root span tree nodes (spans with no parent)
|
|
6323
|
+
*/
|
|
6324
|
+
buildSpanTree() {
|
|
6325
|
+
const spans = this.getAllSpans();
|
|
6326
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
6327
|
+
const roots = [];
|
|
6328
|
+
for (const span of spans) {
|
|
6329
|
+
nodeMap.set(span.id, { span, children: [] });
|
|
6330
|
+
}
|
|
6331
|
+
for (const span of spans) {
|
|
6332
|
+
const node = nodeMap.get(span.id);
|
|
6333
|
+
if (span.parentSpanId && nodeMap.has(span.parentSpanId)) {
|
|
6334
|
+
nodeMap.get(span.parentSpanId).children.push(node);
|
|
6335
|
+
} else {
|
|
6336
|
+
roots.push(node);
|
|
6337
|
+
}
|
|
6338
|
+
}
|
|
6339
|
+
const sortChildren = (node) => {
|
|
6340
|
+
node.children.sort((a, b) => new Date(a.span.startTime).getTime() - new Date(b.span.startTime).getTime());
|
|
6341
|
+
node.children.forEach(sortChildren);
|
|
6342
|
+
};
|
|
6343
|
+
roots.forEach(sortChildren);
|
|
6344
|
+
return roots;
|
|
6345
|
+
}
|
|
6346
|
+
/**
|
|
6347
|
+
* Serialize spans as a tree structure to JSON string
|
|
6348
|
+
*
|
|
6349
|
+
* @param options - Serialization options
|
|
6350
|
+
* @returns JSON string with spans nested in tree format
|
|
6351
|
+
*/
|
|
6352
|
+
toTreeJSON(options) {
|
|
6353
|
+
const indent = options?.indent ?? this.#config.jsonIndent;
|
|
6354
|
+
const includeStats = options?.includeStats ?? true;
|
|
6355
|
+
const data = {
|
|
6356
|
+
tree: this.buildSpanTree()
|
|
6357
|
+
};
|
|
6358
|
+
if (includeStats) {
|
|
6359
|
+
data.statistics = this.getStatistics();
|
|
6360
|
+
}
|
|
6361
|
+
return JSON.stringify(data, this.#jsonReplacer, indent);
|
|
6362
|
+
}
|
|
6363
|
+
/**
|
|
6364
|
+
* Build a normalized tree structure suitable for snapshot testing.
|
|
6365
|
+
*
|
|
6366
|
+
* Normalizations applied:
|
|
6367
|
+
* - Span IDs replaced with stable placeholders (<span-1>, <span-2>, etc.)
|
|
6368
|
+
* - Trace IDs replaced with stable placeholders (<trace-1>, <trace-2>, etc.)
|
|
6369
|
+
* - parentSpanId replaced with normalized parent ID
|
|
6370
|
+
* - Timestamps replaced with durationMs (or null if not ended)
|
|
6371
|
+
* - Empty children arrays are omitted
|
|
6372
|
+
*
|
|
6373
|
+
* @returns Array of normalized root tree nodes
|
|
6374
|
+
*/
|
|
6375
|
+
buildNormalizedTree() {
|
|
6376
|
+
const tree = this.buildSpanTree();
|
|
6377
|
+
const spanIdMap = /* @__PURE__ */ new Map();
|
|
6378
|
+
const traceIdMap = /* @__PURE__ */ new Map();
|
|
6379
|
+
const uuidMapsByKey = /* @__PURE__ */ new Map();
|
|
6380
|
+
const uuidCountersByKey = /* @__PURE__ */ new Map();
|
|
6381
|
+
let spanIdCounter = 1;
|
|
6382
|
+
let traceIdCounter = 1;
|
|
6383
|
+
const uuidRegex2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6384
|
+
const hexId32Regex = /^[0-9a-f]{32}$/i;
|
|
6385
|
+
const prefixedUuidRegex = /^([a-z_]+)_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
6386
|
+
const embeddedPrefixedUuidRegex = /([a-z_]+)_([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/gi;
|
|
6387
|
+
const normalizeUuid = (uuid, key) => {
|
|
6388
|
+
if (!uuidMapsByKey.has(key)) {
|
|
6389
|
+
uuidMapsByKey.set(key, /* @__PURE__ */ new Map());
|
|
6390
|
+
uuidCountersByKey.set(key, 1);
|
|
6391
|
+
}
|
|
6392
|
+
const keyMap = uuidMapsByKey.get(key);
|
|
6393
|
+
if (!keyMap.has(uuid)) {
|
|
6394
|
+
const counter = uuidCountersByKey.get(key);
|
|
6395
|
+
keyMap.set(uuid, `<${key}-${counter}>`);
|
|
6396
|
+
uuidCountersByKey.set(key, counter + 1);
|
|
6397
|
+
}
|
|
6398
|
+
return keyMap.get(uuid);
|
|
6399
|
+
};
|
|
6400
|
+
const normalizeValue = (value, key) => {
|
|
6401
|
+
if (value instanceof Date) {
|
|
6402
|
+
return "<date>";
|
|
6403
|
+
}
|
|
6404
|
+
if (typeof value === "string") {
|
|
6405
|
+
if (key === "traceId" && (uuidRegex2.test(value) || hexId32Regex.test(value))) {
|
|
6406
|
+
if (!traceIdMap.has(value)) {
|
|
6407
|
+
traceIdMap.set(value, `<trace-${traceIdCounter++}>`);
|
|
6408
|
+
}
|
|
6409
|
+
return traceIdMap.get(value);
|
|
6410
|
+
}
|
|
6411
|
+
if (uuidRegex2.test(value)) {
|
|
6412
|
+
return normalizeUuid(value, key ?? "uuid");
|
|
6413
|
+
}
|
|
6414
|
+
const prefixMatch = prefixedUuidRegex.exec(value);
|
|
6415
|
+
if (prefixMatch && prefixMatch[1] && prefixMatch[2]) {
|
|
6416
|
+
const prefix = prefixMatch[1];
|
|
6417
|
+
const uuid = prefixMatch[2];
|
|
6418
|
+
return `${prefix}_${normalizeUuid(uuid, prefix)}`;
|
|
6419
|
+
}
|
|
6420
|
+
if (embeddedPrefixedUuidRegex.test(value)) {
|
|
6421
|
+
embeddedPrefixedUuidRegex.lastIndex = 0;
|
|
6422
|
+
return value.replace(embeddedPrefixedUuidRegex, (_match, prefix, uuid) => {
|
|
6423
|
+
return `${prefix}_${normalizeUuid(uuid, prefix)}`;
|
|
6424
|
+
});
|
|
6425
|
+
}
|
|
6426
|
+
}
|
|
6427
|
+
if (Array.isArray(value)) {
|
|
6428
|
+
return value.map((v) => normalizeValue(v, key));
|
|
6429
|
+
}
|
|
6430
|
+
if (value && typeof value === "object") {
|
|
6431
|
+
const normalized = {};
|
|
6432
|
+
for (const [k, v] of Object.entries(value)) {
|
|
6433
|
+
normalized[k] = normalizeValue(v, k);
|
|
6434
|
+
}
|
|
6435
|
+
return normalized;
|
|
6436
|
+
}
|
|
6437
|
+
return value;
|
|
6438
|
+
};
|
|
6439
|
+
const assignIds = (nodes) => {
|
|
6440
|
+
for (const node of nodes) {
|
|
6441
|
+
spanIdMap.set(node.span.id, `<span-${spanIdCounter++}>`);
|
|
6442
|
+
if (!traceIdMap.has(node.span.traceId)) {
|
|
6443
|
+
traceIdMap.set(node.span.traceId, `<trace-${traceIdCounter++}>`);
|
|
6444
|
+
}
|
|
6445
|
+
assignIds(node.children);
|
|
6446
|
+
}
|
|
6447
|
+
};
|
|
6448
|
+
assignIds(tree);
|
|
6449
|
+
const normalizeNode = (node) => {
|
|
6450
|
+
const span = node.span;
|
|
6451
|
+
const completed = span.endTime !== void 0 && span.endTime !== null;
|
|
6452
|
+
const normalizedSpan = {
|
|
6453
|
+
id: spanIdMap.get(span.id),
|
|
6454
|
+
traceId: traceIdMap.get(span.traceId),
|
|
6455
|
+
name: normalizeValue(span.name, "name"),
|
|
6456
|
+
type: span.type,
|
|
6457
|
+
completed,
|
|
6458
|
+
isEvent: span.isEvent,
|
|
6459
|
+
isRootSpan: span.isRootSpan
|
|
6460
|
+
};
|
|
6461
|
+
if (span.parentSpanId && spanIdMap.has(span.parentSpanId)) {
|
|
6462
|
+
normalizedSpan.parentId = spanIdMap.get(span.parentSpanId);
|
|
6463
|
+
}
|
|
6464
|
+
if (span.entityType) {
|
|
6465
|
+
normalizedSpan.entityType = span.entityType;
|
|
6466
|
+
}
|
|
6467
|
+
if (span.entityId) {
|
|
6468
|
+
normalizedSpan.entityId = normalizeValue(span.entityId, "entityId");
|
|
6469
|
+
}
|
|
6470
|
+
if (span.attributes && Object.keys(span.attributes).length > 0) {
|
|
6471
|
+
normalizedSpan.attributes = normalizeValue(span.attributes);
|
|
6472
|
+
}
|
|
6473
|
+
if (span.metadata && Object.keys(span.metadata).length > 0) {
|
|
6474
|
+
normalizedSpan.metadata = normalizeValue(span.metadata);
|
|
6475
|
+
}
|
|
6476
|
+
if (span.input !== void 0) {
|
|
6477
|
+
normalizedSpan.input = normalizeValue(span.input);
|
|
6478
|
+
}
|
|
6479
|
+
if (span.output !== void 0) {
|
|
6480
|
+
normalizedSpan.output = normalizeValue(span.output);
|
|
6481
|
+
}
|
|
6482
|
+
if (span.errorInfo) {
|
|
6483
|
+
normalizedSpan.errorInfo = span.errorInfo;
|
|
6484
|
+
}
|
|
6485
|
+
if (span.tags && span.tags.length > 0) {
|
|
6486
|
+
normalizedSpan.tags = span.tags;
|
|
6487
|
+
}
|
|
6488
|
+
const result = { span: normalizedSpan };
|
|
6489
|
+
if (node.children.length > 0) {
|
|
6490
|
+
result.children = node.children.map(normalizeNode);
|
|
6491
|
+
}
|
|
6492
|
+
return result;
|
|
6493
|
+
};
|
|
6494
|
+
return tree.map(normalizeNode);
|
|
6495
|
+
}
|
|
6496
|
+
/**
|
|
6497
|
+
* Generate an ASCII tree structure graph for debugging.
|
|
6498
|
+
* Shows span type and name in a hierarchical format.
|
|
6499
|
+
*
|
|
6500
|
+
* @param nodes - Normalized tree nodes (defaults to current normalized tree)
|
|
6501
|
+
* @returns Array of strings representing the tree structure
|
|
6502
|
+
*
|
|
6503
|
+
* @example
|
|
6504
|
+
* ```
|
|
6505
|
+
* agent_run: "agent run: 'test-agent'"
|
|
6506
|
+
* ├── processor_run: "input processor: validator"
|
|
6507
|
+
* │ └── agent_run: "agent run: 'validator-agent'"
|
|
6508
|
+
* └── model_generation: "llm: 'mock-model-id'"
|
|
6509
|
+
* ```
|
|
6510
|
+
*/
|
|
6511
|
+
generateStructureGraph(nodes) {
|
|
6512
|
+
const tree = nodes ?? this.buildNormalizedTree();
|
|
6513
|
+
const lines = [];
|
|
6514
|
+
const buildLines = (node, prefix, isLast, isRoot) => {
|
|
6515
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
6516
|
+
const line = `${prefix}${connector}${node.span.type}: "${node.span.name}"`;
|
|
6517
|
+
lines.push(line);
|
|
6518
|
+
const children = node.children ?? [];
|
|
6519
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
6520
|
+
children.forEach((child, index) => {
|
|
6521
|
+
const childIsLast = index === children.length - 1;
|
|
6522
|
+
buildLines(child, childPrefix, childIsLast, false);
|
|
6523
|
+
});
|
|
6524
|
+
};
|
|
6525
|
+
tree.forEach((rootNode, index) => {
|
|
6526
|
+
if (index > 0) {
|
|
6527
|
+
lines.push("");
|
|
6528
|
+
}
|
|
6529
|
+
buildLines(rootNode, "", true, true);
|
|
6530
|
+
});
|
|
6531
|
+
return lines;
|
|
6532
|
+
}
|
|
6533
|
+
/**
|
|
6534
|
+
* Serialize spans as a normalized tree structure for snapshot testing.
|
|
6535
|
+
* Includes a __structure__ field with an ASCII tree graph for readability.
|
|
6536
|
+
*
|
|
6537
|
+
* @param options - Serialization options
|
|
6538
|
+
* @returns JSON string with normalized spans in tree format
|
|
6539
|
+
*/
|
|
6540
|
+
toNormalizedTreeJSON(options) {
|
|
6541
|
+
const indent = options?.indent ?? this.#config.jsonIndent;
|
|
6542
|
+
const includeStructure = options?.includeStructure ?? true;
|
|
6543
|
+
const normalizedTree = this.buildNormalizedTree();
|
|
6544
|
+
if (includeStructure) {
|
|
6545
|
+
const structureGraph = this.generateStructureGraph(normalizedTree);
|
|
6546
|
+
const data = {
|
|
6547
|
+
__structure__: structureGraph,
|
|
6548
|
+
spans: normalizedTree
|
|
6549
|
+
};
|
|
6550
|
+
return JSON.stringify(data, null, indent);
|
|
6551
|
+
}
|
|
6552
|
+
return JSON.stringify(normalizedTree, null, indent);
|
|
6553
|
+
}
|
|
6554
|
+
/**
|
|
6555
|
+
* Write collected data to a JSON file
|
|
6556
|
+
*
|
|
6557
|
+
* @param filePath - Path to write the JSON file
|
|
6558
|
+
* @param options - Serialization options
|
|
6559
|
+
*/
|
|
6560
|
+
async writeToFile(filePath, options) {
|
|
6561
|
+
const format = options?.format ?? "flat";
|
|
6562
|
+
let json;
|
|
6563
|
+
if (format === "normalized") {
|
|
6564
|
+
json = this.toNormalizedTreeJSON({ indent: options?.indent });
|
|
6565
|
+
} else if (format === "tree") {
|
|
6566
|
+
json = this.toTreeJSON({ indent: options?.indent, includeStats: options?.includeStats });
|
|
6567
|
+
} else {
|
|
6568
|
+
json = this.toJSON(options);
|
|
6569
|
+
}
|
|
6570
|
+
await promises.writeFile(filePath, json, "utf-8");
|
|
6571
|
+
this.logger.info(`JsonExporter: wrote ${this.#events.length} events to ${filePath}`);
|
|
6572
|
+
}
|
|
6573
|
+
/**
|
|
6574
|
+
* Assert that the current normalized tree matches a snapshot file.
|
|
6575
|
+
* Throws an error with a diff if they don't match.
|
|
6576
|
+
*
|
|
6577
|
+
* The snapshot format includes:
|
|
6578
|
+
* - `__structure__`: ASCII tree graph (compared first for quick validation)
|
|
6579
|
+
* - `spans`: The normalized span tree (detailed comparison)
|
|
6580
|
+
*
|
|
6581
|
+
* Supports special markers in the snapshot:
|
|
6582
|
+
* - `{"__or__": ["value1", "value2"]}` - matches if actual equals any listed value
|
|
6583
|
+
* - `{"__any__": "string"}` - matches any string value
|
|
6584
|
+
* - `{"__any__": "number"}` - matches any number value
|
|
6585
|
+
* - `{"__any__": "boolean"}` - matches any boolean value
|
|
6586
|
+
* - `{"__any__": "object"}` - matches any object value
|
|
6587
|
+
* - `{"__any__": "array"}` - matches any array value
|
|
6588
|
+
* - `{"__any__": true}` - matches any non-null/undefined value
|
|
6589
|
+
*
|
|
6590
|
+
* Environment variables:
|
|
6591
|
+
* Use `{ updateSnapshot: true }` option to update the snapshot instead of comparing
|
|
6592
|
+
*
|
|
6593
|
+
* @param snapshotName - Name of the snapshot file (resolved relative to __snapshots__ directory)
|
|
6594
|
+
* @param options - Options for snapshot comparison
|
|
6595
|
+
* @param options.updateSnapshot - If true, update the snapshot file instead of comparing
|
|
6596
|
+
* @throws Error if the snapshot doesn't match (and updateSnapshot is false)
|
|
6597
|
+
*/
|
|
6598
|
+
async assertMatchesSnapshot(snapshotName, options) {
|
|
6599
|
+
const snapshotPath = path.join(SNAPSHOTS_DIR, snapshotName);
|
|
6600
|
+
const normalizedTree = this.buildNormalizedTree();
|
|
6601
|
+
const structureGraph = this.generateStructureGraph(normalizedTree);
|
|
6602
|
+
const currentData = {
|
|
6603
|
+
__structure__: structureGraph,
|
|
6604
|
+
spans: normalizedTree
|
|
6605
|
+
};
|
|
6606
|
+
const currentJson = JSON.stringify(currentData, null, this.#config.jsonIndent);
|
|
6607
|
+
const shouldUpdate = options?.updateSnapshot;
|
|
6608
|
+
if (shouldUpdate) {
|
|
6609
|
+
await promises.writeFile(snapshotPath, currentJson, "utf-8");
|
|
6610
|
+
this.logger.info(`JsonExporter: updated snapshot ${snapshotPath}`);
|
|
6611
|
+
return;
|
|
6612
|
+
}
|
|
6613
|
+
let snapshotData;
|
|
6614
|
+
try {
|
|
6615
|
+
const snapshotContent = await promises.readFile(snapshotPath, "utf-8");
|
|
6616
|
+
snapshotData = JSON.parse(snapshotContent);
|
|
6617
|
+
} catch {
|
|
6618
|
+
throw new Error(`Snapshot file not found: ${snapshotPath}
|
|
6619
|
+
Run with { updateSnapshot: true } to create it.`);
|
|
6620
|
+
}
|
|
6621
|
+
let expectedSpans;
|
|
6622
|
+
let expectedStructure;
|
|
6623
|
+
if (Array.isArray(snapshotData)) {
|
|
6624
|
+
expectedSpans = snapshotData;
|
|
6625
|
+
} else if (snapshotData && typeof snapshotData === "object" && "spans" in snapshotData) {
|
|
6626
|
+
expectedSpans = snapshotData.spans;
|
|
6627
|
+
expectedStructure = snapshotData.__structure__;
|
|
6628
|
+
} else {
|
|
6629
|
+
throw new Error(
|
|
6630
|
+
`Invalid snapshot format in ${snapshotPath}.
|
|
6631
|
+
Expected an array or object with 'spans' property.`
|
|
6632
|
+
);
|
|
6633
|
+
}
|
|
6634
|
+
if (expectedStructure) {
|
|
6635
|
+
const structureMismatches = this.#compareStructure(structureGraph, expectedStructure);
|
|
6636
|
+
if (structureMismatches.length > 0) {
|
|
6637
|
+
throw new Error(
|
|
6638
|
+
`Structure mismatch in snapshot:
|
|
6639
|
+
|
|
6640
|
+
Expected:
|
|
6641
|
+
${expectedStructure.join("\n")}
|
|
6642
|
+
|
|
6643
|
+
Actual:
|
|
6644
|
+
${structureGraph.join("\n")}
|
|
6645
|
+
|
|
6646
|
+
Differences:
|
|
6647
|
+
${structureMismatches.join("\n")}
|
|
6648
|
+
|
|
6649
|
+
Snapshot: ${snapshotPath}
|
|
6650
|
+
Run with { updateSnapshot: true } to update.`
|
|
6651
|
+
);
|
|
6652
|
+
}
|
|
6653
|
+
}
|
|
6654
|
+
const mismatches = [];
|
|
6655
|
+
this.#deepCompareWithMarkers(normalizedTree, expectedSpans, "$.spans", mismatches);
|
|
6656
|
+
if (mismatches.length > 0) {
|
|
6657
|
+
const mismatchDetails = mismatches.map(
|
|
6658
|
+
(m, i) => `${i + 1}. ${m.path}
|
|
6659
|
+
Expected: ${JSON.stringify(m.expected)}
|
|
6660
|
+
Actual: ${JSON.stringify(m.actual)}`
|
|
6661
|
+
).join("\n\n");
|
|
6662
|
+
throw new Error(
|
|
6663
|
+
`Snapshot has ${mismatches.length} mismatch${mismatches.length > 1 ? "es" : ""}:
|
|
6664
|
+
|
|
6665
|
+
${mismatchDetails}
|
|
6666
|
+
|
|
6667
|
+
Snapshot: ${snapshotPath}
|
|
6668
|
+
Run with { updateSnapshot: true } to update.`
|
|
6669
|
+
);
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
/**
|
|
6673
|
+
* Compare two structure graphs and return differences
|
|
6674
|
+
*/
|
|
6675
|
+
#compareStructure(actual, expected) {
|
|
6676
|
+
const diffs = [];
|
|
6677
|
+
const maxLen = Math.max(actual.length, expected.length);
|
|
6678
|
+
for (let i = 0; i < maxLen; i++) {
|
|
6679
|
+
const actualLine = actual[i];
|
|
6680
|
+
const expectedLine = expected[i];
|
|
6681
|
+
if (actualLine !== expectedLine) {
|
|
6682
|
+
if (actualLine === void 0) {
|
|
6683
|
+
diffs.push(`Line ${i + 1}: Missing in actual`);
|
|
6684
|
+
diffs.push(` Expected: ${expectedLine}`);
|
|
6685
|
+
} else if (expectedLine === void 0) {
|
|
6686
|
+
diffs.push(`Line ${i + 1}: Extra in actual`);
|
|
6687
|
+
diffs.push(` Actual: ${actualLine}`);
|
|
6688
|
+
} else {
|
|
6689
|
+
diffs.push(`Line ${i + 1}:`);
|
|
6690
|
+
diffs.push(` Expected: ${expectedLine}`);
|
|
6691
|
+
diffs.push(` Actual: ${actualLine}`);
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
}
|
|
6695
|
+
return diffs;
|
|
6696
|
+
}
|
|
6697
|
+
/**
|
|
6698
|
+
* Deep compare two values, supporting special markers like __or__ and __any__.
|
|
6699
|
+
* Collects all mismatches into the provided array.
|
|
6700
|
+
*/
|
|
6701
|
+
#deepCompareWithMarkers(actual, expected, path, mismatches) {
|
|
6702
|
+
if (this.#isOrMarker(expected)) {
|
|
6703
|
+
const allowedValues = expected.__or__;
|
|
6704
|
+
const matches = allowedValues.some((allowed) => {
|
|
6705
|
+
const tempMismatches = [];
|
|
6706
|
+
this.#deepCompareWithMarkers(actual, allowed, path, tempMismatches);
|
|
6707
|
+
return tempMismatches.length === 0;
|
|
6708
|
+
});
|
|
6709
|
+
if (!matches) {
|
|
6710
|
+
mismatches.push({ path, expected: { __or__: allowedValues }, actual });
|
|
6711
|
+
}
|
|
6712
|
+
return;
|
|
6713
|
+
}
|
|
6714
|
+
if (this.#isAnyMarker(expected)) {
|
|
6715
|
+
const typeConstraint = expected.__any__;
|
|
6716
|
+
if (actual === null || actual === void 0) {
|
|
6717
|
+
mismatches.push({ path, expected: { __any__: typeConstraint }, actual });
|
|
6718
|
+
return;
|
|
6719
|
+
}
|
|
6720
|
+
if (typeConstraint === true) {
|
|
6721
|
+
return;
|
|
6722
|
+
}
|
|
6723
|
+
const actualType = Array.isArray(actual) ? "array" : typeof actual;
|
|
6724
|
+
if (actualType !== typeConstraint) {
|
|
6725
|
+
mismatches.push({
|
|
6726
|
+
path,
|
|
6727
|
+
expected: { __any__: typeConstraint },
|
|
6728
|
+
actual: `(${actualType}) ${JSON.stringify(actual).slice(0, 50)}...`
|
|
6729
|
+
});
|
|
6730
|
+
}
|
|
6731
|
+
return;
|
|
6732
|
+
}
|
|
6733
|
+
if (Array.isArray(expected)) {
|
|
6734
|
+
if (!Array.isArray(actual)) {
|
|
6735
|
+
mismatches.push({ path, expected, actual });
|
|
6736
|
+
return;
|
|
6737
|
+
}
|
|
6738
|
+
if (actual.length !== expected.length) {
|
|
6739
|
+
mismatches.push({ path: `${path}.length`, expected: expected.length, actual: actual.length });
|
|
6740
|
+
return;
|
|
6741
|
+
}
|
|
6742
|
+
for (let i = 0; i < expected.length; i++) {
|
|
6743
|
+
this.#deepCompareWithMarkers(actual[i], expected[i], `${path}[${i}]`, mismatches);
|
|
6744
|
+
}
|
|
6745
|
+
return;
|
|
6746
|
+
}
|
|
6747
|
+
if (expected !== null && typeof expected === "object") {
|
|
6748
|
+
if (actual === null || typeof actual !== "object" || Array.isArray(actual)) {
|
|
6749
|
+
mismatches.push({ path, expected, actual });
|
|
6750
|
+
return;
|
|
6751
|
+
}
|
|
6752
|
+
const expectedObj = expected;
|
|
6753
|
+
const actualObj = actual;
|
|
6754
|
+
for (const key of Object.keys(expectedObj)) {
|
|
6755
|
+
if (this.#isMetadataKey(key)) {
|
|
6756
|
+
continue;
|
|
6757
|
+
}
|
|
6758
|
+
if (!(key in actualObj)) {
|
|
6759
|
+
if (expectedObj[key] !== void 0) {
|
|
6760
|
+
mismatches.push({ path: `${path}.${key}`, expected: expectedObj[key], actual: void 0 });
|
|
6761
|
+
}
|
|
6762
|
+
continue;
|
|
6763
|
+
}
|
|
6764
|
+
this.#deepCompareWithMarkers(actualObj[key], expectedObj[key], `${path}.${key}`, mismatches);
|
|
6765
|
+
}
|
|
6766
|
+
for (const key of Object.keys(actualObj)) {
|
|
6767
|
+
if (this.#isMetadataKey(key)) {
|
|
6768
|
+
continue;
|
|
6769
|
+
}
|
|
6770
|
+
if (!(key in expectedObj)) {
|
|
6771
|
+
if (actualObj[key] !== void 0) {
|
|
6772
|
+
mismatches.push({ path: `${path}.${key}`, expected: void 0, actual: actualObj[key] });
|
|
6773
|
+
}
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
return;
|
|
6777
|
+
}
|
|
6778
|
+
if (actual !== expected) {
|
|
6779
|
+
mismatches.push({ path, expected, actual });
|
|
6780
|
+
}
|
|
6781
|
+
}
|
|
6782
|
+
/**
|
|
6783
|
+
* Check if a value is an __or__ marker object
|
|
6784
|
+
*/
|
|
6785
|
+
#isOrMarker(value) {
|
|
6786
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) && "__or__" in value && Array.isArray(value.__or__);
|
|
6787
|
+
}
|
|
6788
|
+
/**
|
|
6789
|
+
* Check if a value is an __any__ marker object
|
|
6790
|
+
*/
|
|
6791
|
+
#isAnyMarker(value) {
|
|
6792
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
6793
|
+
return false;
|
|
6794
|
+
}
|
|
6795
|
+
if (!("__any__" in value)) {
|
|
6796
|
+
return false;
|
|
6797
|
+
}
|
|
6798
|
+
const constraint = value.__any__;
|
|
6799
|
+
return constraint === true || ["string", "number", "boolean", "object", "array"].includes(constraint);
|
|
6800
|
+
}
|
|
6801
|
+
/**
|
|
6802
|
+
* Check if a key should be skipped during comparison (metadata keys like __structure__)
|
|
6803
|
+
*/
|
|
6804
|
+
#isMetadataKey(key) {
|
|
6805
|
+
return key.startsWith("__") && key.endsWith("__");
|
|
6806
|
+
}
|
|
6807
|
+
/**
|
|
6808
|
+
* Custom JSON replacer to handle Date objects and other special types
|
|
6809
|
+
*/
|
|
6810
|
+
#jsonReplacer = (_key, value) => {
|
|
6811
|
+
if (value instanceof Date) {
|
|
6812
|
+
return value.toISOString();
|
|
6813
|
+
}
|
|
6814
|
+
return value;
|
|
6815
|
+
};
|
|
6816
|
+
// ============================================================================
|
|
6817
|
+
// Debugging Helpers
|
|
6818
|
+
// ============================================================================
|
|
6819
|
+
/**
|
|
6820
|
+
* Get all stored logs
|
|
6821
|
+
*/
|
|
6822
|
+
getLogs() {
|
|
6823
|
+
return [...this.#logs];
|
|
6824
|
+
}
|
|
6825
|
+
/**
|
|
6826
|
+
* Dump logs to console for debugging (uses console.error for visibility in test output)
|
|
6827
|
+
*/
|
|
6828
|
+
dumpLogs() {
|
|
6829
|
+
console.error("\n=== JsonExporter Logs ===");
|
|
6830
|
+
this.#logs.forEach((log) => console.error(log));
|
|
6831
|
+
console.error("=== End Logs ===\n");
|
|
6832
|
+
}
|
|
6833
|
+
/**
|
|
6834
|
+
* Validate final state - useful for test assertions
|
|
6835
|
+
*
|
|
6836
|
+
* @returns Object with validation results
|
|
6837
|
+
*/
|
|
6838
|
+
validateFinalState() {
|
|
6839
|
+
const traceIds = this.getTraceIds();
|
|
6840
|
+
const incompleteSpans = this.getIncompleteSpans();
|
|
6841
|
+
const singleTraceId = traceIds.length === 1;
|
|
6842
|
+
const allSpansComplete = incompleteSpans.length === 0;
|
|
6843
|
+
return {
|
|
6844
|
+
valid: singleTraceId && allSpansComplete,
|
|
6845
|
+
singleTraceId,
|
|
6846
|
+
allSpansComplete,
|
|
6847
|
+
traceIds,
|
|
6848
|
+
incompleteSpans
|
|
6849
|
+
};
|
|
6850
|
+
}
|
|
6851
|
+
// ============================================================================
|
|
6852
|
+
// Reset & Lifecycle
|
|
6853
|
+
// ============================================================================
|
|
6854
|
+
/**
|
|
6855
|
+
* Clear all collected events and state
|
|
6856
|
+
*/
|
|
6857
|
+
clearEvents() {
|
|
6858
|
+
this.#events = [];
|
|
6859
|
+
this.#spanStates.clear();
|
|
6860
|
+
this.#logs = [];
|
|
6861
|
+
}
|
|
6862
|
+
/**
|
|
6863
|
+
* Alias for clearEvents (compatibility with TestExporter)
|
|
6864
|
+
*/
|
|
6865
|
+
reset() {
|
|
6866
|
+
this.clearEvents();
|
|
6867
|
+
}
|
|
6868
|
+
async shutdown() {
|
|
6869
|
+
this.logger.info("JsonExporter shutdown");
|
|
6870
|
+
}
|
|
6871
|
+
// ============================================================================
|
|
6872
|
+
// Private Helpers
|
|
6873
|
+
// ============================================================================
|
|
6874
|
+
/**
|
|
6875
|
+
* Extract unique spans from a list of events
|
|
6876
|
+
*/
|
|
6877
|
+
#getUniqueSpansFromEvents(events) {
|
|
6878
|
+
const spanMap = /* @__PURE__ */ new Map();
|
|
6879
|
+
for (const event of events) {
|
|
6880
|
+
const span = event.exportedSpan;
|
|
6881
|
+
if (event.type === observability.TracingEventType.SPAN_ENDED || !spanMap.has(span.id)) {
|
|
6882
|
+
spanMap.set(span.id, span);
|
|
6883
|
+
}
|
|
6884
|
+
}
|
|
6885
|
+
return Array.from(spanMap.values());
|
|
6886
|
+
}
|
|
6887
|
+
};
|
|
6068
6888
|
|
|
6069
6889
|
// src/usage.ts
|
|
6070
6890
|
function extractUsageMetrics(usage, providerMetadata) {
|
|
@@ -6121,14 +6941,11 @@ var ModelSpanTracker = class {
|
|
|
6121
6941
|
#modelSpan;
|
|
6122
6942
|
#currentStepSpan;
|
|
6123
6943
|
#currentChunkSpan;
|
|
6944
|
+
#currentChunkType;
|
|
6124
6945
|
#accumulator = {};
|
|
6125
6946
|
#stepIndex = 0;
|
|
6126
6947
|
#chunkSequence = 0;
|
|
6127
6948
|
#completionStartTime;
|
|
6128
|
-
/** Tracks tool output accumulators by toolCallId for consolidating sub-agent streams */
|
|
6129
|
-
#toolOutputAccumulators = /* @__PURE__ */ new Map();
|
|
6130
|
-
/** Tracks toolCallIds that had streaming output (to skip redundant tool-result spans) */
|
|
6131
|
-
#streamedToolCallIds = /* @__PURE__ */ new Set();
|
|
6132
6949
|
constructor(modelSpan) {
|
|
6133
6950
|
this.#modelSpan = modelSpan;
|
|
6134
6951
|
}
|
|
@@ -6215,6 +7032,7 @@ var ModelSpanTracker = class {
|
|
|
6215
7032
|
* End the current Model execution step with token usage, finish reason, output, and metadata
|
|
6216
7033
|
*/
|
|
6217
7034
|
#endStepSpan(payload) {
|
|
7035
|
+
this.#endChunkSpan();
|
|
6218
7036
|
if (!this.#currentStepSpan) return;
|
|
6219
7037
|
const output = payload.output;
|
|
6220
7038
|
const { usage: rawUsage, ...otherOutput } = output;
|
|
@@ -6222,8 +7040,10 @@ var ModelSpanTracker = class {
|
|
|
6222
7040
|
const metadata = payload.metadata;
|
|
6223
7041
|
const usage = extractUsageMetrics(rawUsage, metadata?.providerMetadata);
|
|
6224
7042
|
const cleanMetadata = metadata ? { ...metadata } : void 0;
|
|
6225
|
-
if (cleanMetadata
|
|
6226
|
-
|
|
7043
|
+
if (cleanMetadata) {
|
|
7044
|
+
for (const key of ["request", "id", "timestamp", "modelId", "modelVersion", "modelProvider"]) {
|
|
7045
|
+
delete cleanMetadata[key];
|
|
7046
|
+
}
|
|
6227
7047
|
}
|
|
6228
7048
|
this.#currentStepSpan.end({
|
|
6229
7049
|
output: otherOutput,
|
|
@@ -6244,6 +7064,7 @@ var ModelSpanTracker = class {
|
|
|
6244
7064
|
* Create a new chunk span (for multi-part chunks like text-start/delta/end)
|
|
6245
7065
|
*/
|
|
6246
7066
|
#startChunkSpan(chunkType, initialData) {
|
|
7067
|
+
this.#endChunkSpan();
|
|
6247
7068
|
if (!this.#currentStepSpan) {
|
|
6248
7069
|
this.startStep();
|
|
6249
7070
|
}
|
|
@@ -6255,6 +7076,7 @@ var ModelSpanTracker = class {
|
|
|
6255
7076
|
sequenceNumber: this.#chunkSequence
|
|
6256
7077
|
}
|
|
6257
7078
|
});
|
|
7079
|
+
this.#currentChunkType = chunkType;
|
|
6258
7080
|
this.#accumulator = initialData || {};
|
|
6259
7081
|
}
|
|
6260
7082
|
/**
|
|
@@ -6277,13 +7099,14 @@ var ModelSpanTracker = class {
|
|
|
6277
7099
|
output: output !== void 0 ? output : this.#accumulator
|
|
6278
7100
|
});
|
|
6279
7101
|
this.#currentChunkSpan = void 0;
|
|
7102
|
+
this.#currentChunkType = void 0;
|
|
6280
7103
|
this.#accumulator = {};
|
|
6281
7104
|
this.#chunkSequence++;
|
|
6282
7105
|
}
|
|
6283
7106
|
/**
|
|
6284
7107
|
* Create an event span (for single chunks like tool-call)
|
|
6285
7108
|
*/
|
|
6286
|
-
#createEventSpan(chunkType, output) {
|
|
7109
|
+
#createEventSpan(chunkType, output, options) {
|
|
6287
7110
|
if (!this.#currentStepSpan) {
|
|
6288
7111
|
this.startStep();
|
|
6289
7112
|
}
|
|
@@ -6292,8 +7115,10 @@ var ModelSpanTracker = class {
|
|
|
6292
7115
|
type: observability.SpanType.MODEL_CHUNK,
|
|
6293
7116
|
attributes: {
|
|
6294
7117
|
chunkType,
|
|
6295
|
-
sequenceNumber: this.#chunkSequence
|
|
7118
|
+
sequenceNumber: this.#chunkSequence,
|
|
7119
|
+
...options?.attributes
|
|
6296
7120
|
},
|
|
7121
|
+
metadata: options?.metadata,
|
|
6297
7122
|
output
|
|
6298
7123
|
});
|
|
6299
7124
|
if (span) {
|
|
@@ -6321,6 +7146,9 @@ var ModelSpanTracker = class {
|
|
|
6321
7146
|
this.#startChunkSpan("text");
|
|
6322
7147
|
break;
|
|
6323
7148
|
case "text-delta":
|
|
7149
|
+
if (this.#currentChunkType !== "text") {
|
|
7150
|
+
this.#startChunkSpan("text");
|
|
7151
|
+
}
|
|
6324
7152
|
this.#appendToAccumulator("text", chunk.payload.text);
|
|
6325
7153
|
break;
|
|
6326
7154
|
case "text-end": {
|
|
@@ -6338,6 +7166,9 @@ var ModelSpanTracker = class {
|
|
|
6338
7166
|
this.#startChunkSpan("reasoning");
|
|
6339
7167
|
break;
|
|
6340
7168
|
case "reasoning-delta":
|
|
7169
|
+
if (this.#currentChunkType !== "reasoning") {
|
|
7170
|
+
this.#startChunkSpan("reasoning");
|
|
7171
|
+
}
|
|
6341
7172
|
this.#appendToAccumulator("text", chunk.payload.text);
|
|
6342
7173
|
break;
|
|
6343
7174
|
case "reasoning-end": {
|
|
@@ -6384,7 +7215,7 @@ var ModelSpanTracker = class {
|
|
|
6384
7215
|
#handleObjectChunk(chunk) {
|
|
6385
7216
|
switch (chunk.type) {
|
|
6386
7217
|
case "object":
|
|
6387
|
-
if (
|
|
7218
|
+
if (this.#currentChunkType !== "object") {
|
|
6388
7219
|
this.#startChunkSpan("object");
|
|
6389
7220
|
}
|
|
6390
7221
|
break;
|
|
@@ -6394,75 +7225,27 @@ var ModelSpanTracker = class {
|
|
|
6394
7225
|
}
|
|
6395
7226
|
}
|
|
6396
7227
|
/**
|
|
6397
|
-
* Handle tool-
|
|
6398
|
-
*
|
|
7228
|
+
* Handle tool-call-approval chunks.
|
|
7229
|
+
* Creates a span for approval requests so they can be seen in traces for debugging.
|
|
6399
7230
|
*/
|
|
6400
|
-
#
|
|
6401
|
-
if (chunk.type !== "tool-
|
|
7231
|
+
#handleToolApprovalChunk(chunk) {
|
|
7232
|
+
if (chunk.type !== "tool-call-approval") return;
|
|
6402
7233
|
const payload = chunk.payload;
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
if (!acc) {
|
|
6406
|
-
if (!this.#currentStepSpan) {
|
|
6407
|
-
this.startStep();
|
|
6408
|
-
}
|
|
6409
|
-
acc = {
|
|
6410
|
-
toolName: toolName || "unknown",
|
|
6411
|
-
toolCallId,
|
|
6412
|
-
text: "",
|
|
6413
|
-
reasoning: "",
|
|
6414
|
-
sequenceNumber: this.#chunkSequence++,
|
|
6415
|
-
// Name the span 'tool-result' for consistency (tool-call → tool-result)
|
|
6416
|
-
span: this.#currentStepSpan?.createChildSpan({
|
|
6417
|
-
name: `chunk: 'tool-result'`,
|
|
6418
|
-
type: observability.SpanType.MODEL_CHUNK,
|
|
6419
|
-
attributes: {
|
|
6420
|
-
chunkType: "tool-result",
|
|
6421
|
-
sequenceNumber: this.#chunkSequence - 1
|
|
6422
|
-
}
|
|
6423
|
-
})
|
|
6424
|
-
};
|
|
6425
|
-
this.#toolOutputAccumulators.set(toolCallId, acc);
|
|
6426
|
-
}
|
|
6427
|
-
if (output && typeof output === "object" && "type" in output) {
|
|
6428
|
-
const innerType = output.type;
|
|
6429
|
-
switch (innerType) {
|
|
6430
|
-
case "text-delta":
|
|
6431
|
-
if (output.payload?.text) {
|
|
6432
|
-
acc.text += output.payload.text;
|
|
6433
|
-
}
|
|
6434
|
-
break;
|
|
6435
|
-
case "reasoning-delta":
|
|
6436
|
-
if (output.payload?.text) {
|
|
6437
|
-
acc.reasoning += output.payload.text;
|
|
6438
|
-
}
|
|
6439
|
-
break;
|
|
6440
|
-
case "finish":
|
|
6441
|
-
case "workflow-finish":
|
|
6442
|
-
this.#endToolOutputSpan(toolCallId);
|
|
6443
|
-
break;
|
|
6444
|
-
}
|
|
6445
|
-
}
|
|
6446
|
-
}
|
|
6447
|
-
/**
|
|
6448
|
-
* End a tool output span and clean up the accumulator
|
|
6449
|
-
*/
|
|
6450
|
-
#endToolOutputSpan(toolCallId) {
|
|
6451
|
-
const acc = this.#toolOutputAccumulators.get(toolCallId);
|
|
6452
|
-
if (!acc) return;
|
|
6453
|
-
const output = {
|
|
6454
|
-
toolCallId: acc.toolCallId,
|
|
6455
|
-
toolName: acc.toolName
|
|
6456
|
-
};
|
|
6457
|
-
if (acc.text) {
|
|
6458
|
-
output.text = acc.text;
|
|
7234
|
+
if (!this.#currentStepSpan) {
|
|
7235
|
+
this.startStep();
|
|
6459
7236
|
}
|
|
6460
|
-
|
|
6461
|
-
|
|
7237
|
+
const span = this.#currentStepSpan?.createEventSpan({
|
|
7238
|
+
name: `chunk: 'tool-call-approval'`,
|
|
7239
|
+
type: observability.SpanType.MODEL_CHUNK,
|
|
7240
|
+
attributes: {
|
|
7241
|
+
chunkType: "tool-call-approval",
|
|
7242
|
+
sequenceNumber: this.#chunkSequence
|
|
7243
|
+
},
|
|
7244
|
+
output: payload
|
|
7245
|
+
});
|
|
7246
|
+
if (span) {
|
|
7247
|
+
this.#chunkSequence++;
|
|
6462
7248
|
}
|
|
6463
|
-
acc.span?.end({ output });
|
|
6464
|
-
this.#toolOutputAccumulators.delete(toolCallId);
|
|
6465
|
-
this.#streamedToolCallIds.add(toolCallId);
|
|
6466
7249
|
}
|
|
6467
7250
|
/**
|
|
6468
7251
|
* Wraps a stream with model tracing transform to track MODEL_STEP and MODEL_CHUNK spans.
|
|
@@ -6537,8 +7320,6 @@ var ModelSpanTracker = class {
|
|
|
6537
7320
|
// Internal watch event
|
|
6538
7321
|
case "tool-error":
|
|
6539
7322
|
// Tool error handling
|
|
6540
|
-
case "tool-call-approval":
|
|
6541
|
-
// Approval request (not content)
|
|
6542
7323
|
case "tool-call-suspended":
|
|
6543
7324
|
// Suspension (not content)
|
|
6544
7325
|
case "reasoning-signature":
|
|
@@ -6547,17 +7328,28 @@ var ModelSpanTracker = class {
|
|
|
6547
7328
|
// Redacted content metadata
|
|
6548
7329
|
case "step-output":
|
|
6549
7330
|
break;
|
|
7331
|
+
case "tool-call-approval":
|
|
7332
|
+
this.#handleToolApprovalChunk(chunk);
|
|
7333
|
+
break;
|
|
6550
7334
|
case "tool-output":
|
|
6551
|
-
this.#handleToolOutputChunk(chunk);
|
|
6552
7335
|
break;
|
|
6553
7336
|
case "tool-result": {
|
|
6554
|
-
const
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
7337
|
+
const {
|
|
7338
|
+
// Metadata - tool call context (unique to tool-result chunks)
|
|
7339
|
+
toolCallId,
|
|
7340
|
+
toolName,
|
|
7341
|
+
isError,
|
|
7342
|
+
dynamic,
|
|
7343
|
+
providerExecuted,
|
|
7344
|
+
providerMetadata,
|
|
7345
|
+
// Output - the actual result
|
|
7346
|
+
result} = chunk.payload || {};
|
|
7347
|
+
const metadata = { toolCallId, toolName };
|
|
7348
|
+
if (isError !== void 0) metadata.isError = isError;
|
|
7349
|
+
if (dynamic !== void 0) metadata.dynamic = dynamic;
|
|
7350
|
+
if (providerExecuted !== void 0) metadata.providerExecuted = providerExecuted;
|
|
7351
|
+
if (providerMetadata !== void 0) metadata.providerMetadata = providerMetadata;
|
|
7352
|
+
this.#createEventSpan(chunk.type, result, { metadata });
|
|
6561
7353
|
break;
|
|
6562
7354
|
}
|
|
6563
7355
|
}
|
|
@@ -6573,7 +7365,11 @@ var DEFAULT_KEYS_TO_STRIP = /* @__PURE__ */ new Set([
|
|
|
6573
7365
|
"experimental_providerMetadata",
|
|
6574
7366
|
"providerMetadata",
|
|
6575
7367
|
"steps",
|
|
6576
|
-
"tracingContext"
|
|
7368
|
+
"tracingContext",
|
|
7369
|
+
"execute",
|
|
7370
|
+
// Tool execute functions
|
|
7371
|
+
"validate"
|
|
7372
|
+
// Schema validate functions
|
|
6577
7373
|
]);
|
|
6578
7374
|
var DEFAULT_DEEP_CLEAN_OPTIONS = Object.freeze({
|
|
6579
7375
|
keysToStrip: DEFAULT_KEYS_TO_STRIP,
|
|
@@ -6600,6 +7396,50 @@ function truncateString(s, maxChars) {
|
|
|
6600
7396
|
}
|
|
6601
7397
|
return s.slice(0, maxChars) + "\u2026[truncated]";
|
|
6602
7398
|
}
|
|
7399
|
+
function isJsonSchema(val) {
|
|
7400
|
+
if (typeof val !== "object" || val === null) return false;
|
|
7401
|
+
if (val.$schema && typeof val.$schema === "string" && val.$schema.includes("json-schema")) {
|
|
7402
|
+
return true;
|
|
7403
|
+
}
|
|
7404
|
+
if (val.type === "object" && val.properties && typeof val.properties === "object") {
|
|
7405
|
+
return true;
|
|
7406
|
+
}
|
|
7407
|
+
return false;
|
|
7408
|
+
}
|
|
7409
|
+
function compressJsonSchema(schema, depth = 0) {
|
|
7410
|
+
if (depth > 3) {
|
|
7411
|
+
return schema.type || "object";
|
|
7412
|
+
}
|
|
7413
|
+
if (schema.type !== "object" || !schema.properties) {
|
|
7414
|
+
return schema.type || schema;
|
|
7415
|
+
}
|
|
7416
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
7417
|
+
const compressed = {};
|
|
7418
|
+
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
|
7419
|
+
const prop = propSchema;
|
|
7420
|
+
let value = prop.type || "unknown";
|
|
7421
|
+
if (prop.type === "object" && prop.properties) {
|
|
7422
|
+
value = compressJsonSchema(prop, depth + 1);
|
|
7423
|
+
if (required.has(key)) {
|
|
7424
|
+
compressed[key + " (required)"] = value;
|
|
7425
|
+
continue;
|
|
7426
|
+
}
|
|
7427
|
+
} else if (prop.type === "array" && prop.items) {
|
|
7428
|
+
if (prop.items.type === "object" && prop.items.properties) {
|
|
7429
|
+
value = [compressJsonSchema(prop.items, depth + 1)];
|
|
7430
|
+
} else {
|
|
7431
|
+
value = `${prop.items.type || "any"}[]`;
|
|
7432
|
+
}
|
|
7433
|
+
} else if (prop.enum) {
|
|
7434
|
+
value = prop.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
7435
|
+
}
|
|
7436
|
+
if (required.has(key) && typeof value === "string") {
|
|
7437
|
+
value += " (required)";
|
|
7438
|
+
}
|
|
7439
|
+
compressed[key] = value;
|
|
7440
|
+
}
|
|
7441
|
+
return compressed;
|
|
7442
|
+
}
|
|
6603
7443
|
function deepClean(value, options = DEFAULT_DEEP_CLEAN_OPTIONS) {
|
|
6604
7444
|
const { keysToStrip, maxDepth, maxStringLength, maxArrayLength, maxObjectKeys } = options;
|
|
6605
7445
|
const seen = /* @__PURE__ */ new WeakSet();
|
|
@@ -6659,6 +7499,15 @@ function deepClean(value, options = DEFAULT_DEEP_CLEAN_OPTIONS) {
|
|
|
6659
7499
|
if (val instanceof ArrayBuffer) {
|
|
6660
7500
|
return `[ArrayBuffer byteLength=${val.byteLength}]`;
|
|
6661
7501
|
}
|
|
7502
|
+
if (typeof val.serializeForSpan === "function") {
|
|
7503
|
+
try {
|
|
7504
|
+
return helper(val.serializeForSpan(), depth);
|
|
7505
|
+
} catch {
|
|
7506
|
+
}
|
|
7507
|
+
}
|
|
7508
|
+
if (isJsonSchema(val)) {
|
|
7509
|
+
return helper(compressJsonSchema(val), depth);
|
|
7510
|
+
}
|
|
6662
7511
|
const cleaned = {};
|
|
6663
7512
|
const entries = Object.entries(val);
|
|
6664
7513
|
let keyCount = 0;
|
|
@@ -6766,9 +7615,10 @@ var BaseSpan = class {
|
|
|
6766
7615
|
this.isInternal = isSpanInternal(this.type, options.tracingPolicy?.internal);
|
|
6767
7616
|
this.traceState = options.traceState;
|
|
6768
7617
|
this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
|
|
6769
|
-
|
|
6770
|
-
this.
|
|
6771
|
-
this.
|
|
7618
|
+
const entityParent = this.getParentSpan(false);
|
|
7619
|
+
this.entityType = options.entityType ?? entityParent?.entityType;
|
|
7620
|
+
this.entityId = options.entityId ?? entityParent?.entityId;
|
|
7621
|
+
this.entityName = options.entityName ?? entityParent?.entityName;
|
|
6772
7622
|
if (this.isEvent) {
|
|
6773
7623
|
this.output = deepClean(options.output, this.deepCleanOptions);
|
|
6774
7624
|
} else {
|
|
@@ -6795,14 +7645,25 @@ var BaseSpan = class {
|
|
|
6795
7645
|
get isRootSpan() {
|
|
6796
7646
|
return !this.parent;
|
|
6797
7647
|
}
|
|
7648
|
+
/** Get the closest parent span, optionally skipping internal spans */
|
|
7649
|
+
getParentSpan(includeInternalSpans) {
|
|
7650
|
+
if (!this.parent) {
|
|
7651
|
+
return void 0;
|
|
7652
|
+
}
|
|
7653
|
+
if (includeInternalSpans) return this.parent;
|
|
7654
|
+
if (this.parent.isInternal) return this.parent.getParentSpan(includeInternalSpans);
|
|
7655
|
+
return this.parent;
|
|
7656
|
+
}
|
|
6798
7657
|
/** Get the closest parent spanId that isn't an internal span */
|
|
6799
7658
|
getParentSpanId(includeInternalSpans) {
|
|
6800
7659
|
if (!this.parent) {
|
|
6801
7660
|
return this.parentSpanId;
|
|
6802
7661
|
}
|
|
6803
|
-
|
|
6804
|
-
if (
|
|
6805
|
-
|
|
7662
|
+
const parentSpan = this.getParentSpan(includeInternalSpans);
|
|
7663
|
+
if (parentSpan) {
|
|
7664
|
+
return parentSpan.id;
|
|
7665
|
+
}
|
|
7666
|
+
return this.parent.getParentSpanId(includeInternalSpans);
|
|
6806
7667
|
}
|
|
6807
7668
|
/** Find the closest parent span of a specific type by walking up the parent chain */
|
|
6808
7669
|
findParent(spanType) {
|
|
@@ -7846,6 +8707,7 @@ exports.DEFAULT_KEYS_TO_STRIP = DEFAULT_KEYS_TO_STRIP;
|
|
|
7846
8707
|
exports.DefaultExporter = DefaultExporter;
|
|
7847
8708
|
exports.DefaultObservabilityInstance = DefaultObservabilityInstance;
|
|
7848
8709
|
exports.DefaultSpan = DefaultSpan;
|
|
8710
|
+
exports.JsonExporter = JsonExporter;
|
|
7849
8711
|
exports.ModelSpanTracker = ModelSpanTracker;
|
|
7850
8712
|
exports.NoOpSpan = NoOpSpan;
|
|
7851
8713
|
exports.Observability = Observability;
|