Skip to content

feat(node): Add Mistral AI integration - #24243

Merged
JPeer264 merged 26 commits into
developfrom
nh/mistral-integration
Sep 15, 2026
Merged

JPeer264 merged 26 commits into
developfrom
nh/mistral-integration

Conversation

@nicohrubec

@nicohrubec nicohrubec commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

Adds a gen_ai integration for the @mistralai/mistralai v2 SDK.

  • Automatic: Sentry.mistralAIIntegration()
  • Manual: Sentry.instrumentMistralAiClient(client)

Instruments chat.complete/stream (gen_ai.chat), embeddings.create (gen_ai.embeddings), and agents.complete/stream (gen_ai.invoke_agent), including streaming, with recordInputs/recordOutputs (PII) controls. Provider mistral, origin auto.ai.mistral.

Trace from my local sample app:
Screenshot 2026-09-11 at 11 21 50

Sorry for the large PR and the AI integrations more generally could also use some refactors. However, to get this out soon I suggest to follow up on this with a broader sweep.

Add span-streaming (`traceLifecycle: 'stream'`) node integration tests
for a planned `@mistralai/mistralai` gen_ai integration, mirroring the
OpenAI suite. Covers chat, embeddings, agents (invoke_agent) and fim
(text_completion), across PII-off, PII-on and explicit-integration-option
variants.

