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