@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/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?.request) {
6224
- delete cleanMetadata.request;
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 (!this.#hasActiveChunkSpan()) {
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-output chunks from sub-agents.
6396
- * Consolidates streaming text/reasoning deltas into a single span per tool call.
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
- #handleToolOutputChunk(chunk) {
6399
- if (chunk.type !== "tool-output") return;
7228
+ #handleToolApprovalChunk(chunk) {
7229
+ if (chunk.type !== "tool-call-approval") return;
6400
7230
  const payload = chunk.payload;
6401
- const { output, toolCallId, toolName } = payload;
6402
- let acc = this.#toolOutputAccumulators.get(toolCallId);
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
- if (acc.reasoning) {
6459
- output.reasoning = acc.reasoning;
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 toolCallId = chunk.payload?.toolCallId;
6553
- if (toolCallId && this.#streamedToolCallIds.has(toolCallId)) {
6554
- this.#streamedToolCallIds.delete(toolCallId);
6555
- break;
6556
- }
6557
- const { args, ...cleanPayload } = chunk.payload || {};
6558
- this.#createEventSpan(chunk.type, cleanPayload);
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
- this.entityType = options.entityType ?? options.parent?.entityType;
6768
- this.entityId = options.entityId ?? options.parent?.entityId;
6769
- this.entityName = options.entityName ?? options.parent?.entityName;
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
- if (includeInternalSpans) return this.parent.id;
6802
- if (this.parent.isInternal) return this.parent.getParentSpanId(includeInternalSpans);
6803
- return this.parent.id;
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