These tests are expected to fail until the `mistralAIIntegration` /
`instrumentMistralClient` instrumentation is implemented (TDD step 1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 28.96 kB - -
@sentry/browser - with treeshaking flags 27.26 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.15 kB - -
@sentry/browser (incl. Tracing) 50.51 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.52 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.5 kB - -
@sentry/browser (incl. Tracing, Replay) 90.07 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.16 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.77 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.73 kB - -
@sentry/browser (incl. Feedback) 46.46 kB - -
@sentry/browser (incl. sendFeedback) 34.01 kB - -
@sentry/browser (incl. FeedbackAsync) 39.12 kB - -
@sentry/browser (incl. Metrics) 29.98 kB - -
@sentry/browser (incl. Logs) 30.24 kB - -
@sentry/browser (incl. Metrics & Logs) 30.91 kB - -
@sentry/react 30.72 kB - -
@sentry/react (incl. Tracing) 52.81 kB - -
@sentry/vue 36.2 kB - -
@sentry/vue (incl. Tracing) 52.76 kB - -
@sentry/svelte 28.98 kB - -
CDN Bundle 30.7 kB - -
CDN Bundle (incl. Tracing) 51.01 kB - -
CDN Bundle (incl. Logs, Metrics) 32.98 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 52.99 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.67 kB - -
CDN Bundle (incl. Tracing, Replay) 88.56 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.53 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.64 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.63 kB - -
CDN Bundle - uncompressed 90.83 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.33 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.41 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.29 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 226.82 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 271.9 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 277.85 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.6 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.54 kB - -
@sentry/nextjs (client) 55.13 kB - -
@sentry/sveltekit (client) 50.93 kB - -
@sentry/core/server 37.13 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 132.21 kB +1.21% +1.57 kB 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.03 kB +0.18% +145 B 🔺
@sentry/node - without tracing 89.83 kB +0.19% +162 B 🔺
@sentry/node - without channel injection 111.08 kB +1.41% +1.54 kB 🔺
@sentry/aws-serverless 98.08 kB +0.18% +173 B 🔺
@sentry/cloudflare (withSentry) - minified 203.54 kB +0.09% +179 B 🔺
@sentry/cloudflare (withSentry) 506.83 kB +0.08% +389 B 🔺

View base workflow run

Instrument `@mistralai/mistralai` v2 with gen_ai spans, turning the
previously-failing integration tests green.

- Automatic `mistralAIIntegration()` via the orchestrion diagnostics
  channels (default-on in Node)
- Manual `instrumentMistralClient()` proxy for edge runtimes
- Covers chat, embeddings, agents (invoke_agent) and fim
  (text_completion), including streaming, with `recordInputs` /
  `recordOutputs` controls

Mistral's typed responses/usage are camelCase, so the response/stream
mapping reads `promptTokens`/`completionTokens`/`totalTokens` and
`choices[].finishReason` directly. `@mistralai/mistralai` v2 is ESM-only,
so the CJS test variants are marked `failsOnCjs`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/server-utils/src/ai/mistral/streaming.ts
- Remove `failsOnCjs` from the Mistral suite: CJS auto-instrumentation
  works on a full build (it only failed under a partial local rebuild),
  so the tests pass in both ESM and CJS.
- Re-export `mistralAIIntegration` / `instrumentMistralClient` from the
  dependent SDK packages (aws-serverless, bun, elysia, deno,
  google-cloud-serverless, astro, cloudflare, vercel-edge) so the
  node-exports consistency check passes.
- Add `Mistral` to the Deno default-integrations snapshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread dev-packages/node-integration-tests/suites/tracing/mistral/test.ts
@nicohrubec nicohrubec changed the title test(node): Add failing integration tests for Mistral AI SDK feat(node): Add Mistral AI integration Sep 9, 2026
nicohrubec and others added 9 commits September 9, 2026 15:31
Exclude the fim endpoints for now — `text_completion` is not yet used by
any other AI integration, so defer it to a follow-up. Remaining scope:
chat, embeddings, and agents.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`@mistralai/mistralai` v2 ships no CJS build, so CJS consumers load it via
`require(esm)`, whose auto-instrumentation is inconsistent across Node
versions (works on 24/26, fails on 22). The SDK's native mode is ESM, so
use `createEsmTests` and cover it there only.

Also give the embeddings mock a distinct id per call shape so the
single-input span is targeted unambiguously.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`@langchain/mistralai` drives the `@mistralai/mistralai` SDK under the
hood, so with the LangChain integration active both it and
`mistralAIIntegration` would instrument the same call, producing two
spans. Add `Mistral` to LangChain's `SKIPPED_PROVIDERS`, matching the
existing OpenAI/Anthropic/Google handling. This also puts the previously
unused `MISTRAL_INTEGRATION_NAME` constant to use.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Record request tool definitions on `gen_ai.tool.definitions` (gated by
  recordInputs), restoring the `recordInputs` arg to
  `extractRequestAttributes`.
- Accumulate streamed tool calls by index (concatenating fragmented
  `function.arguments`) and emit them on `gen_ai.response.tool_calls`;
  non-streaming tool calls were already captured.
- Add `scenario-tools.mjs` + a test asserting tool definitions and tool
  calls for both streaming and non-streaming chat.

Brings Mistral to parity with the OpenAI integration for function tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read the span-name token from the already-extracted request attributes
(model, or the agent name for invoke_agent spans) instead of re-reading
the raw params — mirroring the OpenAI integration. Removes the
`getModelForSpanName` helper. No change to emitted attributes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Hoist the single `recordOutputs` check to wrap both content and tool
  calls (was checked twice), matching the OpenAI streaming path.
- Drop the dead `?? ''` in tool-call argument accumulation (the first
  chunk already seeds it to '').
- Simplify the chunk unwrap to a single object guard.

No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Align `gen_ai.provider.name` (and the matching `sentry.origin`) with the
package-scope slug `mistralai` — consistent with how `@google/genai`
maps to `google_genai`, and with the existing LangChain path which
already reports Mistral as `mistralai`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lAiClient

Align the manual instrumentation API with the other providers, which all
use the `instrument<Provider>AiClient` form (`instrumentOpenAiClient`,
`instrumentAnthropicAiClient`, `instrumentWorkersAiClient`). Renamed the
export and all per-package re-exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tion

# Conflicts:
#	packages/deno/test/__snapshots__/mod.test.ts.snap

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Comment thread packages/server-utils/src/ai/mistral/streaming.ts
nicohrubec and others added 2 commits September 11, 2026 15:32
- Set error status on the span when a Mistral stream throws mid-iteration,
  so failed streams no longer end as successful gen_ai spans.
- Decide streaming from the SDK method alone; `stream: true` on `complete`
  still returns a completion in v2, so it must not be wrapped as a stream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The default-on Mistral AI integration grows the @sentry/node bundle past the current budgets (130.52 kB > 130, 109.42 kB > 109). Raise both limits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicohrubec
nicohrubec marked this pull request as ready for review September 11, 2026 14:17
@nicohrubec
nicohrubec requested review from a team as code owners September 11, 2026 14:17
@nicohrubec
nicohrubec requested review from JPeer264, RulaKhaled, andreiborza, mydea and s1gr1d and removed request for a team September 11, 2026 14:17
@RulaKhaled
RulaKhaled added this pull request to stack #24379 September 14, 2026 18:53
RulaKhaled and others added 2 commits September 14, 2026 21:58
Cancelling a stream can reject a `read()` that is already in flight, because the
cancel aborts the HTTP body the stream reads from. The cancel wrappers awaited
the underlying cancel before ending the span, so that rejection reached `settle`
first and recorded a deliberate abort as `internal_error`.

The caller's intent is now recorded synchronously, before the underlying cancel
is awaited, and an error arriving afterwards no longer sets error status. That
makes the outcome independent of which of the two settles first.

A stream that fails on its own still ends as `internal_error`; only a cancel the
caller asked for suppresses it.

Note that a spec `ReadableStream` resolves an in-flight read with `done: true`
on cancel rather than rejecting it, so this is only reachable on a stream backed
by a live connection. The regression test drives the reader directly for that
reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…Through

`tee`, `pipeTo` and `pipeThrough` acquire their reader through internal slots
rather than by calling the public `getReader`, so patching `getReader` and the
async iterator left them uninstrumented. A stream drained that way delivered
every chunk correctly and produced no gen_ai span at all: the span was opened,
never ended, and so never flushed. The surrounding trace still looked complete,
which made the loss silent.

These three now pull through the wrapped reader, so accumulation and span ending
stay in one place and a teed stream is still recorded once rather than twice.

The sub-stream follows `monitorStream` in `@sentry/deno` with two deliberate
differences: chunks are pulled on demand rather than drained in a `start` loop,
so backpressure still reaches the source, and `cancel` is forwarded, which is the
hole #24054 fixed there.

`@sentry/openai` and friends need none of this because their streams are plain
async iterables. Mistral's `EventStream` extends `ReadableStream`, so it has
drain paths they do not. Workers AI solves the same problem by returning a new
stream, which is not available here: the orchestrion path publishes on `asyncEnd`
after the caller already holds the stream, and Node's `tracingChannel` returns
the original value regardless of what a subscriber assigns to `ctx.result`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@RulaKhaled RulaKhaled left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall looks good, direction of instrumentation makes sense. i pushed three commits that fold in some changes:

  • manual path no longer swaps the EventStream for a bare async generator, so getReader/tee/pipeTo/cancel survive
  • spans close on every drain path now, not just for await: getReader(), cancel(), tee(), pipeTo(), pipeThrough(). the last three take their reader from internal slots, so they were delivering every chunk fine and producing no gen_ai span at all
  • a cancelled stream isn't recorded as internal_error anymore
  • invoke_agent spans drop the agent id from the name under span streaming, ids are per-agent so it was high cardinality
  • gen_ai.request.stream comes from the method, streaming methods get their own channels
  • chat.parse / chat.parseStream produce spans, they call the request fns directly rather than this.complete so they were invisible
  • array-shaped streamed content is recorded instead of dropped

two that are minor. gen_ai.response.text is now a stringified array per the conventions spec instead of one concatenated blob, and gen_ai.output.messages is set on both paths since it's the non-deprecated replacement. that meant moving setOutputMessagesAttribute out of ai/workers-ai/utils into ai/core/utils so both share it, workers-ai call sites are unchanged and its suites still pass.

  • also bumped the two @sentry/node size limits by 1 KB.
  • 32 unit tests plus an integration scenario for instrumentMistralAiClient, which had no coverage at all before.
  • stacked the e2e app in #24378 — one express app run twice, dev through the runtime hook and prod through an esbuild build, 24 tests across both. needs retargeting to develop once this lands.

`chat.parse` and `chat.parseStream` had orchestrion entries but were only
exercised against a stubbed client, so nothing proved the transform actually
matched those methods on the real SDK.

Drives both through the mock server with a zod `responseFormat` and asserts one
span each. They call the underlying request functions directly rather than
`this.complete` / `this.stream`, so the assertion also pins down that they
neither go uninstrumented nor produce a second span from the sibling method.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread packages/server-utils/src/ai/mistral/streaming.ts
Comment thread dev-packages/node-integration-tests/suites/tracing/mistral/instrument-manual.mjs Outdated

@mydea mydea left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generally fine, a few small nits and seems worth it to do a follow up with a bit of cleanup!

Comment thread dev-packages/node-integration-tests/suites/tracing/mistral/instrument.mjs Outdated
Comment thread packages/server-utils/src/ai/mistral/index.ts Outdated
Comment thread packages/server-utils/src/ai/mistral/utils.ts
Comment thread packages/server-utils/src/integrations/mistral.ts Outdated

const finishReasons = choices
.map(choice => choice.finishReason)
.filter((reason): reason is string => typeof reason === 'string');

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Maybe we should also check against empty strings? It was done for responseTexts as well

Suggested change
.filter((reason): reason is string => typeof reason === 'string');
.filter((reason): reason is string => typeof reason === 'string' && Boolean(reason));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return {
responseText: contentToString(message?.content),
toolCalls: Array.isArray(message?.toolCalls) ? message.toolCalls : undefined,
finishReason: typeof choice.finishReason === 'string' ? choice.finishReason : undefined,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};
});

