@mastra/observability 1.16.5 → 1.16.6-alpha.2

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
@@ -1759,6 +1759,34 @@ function addUsageStats(a, b) {
1759
1759
  function supportsModelInference() {
1760
1760
  return coreFeatures.has("model-inference-span");
1761
1761
  }
1762
+ function parseGatewayCost(providerMetadata) {
1763
+ const rawCost = providerMetadata?.gateway?.cost;
1764
+ const cost = typeof rawCost === "number" ? rawCost : typeof rawCost === "string" && rawCost.trim().length > 0 ? Number(rawCost) : void 0;
1765
+ return typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : void 0;
1766
+ }
1767
+ function getGatewayCostContext({ stepProviderMetadata }) {
1768
+ if (!stepProviderMetadata) return;
1769
+ if (!stepProviderMetadata.some((metadata) => metadata?.gateway !== void 0)) return;
1770
+ const costs = [];
1771
+ for (const metadata of stepProviderMetadata) {
1772
+ const cost = parseGatewayCost(metadata);
1773
+ if (cost === void 0) return;
1774
+ costs.push(cost);
1775
+ }
1776
+ const estimatedCost = costs.reduce((total, cost) => total + cost, 0);
1777
+ if (!Number.isFinite(estimatedCost)) return;
1778
+ return {
1779
+ estimatedCost,
1780
+ costUnit: "USD",
1781
+ costMetadata: {
1782
+ source: "provider_reported",
1783
+ sdkProvider: "vercel_ai_gateway",
1784
+ sdkCostField: "gateway.cost",
1785
+ scope: "query_total",
1786
+ reportedStepCount: costs.length
1787
+ }
1788
+ };
1789
+ }
1762
1790
  function formatPreviewLabel(label, fallback) {
1763
1791
  return typeof label === "string" && label.length > 0 ? label : fallback;
1764
1792
  }
@@ -1940,10 +1968,11 @@ var ModelSpanTracker = class {
1940
1968
  * If usage is provided, it will be converted to UsageStats with cache token details.
1941
1969
  */
1942
1970
  endGeneration(options) {
1943
- const { usage, providerMetadata, ...spanOptions } = options ?? {};
1971
+ const { usage, providerMetadata, stepProviderMetadata, ...spanOptions } = options ?? {};
1944
1972
  if (spanOptions.attributes) {
1945
1973
  spanOptions.attributes.completionStartTime = this.#completionStartTime;
1946
1974
  spanOptions.attributes.usage = extractUsageMetrics(usage, providerMetadata);
1975
+ if (!spanOptions.attributes.costContext) spanOptions.attributes.costContext = getGatewayCostContext({ stepProviderMetadata });
1947
1976
  }
1948
1977
  this.#modelSpan?.end(spanOptions);
1949
1978
  }
@@ -2431,12 +2460,12 @@ function isSpanInternal(spanType, flags) {
2431
2460
  /**
2432
2461
  * Get the external parent span ID from CreateSpanOptions.
2433
2462
  *
2434
- * If the parent is internal, walks up the parent chain to find
2435
- * the closest external ancestor. If the parent is already external,
2436
- * returns its ID directly.
2463
+ * If the parent is internal or excluded from export (`excludeSpanTypes`),
2464
+ * walks up the parent chain to find the closest exported ancestor.
2465
+ * If the parent is already exportable, returns its ID directly.
2437
2466
  *
2438
2467
  * This is useful when exporting spans to external observability systems
2439
- * that shouldn't include internal framework spans.
2468
+ * that shouldn't include internal framework spans or excluded types.
2440
2469
  *
2441
2470
  * @param options - Span creation options
2442
2471
  * @returns The external parent span ID, or undefined if no external parent exists
@@ -2457,8 +2486,9 @@ function isSpanInternal(spanType, flags) {
2457
2486
  */
2458
2487
  function getExternalParentId(options) {
2459
2488
  if (!options.parent) return;
2460
- if (options.parent.isInternal) return options.parent.getParentSpanId(false);
2461
- else return options.parent.id;
2489
+ const parent = options.parent;
2490
+ if (parent.isInternal || parent.isExcluded) return parent.getParentSpanId(false);
2491
+ return parent.id;
2462
2492
  }
2463
2493
  var BaseSpan = class {
2464
2494
  name;
@@ -2497,6 +2527,10 @@ var BaseSpan = class {
2497
2527
  * when the span is internal and includeInternalSpans is false, or when
2498
2528
  * the subclass is always excluded (e.g., NoOpSpan).
2499
2529
  *
2530
+ * Public so parent-chain walks (`getParentSpan`) can skip ancestors that
2531
+ * exporters will never receive — otherwise descendants export a
2532
+ * `parentSpanId` pointing at a dropped span and become orphans.
2533
+ *
2500
2534
  * Note: metadata is still attached and deepCleaned because it is read in
2501
2535
  * process by getCorrelationContext() and by getLoggerContext() /
2502
2536
  * getMetricsContext() (which structuredClone it).
@@ -2568,14 +2602,20 @@ var BaseSpan = class {
2568
2602
  get isRootSpan() {
2569
2603
  return !this.parent;
2570
2604
  }
2571
- /** Get the closest parent span, optionally skipping internal spans */
2605
+ /**
2606
+ * Get the closest parent span.
2607
+ * Always skips ancestors dropped by `excludeSpanTypes` (`isExcluded`), so
2608
+ * exported `parentSpanId` values never point at spans exporters omit.
2609
+ * When `includeInternalSpans` is false/undefined, also skips `isInternal`
2610
+ * ancestors.
2611
+ */
2572
2612
  getParentSpan(includeInternalSpans) {
2573
2613
  if (!this.parent) return;
2574
- if (includeInternalSpans) return this.parent;
2575
- if (this.parent.isInternal) return this.parent.getParentSpan(includeInternalSpans);
2614
+ const parent = this.parent;
2615
+ if (parent.isExcluded || !includeInternalSpans && parent.isInternal) return parent.getParentSpan(includeInternalSpans);
2576
2616
  return this.parent;
2577
2617
  }
2578
- /** Get the closest parent spanId that isn't an internal span */
2618
+ /** Get the closest parent spanId that will reach exporters (unless includeInternalSpans) */
2579
2619
  getParentSpanId(includeInternalSpans) {
2580
2620
  if (!this.parent) return this.parentSpanId;
2581
2621
  const parentSpan = this.getParentSpan(includeInternalSpans);
@@ -8696,6 +8736,23 @@ function isInstance(obj) {
8696
8736
  return obj instanceof BaseObservabilityInstance;
8697
8737
  }
8698
8738
  /**
8739
+ * Delays (ms) between attempts to rehydrate a trace from storage when an
8740
+ * annotation (score/feedback) targets a span that has not been flushed by
8741
+ * the configured exporters yet. Exporters buffer and flush asynchronously:
8742
+ * by default they flush at 1000 spans or after a 5000ms wait
8743
+ * (`maxBatchWaitMs`), so an annotation emitted right after a span ends can
8744
+ * race the flush and be silently dropped. The schedule below sums to
8745
+ * ~5.85s, covering the default flush interval with margin.
8746
+ */
8747
+ const RECORDED_TRACE_LOOKUP_RETRY_DELAYS_MS = [
8748
+ 100,
8749
+ 250,
8750
+ 500,
8751
+ 1e3,
8752
+ 2e3,
8753
+ 2e3
8754
+ ];
8755
+ /**
8699
8756
  * Top-level observability entrypoint. Manages a registry of ObservabilityInstance
8700
8757
  * configurations and provides instance selection via config selectors.
8701
8758
  */
@@ -8852,14 +8909,15 @@ var Observability = class extends MastraBase {
8852
8909
  return;
8853
8910
  }
8854
8911
  if (!args.traceId) return;
8855
- const trace = await this.#getStoredTrace(args.traceId);
8856
- if (!trace) return;
8857
- const event = buildRecordedScoreEventFromTrace({
8912
+ const event = await this.#buildRecordedEventWithRetry(args.traceId, (trace) => buildRecordedScoreEventFromTrace({
8858
8913
  trace,
8859
8914
  spanId: args.spanId,
8860
8915
  score: args.score
8861
- });
8862
- if (!event) return;
8916
+ }));
8917
+ if (!event) {
8918
+ this.logger?.warn(`Score event was dropped because the target trace/span was not found in observability storage (traceId: ${args.traceId}, spanId: ${args.spanId})`);
8919
+ return;
8920
+ }
8863
8921
  await this.#emitRecordedEvent(event);
8864
8922
  }
8865
8923
  async addFeedback(args) {
@@ -8875,14 +8933,15 @@ var Observability = class extends MastraBase {
8875
8933
  return;
8876
8934
  }
8877
8935
  if (!args.traceId) return;
8878
- const trace = await this.#getStoredTrace(args.traceId);
8879
- if (!trace) return;
8880
- const event = buildRecordedFeedbackEventFromTrace({
8936
+ const event = await this.#buildRecordedEventWithRetry(args.traceId, (trace) => buildRecordedFeedbackEventFromTrace({
8881
8937
  trace,
8882
8938
  spanId: args.spanId,
8883
8939
  feedback: args.feedback
8884
- });
8885
- if (!event) return;
8940
+ }));
8941
+ if (!event) {
8942
+ this.logger?.warn(`Feedback event was dropped because the target trace/span was not found in observability storage (traceId: ${args.traceId}, spanId: ${args.spanId})`);
8943
+ return;
8944
+ }
8886
8945
  await this.#emitRecordedEvent(event);
8887
8946
  }
8888
8947
  /** Register a named observability instance, optionally marking it as default. */
@@ -8947,6 +9006,20 @@ var Observability = class extends MastraBase {
8947
9006
  if (!storage) return null;
8948
9007
  return await storage.getStore("observability") ?? null;
8949
9008
  }
9009
+ /**
9010
+ * Build a recorded score/feedback event from storage, retrying briefly to
9011
+ * ride out the async exporter flush: annotations emitted right after a
9012
+ * span ends can otherwise race the flush and be silently dropped.
9013
+ */
9014
+ async #buildRecordedEventWithRetry(traceId, build) {
9015
+ for (const delayMs of [0, ...RECORDED_TRACE_LOOKUP_RETRY_DELAYS_MS]) {
9016
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
9017
+ const trace = await this.#getStoredTrace(traceId);
9018
+ const event = trace ? build(trace) : null;
9019
+ if (event) return event;
9020
+ }
9021
+ return null;
9022
+ }
8950
9023
  async #getStoredTrace(traceId) {
8951
9024
  const observabilityStorage = await this.#getObservabilityStorage();
8952
9025
  if (!observabilityStorage) return null;