@mastra/datadog 1.1.1-alpha.1 → 1.2.0-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
@@ -2,8 +2,25 @@ import { SpanType } from '@mastra/core/observability';
2
2
  import { omitKeys } from '@mastra/core/utils';
3
3
  import { BaseExporter, getExternalParentId } from '@mastra/observability';
4
4
  import tracer3 from 'dd-trace';
5
+ import { coreFeatures } from '@mastra/core/features';
5
6
 
6
7
  // src/bridge.ts
8
+ var FEATURE = "model-inference-span";
9
+ var observabilityFeatures;
10
+ var featureLoadPromise;
11
+ function loadObservabilityFeatures() {
12
+ if (!featureLoadPromise) {
13
+ featureLoadPromise = import('@mastra/observability').then((mod) => {
14
+ observabilityFeatures = mod.observabilityFeatures;
15
+ }).catch(() => {
16
+ });
17
+ }
18
+ return featureLoadPromise;
19
+ }
20
+ void loadObservabilityFeatures();
21
+ function isModelInferenceEnabled() {
22
+ return observabilityFeatures?.has(FEATURE) === true && coreFeatures.has(FEATURE);
23
+ }
7
24
 
8
25
  // src/metrics.ts
9
26
  function formatUsageMetrics(usage) {
@@ -29,18 +46,26 @@ function formatUsageMetrics(usage) {
29
46
  }
30
47
  return Object.keys(result).length > 0 ? result : void 0;
31
48
  }
32
- var SPAN_TYPE_TO_KIND = {
49
+ var SPAN_TYPE_TO_KIND_LEGACY = {
33
50
  [SpanType.AGENT_RUN]: "agent",
34
- // MODEL_GENERATION is the wrapper around 1..N MODEL_STEPs (the actual API calls).
35
- // It maps to 'workflow' so Datadog doesn't double-count it as an LLM call.
36
51
  [SpanType.MODEL_GENERATION]: "workflow",
37
- // MODEL_STEP is "Single model execution step within a generation (one API call)"
38
- // per packages/core/src/observability/types/tracing.ts, so it is the real LLM span.
39
52
  [SpanType.MODEL_STEP]: "llm",
40
53
  [SpanType.TOOL_CALL]: "tool",
41
54
  [SpanType.MCP_TOOL_CALL]: "tool",
42
55
  [SpanType.WORKFLOW_RUN]: "workflow"
43
56
  };
57
+ var SPAN_TYPE_TO_KIND_INFERENCE = {
58
+ [SpanType.AGENT_RUN]: "agent",
59
+ [SpanType.MODEL_GENERATION]: "workflow",
60
+ [SpanType.MODEL_STEP]: "workflow",
61
+ [SpanType.MODEL_INFERENCE]: "llm",
62
+ [SpanType.TOOL_CALL]: "tool",
63
+ [SpanType.MCP_TOOL_CALL]: "tool",
64
+ [SpanType.WORKFLOW_RUN]: "workflow"
65
+ };
66
+ function getSpanTypeToKind() {
67
+ return isModelInferenceEnabled() ? SPAN_TYPE_TO_KIND_INFERENCE : SPAN_TYPE_TO_KIND_LEGACY;
68
+ }
44
69
  var tracerInitFlag = { done: false };
45
70
  function ensureTracer(config) {
46
71
  if (tracerInitFlag.done) return;
@@ -66,7 +91,7 @@ function ensureTracer(config) {
66
91
  tracerInitFlag.done = true;
67
92
  }
68
93
  function kindFor(spanType) {
69
- return SPAN_TYPE_TO_KIND[spanType] || "task";
94
+ return getSpanTypeToKind()[spanType] || "task";
70
95
  }
71
96
  function toDate(value) {
72
97
  return value instanceof Date ? value : new Date(value);
@@ -441,7 +466,8 @@ var DatadogBridge = class extends BaseExporter {
441
466
  if (span.output !== void 0) {
442
467
  annotations.outputData = formatOutput(span.output, span.type);
443
468
  }
444
- if (span.type === SpanType.MODEL_STEP) {
469
+ const usageSpanType = isModelInferenceEnabled() ? SpanType.MODEL_INFERENCE : SpanType.MODEL_STEP;
470
+ if (span.type === usageSpanType) {
445
471
  const usage = span.attributes?.usage;
446
472
  const metrics = formatUsageMetrics(usage);
447
473
  if (metrics) {
@@ -707,7 +733,8 @@ var DatadogExporter = class extends BaseExporter {
707
733
  if (span.output !== void 0) {
708
734
  annotations.outputData = formatOutput(span.output, span.type);
709
735
  }
710
- if (span.type === SpanType.MODEL_STEP) {
736
+ const usageSpanType = isModelInferenceEnabled() ? SpanType.MODEL_INFERENCE : SpanType.MODEL_STEP;
737
+ if (span.type === usageSpanType) {
711
738
  const usage = span.attributes?.usage;
712
739
  const metrics = formatUsageMetrics(usage);
713
740
  if (metrics) {
@@ -777,6 +804,63 @@ var DatadogExporter = class extends BaseExporter {
777
804
  }
778
805
  return annotations;
779
806
  }
807
+ /**
808
+ * Submit an eval score to Datadog LLM Observability for the matching ddSpan.
809
+ *
810
+ * Ordering constraint: the matching span must have already been emitted to dd-trace
811
+ * (i.e. its `SPAN_ENDED` event must have been processed and the trace tree flushed).
812
+ * On Mastra's normal scoring path this is always true — scorer hooks fire after the
813
+ * scored entity completes, so the root span has ended by the time `onScoreEvent` runs.
814
+ *
815
+ * If a score arrives for an unexported span (either before `SPAN_ENDED` or after the
816
+ * `traceState` entry has been cleaned up), the event is dropped and a warning is logged
817
+ * so the misuse is observable. Scores must therefore only be submitted for spans whose
818
+ * lifecycle has completed.
819
+ */
820
+ async onScoreEvent(event) {
821
+ if (this.isDisabled || !tracer3.llmobs?.submitEvaluation) return;
822
+ const { score } = event;
823
+ if (!score.traceId || !score.spanId) {
824
+ this.logger.warn("Datadog exporter: dropping score with no traceId/spanId", {
825
+ scorerId: score.scorerId
826
+ });
827
+ return;
828
+ }
829
+ const ctx = this.traceState.get(score.traceId)?.contexts.get(score.spanId);
830
+ const exported = ctx?.exported;
831
+ if (!exported) {
832
+ this.logger.warn(
833
+ "Datadog exporter: dropping score for span that has not been emitted to dd-trace yet (span_ended must be processed before submitting a score for it)",
834
+ {
835
+ traceId: score.traceId,
836
+ spanId: score.spanId,
837
+ scorerId: score.scorerId
838
+ }
839
+ );
840
+ return;
841
+ }
842
+ try {
843
+ tracer3.llmobs.submitEvaluation(
844
+ { traceId: exported.traceId, spanId: exported.spanId },
845
+ {
846
+ label: score.scorerName ?? score.scorerId,
847
+ value: score.score,
848
+ metricType: "score",
849
+ mlApp: this.config.mlApp,
850
+ timestampMs: score.timestamp instanceof Date ? score.timestamp.getTime() : Date.now(),
851
+ ...score.reason ? { reasoning: score.reason } : {},
852
+ ...score.metadata ? { metadata: score.metadata } : {}
853
+ }
854
+ );
855
+ } catch (err) {
856
+ this.logger.error("Datadog exporter: Failed to submit evaluation", {
857
+ error: err,
858
+ traceId: score.traceId,
859
+ spanId: score.spanId,
860
+ scorerId: score.scorerId
861
+ });
862
+ }
863
+ }
780
864
  /**
781
865
  * Force flush any buffered spans without shutting down the exporter.
782
866
  * This is useful in serverless environments where you need to ensure spans