const responseTexts = outputMessages.map(message => message.responseText).filter(Boolean);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super-l: Theoretically we could turn around .filter with .map so we could reduce the second iteration a little (not sure if this performance improvement is worth it though):

const responseTexts = outputMessages
  .filter(message => Boolean(message.responseText))
  .map(message => message.responseText);

if (Array.isArray(response.choices)) {
const choices = response.choices as Array<Record<string, unknown>>;

const finishReasons = choices

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { extractSystemInstructions, setTokenUsageAttributes } from '../core/utils';
import { extractSystemInstructions, setOutputMessagesAttribute, setTokenUsageAttributes } from '../core/utils';
// Re-exported so `workers-ai/streaming.ts` keeps importing it from this module.
export { setOutputMessagesAttribute };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Moving the export to after the imports would make it easier to see that something is exported, even though it wouldn't make a difference for the implenentation

Comment thread dev-packages/node-integration-tests/suites/tracing/mistral/instrument.mjs Outdated
JPeer264 and others added 7 commits September 15, 2026 10:54
…trument-manual.mjs

Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
…trument-with-options.mjs

Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
…trument-with-pii.mjs

Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
…trument.mjs

Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
Co-authored-by: Francesco Gringl-Novy <francesco.novy@sentry.io>
Comment on lines +64 to +67
}
}

