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