@dbx-tools/appkit-mastra 0.9.60 → 0.9.61

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/src/telemetry.ts CHANGED
@@ -20,11 +20,14 @@
20
20
 
21
21
  import { StringDecoder } from "node:string_decoder";
22
22
 
23
- import { json, log, object } from "@dbx-tools/shared-core";
23
+ import { json, log, object, stringUtils } from "@dbx-tools/shared-core";
24
+ import { feedback, thread } from "@dbx-tools/shared-mastra";
24
25
  import { context, SpanKind, trace, type Span } from "@opentelemetry/api";
25
26
  import { getRPCMetadata, RPCType } from "@opentelemetry/core";
26
27
  import type express from "express";
27
28
 
29
+ import { USER_EMAIL_HEADER, USER_ID_HEADER } from "./identity.ts";
30
+
28
31
  const logger = log.logger("mastra/telemetry");
29
32
 
30
33
  /** Cap on each payload copied onto a span, so one turn cannot bloat the export. */
@@ -56,6 +59,34 @@ export const CHAT_RESPONSE_ATTR = "appkit.mastra.chat.response";
56
59
  export const CHAT_IDENTITY_ATTR = "appkit.mastra.identity.mode";
57
60
  /** Whether the streamed turn emitted a Genie tool or progress event. */
58
61
  export const CHAT_GENIE_USED_ATTR = "appkit.mastra.genie.used";
62
+ /**
63
+ * MLflow span-type attribute. Databricks maps this (or `gen_ai.operation.name`)
64
+ * onto the type chip in the experiment UI. Custom values such as `GENIE` are
65
+ * allowed.
66
+ */
67
+ export const MLFLOW_SPAN_TYPE_ATTR = "mlflow.spanType";
68
+ /** GenAI semantic-convention operation name, an alternate span-type source. */
69
+ export const GEN_AI_OPERATION_NAME_ATTR = "gen_ai.operation.name";
70
+ /**
71
+ * Root-span user id the MLflow experiment UI's User column reads from OTel
72
+ * traces. Prefer the forwarded email when present so the column is human.
73
+ */
74
+ export const MLFLOW_USER_ATTR = "user.id";
75
+ /** Root-span session id that enables the MLflow Sessions tab. */
76
+ export const MLFLOW_SESSION_ATTR = "session.id";
77
+ /**
78
+ * Prefix Databricks/MLflow copies onto the unified-view `tags` map. The
79
+ * suffix after this prefix is the tag name shown in the experiment UI.
80
+ */
81
+ export const MLFLOW_TRACE_TAG_PREFIX = "mlflow.trace.tag.";
82
+ /** Experiment-UI tag marking a turn that called Genie. */
83
+ export const MLFLOW_GENIE_TAG = "genie";
84
+ /** Root-span type for a Mastra agent turn. */
85
+ export const MLFLOW_SPAN_TYPE_AGENT = "AGENT";
86
+ /** Span type for an `ask_genie` tool call. */
87
+ export const MLFLOW_SPAN_TYPE_GENIE = "GENIE";
88
+ /** Attribute key for the `genie` trace tag. */
89
+ export const MLFLOW_GENIE_TAG_ATTR = `${MLFLOW_TRACE_TAG_PREFIX}${MLFLOW_GENIE_TAG}`;
59
90
 
60
91
  /** Root-span metadata known before a chat request is dispatched. */
61
92
  export interface ChatTurnTelemetryOptions {
@@ -68,6 +99,57 @@ interface TraceTarget {
68
99
  owned: boolean;
69
100
  }
70
101
 
102
+ /** OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars. */
103
+ const INVALID_TRACE_ID = "0".repeat(32);
104
+
105
+ /**
106
+ * Publish the turn's MLflow trace id before the handler writes the body.
107
+ *
108
+ * The chat UI only shows thumbs when this header is present. Stamping it here
109
+ * (on the same span the middleware just resolved) avoids relying on async
110
+ * Express continuation still sitting inside `context.with()`.
111
+ */
112
+ /**
113
+ * Mark the active span as a Genie call so the experiment UI types it `GENIE`
114
+ * and tags the trace `genie=true`. Safe no-op when no recording span is active.
115
+ */
116
+ export function stampGenieToolSpan(span: Span | undefined = trace.getActiveSpan()): void {
117
+ if (!span?.isRecording()) return;
118
+ span.setAttribute(MLFLOW_SPAN_TYPE_ATTR, MLFLOW_SPAN_TYPE_GENIE);
119
+ span.setAttribute(GEN_AI_OPERATION_NAME_ATTR, "execute_tool");
120
+ span.setAttribute(MLFLOW_GENIE_TAG_ATTR, "true");
121
+ }
122
+
123
+ function requestHeader(req: express.Request, name: string): string | undefined {
124
+ const fromMethod =
125
+ typeof req.header === "function" ? stringUtils.trimToNull(req.header(name)) : undefined;
126
+ if (fromMethod) return fromMethod;
127
+ const raw = req.headers?.[name.toLowerCase()];
128
+ const value = Array.isArray(raw) ? raw[0] : raw;
129
+ return stringUtils.trimToNull(typeof value === "string" ? value : undefined) ?? undefined;
130
+ }
131
+
132
+ function requestQuery(req: express.Request, name: string): string | undefined {
133
+ const raw = req.query?.[name];
134
+ const value = Array.isArray(raw) ? raw[0] : raw;
135
+ return stringUtils.trimToNull(typeof value === "string" ? value : undefined) ?? undefined;
136
+ }
137
+
138
+ function stampMlflowActor(span: Span, req: express.Request): void {
139
+ const user = requestHeader(req, USER_EMAIL_HEADER) ?? requestHeader(req, USER_ID_HEADER);
140
+ if (user) span.setAttribute(MLFLOW_USER_ATTR, user);
141
+ const session =
142
+ requestHeader(req, thread.THREAD_ID_HEADER) ?? requestQuery(req, thread.THREAD_ID_QUERY);
143
+ if (session) span.setAttribute(MLFLOW_SESSION_ATTR, session);
144
+ }
145
+
146
+ function stampMlflowTraceId(res: express.Response, span: Span): void {
147
+ if (res.headersSent) return;
148
+ const traceId = span.spanContext().traceId;
149
+ if (!traceId || traceId === INVALID_TRACE_ID) return;
150
+ res.setHeader(feedback.MLFLOW_TRACE_ID_HEADER, `tr-${traceId}`);
151
+ }
152
+
71
153
  /** Return one raw prompt when the request is exactly one text-only user message. */
72
154
  export function textOnlyChatInput(messages: unknown): string | undefined {
73
155
  if (typeof messages === "string") return messages || undefined;
@@ -276,6 +358,10 @@ export function chatTurnTelemetryMiddleware(
276
358
  next();
277
359
  return;
278
360
  }
361
+ stampMlflowTraceId(res, target.span);
362
+ stampMlflowActor(target.span, req);
363
+ target.span.setAttribute(MLFLOW_SPAN_TYPE_ATTR, MLFLOW_SPAN_TYPE_AGENT);
364
+ target.span.setAttribute(GEN_AI_OPERATION_NAME_ATTR, "invoke_agent");
279
365
  const identity =
280
366
  typeof options.identity === "function" ? options.identity(req) : options.identity;
281
367
  if (identity) {
@@ -311,7 +397,9 @@ export function chatTurnTelemetryMiddleware(
311
397
  fullOutputRecorded = true;
312
398
  };
313
399
  const recordGenie = (used: boolean): void => {
314
- if (used) target.span.setAttribute(CHAT_GENIE_USED_ATTR, true);
400
+ if (!used) return;
401
+ target.span.setAttribute(CHAT_GENIE_USED_ATTR, true);
402
+ target.span.setAttribute(MLFLOW_GENIE_TAG_ATTR, "true");
315
403
  };
316
404
  const endOwnedSpan = (): void => {
317
405
  if (!target.owned || ownedSpanEnded) return;