/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: For streaming channels, if a result is not a valid async-iterable, the span ends prematurely and silently fails to record any stream data or response attributes.
Severity: MEDIUM

Suggested Fix

The logic in the end event subscriber should be modified. When deferSpanEnd returns false for a channel configured for streaming, it should not proceed to call endBoundSpan immediately. Instead, it should handle this case as a failed stream instrumentation, potentially by logging a warning, to avoid silently creating an incomplete trace.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/server-utils/src/integrations/mistral.ts#L64-L67

Potential issue: In the auto-instrumentation for Mistral streaming channels, the
`deferSpanEnd` callback's behavior depends on `instrumentEventStream`. If a streaming
method returns a result that is not a valid async-iterable (e.g., due to an SDK version
change), `instrumentEventStream` returns `false`. This causes the span to end
immediately instead of being deferred. The `beforeSpanEnd` callback,
`addResponseAttributes`, is then incorrectly called with the stream-like object. While
this does not crash due to defensive checks, it results in a silent failure where the
span closes prematurely without recording any stream data or response attributes,
leading to incomplete traces.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6d04ea5. Configure here.

if (choice.finishReason) {
state.finishReasons.push(choice.finishReason);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Streaming merges multiple choices

Medium Severity

Streamed completions with n greater than 1 fold every choice into one output. Text from all choices is concatenated, and tool calls are keyed only by index, so two choices that both emit index 0 overwrite or join each other's function.arguments. Non-streaming already keeps one gen_ai.output.messages entry per choice.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d04ea5. Configure here.

@JPeer264
JPeer264 merged commit ecae057 into develop Sep 15, 2026
624 of 629 checks passed
@JPeer264
JPeer264 deleted the nh/mistral-integration branch September 15, 2026 11:10
RulaKhaled added a commit that referenced this pull request Sep 15, 2026
…24378)

Stacked on #24243. Adds an E2E test app for the Mistral integration.

The suite runs twice against the same Express app, selected by TEST_ENV.
development serves unbundled ESM through the runtime --import hook;
production serves an esbuild bundle transformed by sentryEsbuildPlugin.
Every assertion runs against both paths, which matters most for a
provider that ships ESM-only.

What it covers:

- gen_ai spans for streaming and non-streaming calls, with request,
response and usage attributes.
- The tee() and pipeThrough drain paths, each asserting one span and
only one.
- A failed call reaching Sentry as an error, with the span marked
errored and on the same trace.
- A manual span under the request span, and the gen_ai span directly
under the manual one.
- dataloader spans in the same trace as gen_ai spans.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
andreiborza added a commit to getsentry/sentry-docs that referenced this pull request Sep 22, 2026
Documents the Mistral AI integration added in
getsentry/sentry-javascript#24243, including automatic and manual setup,
privacy controls, and supported operations. Restricts the page and
discovery links to supported platforms.

## IS YOUR CHANGE URGENT?

- [x] No deadline: Not urgent, can wait up to 1 week+

---------

Co-authored-by: GPT-6 <codex@openai.com>
Co-authored-by: Andrei <168741329+andreiborza@users.noreply.github.com>
Co-authored-by: Jan Peer Stöcklmair <jan.peer@sentry.io>
Co-authored-by: Andrei Borza <andrei.borza@sentry.io>
nicohrubec added a commit that referenced this pull request Sep 23, 2026
Export `instrumentMistralAiClient` from the SvelteKit and Remix server
entry points, matching their existing AI manual wrappers. Completes the
wrapper exports for #24243.

---------

Co-authored-by: GPT-6 <codex@openai.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants