Mastra
Mastra is a TypeScript framework for building agents, tools, and workflows, with its own observability layer that emits a span for every step an agent takes.
The deepeval integration plugs a DeepEvalExporter into that observability layer. Every agent run, model generation, tool call, and RAG operation becomes a span you can inspect and evaluate, without rewriting your Mastra app.
deepeval's Mastra integration enables you to:
- Trace every agent run โ register the exporter once on your
Mastrainstance and everygenerate(...)call produces a trace. - Evaluate traces or individual components with any
deepevalmetric. - Run evals from scripts or CI/CD โ same exporter, different surfaces.
- Customize trace and span data through exporter config, Mastra's per-request
tracingOptions, anddeepeval's span-staging helpers.
Getting Started
Installation
npm install -D deepeval @mastra/core @mastra/observabilityNo Confident AI account is needed to evaluate locally. Traces are built in-process either way; a CONFIDENT_API_KEY only adds the upload, so run npx deepeval login when you want them in a shared dashboard too.
Instrument and evaluate
Register a DeepEvalExporter on your Mastra instance's Observability config, then run your goldens through the agent.
import { Observability } from "@mastra/observability";
import { createTool } from "@mastra/core/tools";
import { Mastra } from "@mastra/core/mastra";
import { Agent } from "@mastra/core/agent";
import { z } from "zod";
import { DeepEvalExporter } from "deepeval/integrations/mastra";
import { EvaluationDataset, Golden } from "deepeval/dataset";
import { TaskCompletionMetric } from "deepeval/metrics";
const getWeather = createTool({
id: "getWeather",
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ weather: z.string() }),
execute: async ({ city }) => ({
weather: city.toLowerCase() === "tokyo" ? "Sunny, 72F" : "Rainy, 55F",
}),
});
const weatherAgent = new Agent({
id: "weather-agent",
name: "Weather Agent",
instructions: "Always use getWeather, then reply in one sentence.",
model: "openai/gpt-4o-mini",
tools: { getWeather },
});
const mastra = new Mastra({
agents: { weatherAgent },
observability: new Observability({
configs: {
deepeval: {
serviceName: "weather-app",
exporters: [new DeepEvalExporter()],
},
},
}),
});
const ask = (question: string) =>
mastra.getAgent("weatherAgent").generate(question);
// Goldens are the inputs you want to evaluate.
const dataset = new EvaluationDataset({
goldens: [new Golden({ input: "What's the weather in Tokyo?" })],
});
// The `TaskCompletionMetric` is passed into the `evalsIterator`.
for await (const golden of dataset.evalsIterator({
metrics: [new TaskCompletionMetric()],
})) {
await ask((golden as Golden).input);
}Done โ
. You've run your first eval with full traceability into Mastra via deepeval.
What gets traced
Every Mastra run produces a trace โ the end-to-end unit your user observes. Inside it, each span Mastra exports is mapped to the deepeval span type that matches it:
| Mastra span type | deepeval span |
|---|---|
AGENT_RUN, WORKFLOW_RUN | Agent |
MODEL_GENERATION | LLM |
TOOL_CALL, MCP_TOOL_CALL, PROVIDER_TOOL_CALL, CLIENT_TOOL_CALL | Tool |
RAG_EMBEDDING, RAG_VECTOR_OPERATION | Retriever |
| anything else | Custom |
Trace โ what the user observes
โโโ Agent: Weather Agent โ one generate(...) call
โโโ LLM: gpt-4o-mini โ component span: model chooses a tool
โโโ Tool: getWeather โ component span: tool input + output
โโโ LLM: gpt-4o-mini โ component span: final answerThe trace and its component spans are independently evaluable. Streaming chunk spans (MODEL_CHUNK) and Mastra's zero-duration event spans are dropped rather than turned into noise, and every tool call is also collected onto the trace's toolsCalled so trace-level metrics can reason about tool use.
Running evals
There are two surfaces for running evals against a Mastra app. Pick by where you want results to surface โ your terminal during development, or your CI pipeline as a pass/fail gate.
In CI/CD (Vitest)
Use the toPass() matcher. The golden is the subject; task produces the trace judged against it. A failing metric fails the test, which fails the build.
import { it, expect } from "vitest";
import { EvaluationDataset, Golden } from "deepeval/dataset";
import { TaskCompletionMetric } from "deepeval/metrics";
import { ask } from "./mastra-agent";
import "deepeval/vitest";
const dataset = new EvaluationDataset({
goldens: [
new Golden({ input: "What's the weather in Tokyo?" }),
new Golden({ input: "What's the weather in London?" }),
],
});
it.each(dataset.goldens as Golden[])(
"completes the weather task",
async (golden) => {
await expect(golden).toPass([new TaskCompletionMetric()], {
task: (g) => ask(g.input),
});
},
);Run it with:
npx deepeval test run mastra-agent.test.tsIn a script
Use EvaluationDataset + evalsIterator(...). Each Golden becomes one Mastra run, and metrics passed to the iterator score the resulting trace end-to-end.
const dataset = new EvaluationDataset({
goldens: [
new Golden({ input: "What's the weather in Tokyo?" }),
new Golden({ input: "What's the weather in London?" }),
],
});
for await (const golden of dataset.evalsIterator({
metrics: [new TaskCompletionMetric()],
})) {
await ask((golden as Golden).input);
}
await mastra.observability.shutdown();Applying metrics to components
Passing metrics=[...] to evalsIterator evaluates the overall Mastra run. To evaluate a component instead, attach metrics where Mastra creates that component.
Agent spans (sub-agents)
Wrap the call in nextAgentSpan(...). The exporter drains the staged config onto the first agent span it opens inside the callback โ useful for scoring a sub-agent or a workflow step in isolation.
import { TaskCompletionMetric } from "deepeval/metrics";
import { nextAgentSpan } from "deepeval/tracing";
...
for await (const golden of dataset.evalsIterator()) {
await nextAgentSpan({ metrics: [new TaskCompletionMetric()] }, () =>
ask((golden as Golden).input),
);
}LLM calls
Wrap the call in nextLlmSpan(...). The exporter drains the staged config onto the first LLM span it opens inside the callback; later model generations in the same run get nothing.
import { AnswerRelevancyMetric } from "deepeval/metrics";
import { nextLlmSpan } from "deepeval/tracing";
...
for await (const golden of dataset.evalsIterator()) {
await nextLlmSpan({ metrics: [new AnswerRelevancyMetric()] }, () =>
ask((golden as Golden).input),
);
}Staged fields also beat the exporter's static config, so a nextLlmSpan({ metricCollection }) overrides llmMetricCollection for that one span.
Tool calls
Inside a Mastra tool body, your code is the tool span, so reach it directly with updateCurrentSpan(...). This is the only place expectedTools can be supplied per call โ nothing can infer it for you.
import { ToolCorrectnessMetric } from "deepeval/metrics";
import { updateCurrentSpan } from "deepeval/tracing";
import { ToolCall } from "deepeval/test-case";
...
const getWeather = createTool({
id: "getWeather",
description: "Get the current weather for a city.",
inputSchema: z.object({ city: z.string() }),
outputSchema: z.object({ weather: z.string() }),
execute: async ({ city }) => {
updateCurrentSpan({
metrics: [new ToolCorrectnessMetric()],
toolsCalled: [new ToolCall({ name: "getWeather", inputParameters: { city } })],
expectedTools: [new ToolCall({ name: "getWeather" })],
});
return { weather: city.toLowerCase() === "tokyo" ? "Sunny, 72F" : "Rainy, 55F" };
},
});nextToolSpan(...) stages the same fields from outside the tool, on the same one-shot semantic as nextLlmSpan. Prefer it for deterministic tools where you only want traceability and metadata rather than a metric.
Retriever calls
Mastra's RAG embedding and vector-operation spans become retriever spans, so nextRetrieverSpan(...) stages a metric (or a Confident AI metricCollection) on the first one in the callback.
import { nextRetrieverSpan } from "deepeval/tracing";
...
for await (const golden of dataset.evalsIterator()) {
await nextRetrieverSpan({ metricCollection: "retriever_v1" }, () =>
ask((golden as Golden).input),
);
}Customizing trace and span data
Mastra owns the span lifecycle, so customization happens at the exporter or the span-staging boundary.
- Use
new DeepEvalExporter({...})for defaults that apply to every trace the exporter produces. - Use Mastra's per-request
tracingOptionsfor values that change per call โ these override the exporter's defaults. - Use
nextAgentSpan(...)/nextLlmSpan(...)/nextToolSpan(...)/nextRetrieverSpan(...)to stage component-level fields onto the next span Mastra opens. - Use
updateCurrentSpan(...)inside a tool body, where your code already sits on the span.
Exporter-level defaults:
const exporter = new DeepEvalExporter({
name: "weather-app",
environment: "development",
tags: ["mastra", "weather"],
metadata: { team: "support" },
userId: "user-123",
});Per-request context, read off the root span's metadata and tags:
await mastra.getAgent("weatherAgent").generate("What's the weather in Tokyo?", {
tracingOptions: {
metadata: { threadId: "thread-123", userId: "user-xyz", team: "growth" },
tags: ["production"],
},
});threadId, userId, traceName, testCaseId and turnId are read as trace fields; sessionId and resourceId are accepted as aliases for the first two. Every other key falls through to the trace's metadata, merged onto whatever the exporter's config already set.
Advanced patterns
Score every matching span with setTracingContext
Where next*Span(...) is one-shot, setTracingContext(...) is scope-wide: it applies to every matching span in the callback. This is what you want when an agent makes several model calls per run and you want all of them scored.
import { AnswerRelevancyMetric, ToolCorrectnessMetric } from "deepeval/metrics";
import { setTracingContext } from "deepeval/tracing";
...
await setTracingContext(
{
llmSpanContext: {
metrics: [new AnswerRelevancyMetric()],
toolsMetrics: [new ToolCorrectnessMetric()],
},
},
() => ask("What's the weather in Tokyo?"),
);toolsMetrics and toolsMetricCollection are declared on llmSpanContext but target the tool spans in scope. agentSpanContext does the same for agent spans.
Mastra runs own their trace
Unlike callback-based integrations, the exporter starts a new trace for each Mastra trace id rather than nesting under an enclosing span. Wrapping generate(...) in observe(...) therefore produces two sibling traces rather than one nested tree, and evalsIterator() / toPass() will warn that the callback produced more than one trace โ trace-level metrics then judge only the one carrying the turn's output.
So instead of wrapping the run, put your own logic in a Mastra tool or workflow step and let Mastra parent it.
API reference
new DeepEvalExporter({...}) accepts the following config. Each one is a default for traces produced by that exporter.
| Field | Type | Description |
|---|---|---|
apiKey | string | Confident AI API key. Defaults to CONFIDENT_API_KEY. |
environment | string | Trace environment, e.g. "development" or "production". |
name | string | Default trace name. Falls back to the Observability config's serviceName. |
tags | string[] | Tags applied to traces produced by this exporter. |
metadata | Record<string, any> | Trace metadata applied when the exporter starts a trace. |
threadId | string | Groups related runs into a single trace thread. |
userId | string | Actor identifier for the trace. |
testCaseId | string | Optional test case identifier. |
turnId | string | Optional turn identifier for conversational traces. |
metricCollection | string | Trace-level metric collection, for online evals on live traffic. |
traceMetricCollection | string | Trace-level metric collection. Takes precedence over metricCollection. |
llmMetricCollection | string | Metric collection applied to every LLM span. |
agentMetricCollection | string | Metric collection applied to every agent span. |
toolMetricCollectionMap | Record<string, string> | Metric collection per tool, keyed by tool name. |
prompt | Prompt | Prompt version to associate with every LLM span. |
debug | boolean | Log exporter errors instead of swallowing them. |
The exporter never sets trace-level metrics โ those come from evalsIterator({ metrics }) or toPass([...]) in an eval, and from a metricCollection in production.
For native tracing helpers (observe, updateCurrentTrace, updateCurrentSpan) see the tracing reference.
FAQs
Do I need an API key to evaluate a Mastra app locally?
DeepEvalExporter builds every span in-process, so metrics, datasets, evalsIterator() and toPass() all work with no CONFIDENT_API_KEY set. The key buys one thing: uploading the trace to Confident AI, and deepeval skips that step rather than failing the run.Can I evaluate a sub-agent inside my Mastra run?
AGENT_RUN and WORKFLOW_RUN, so staging a metric with nextAgentSpan({ metrics: [...] }, () => ...) scores that sub-agent in isolation without touching the parent. It is one-shot per callback, so to score every agent span use setTracingContext with an agentSpanContext instead.Can I gate CI/CD on my Mastra agent's metrics?
expect(golden).toPass([...], { task: (g) => ask(g.input) }) in a Vitest test and run npx deepeval test run so a failing metric fails the build. The matcher waits for Mastra's exporter to settle before scoring, so an asynchronously delivered span is not a race.Can I monitor a Mastra app in production?
tracingOptions.metadata with a threadId / userId per request; those live traces support online evals on real traffic when a metricCollection is set on the exporter.