Skip to content

test(server-utils): Cover the Flue instrumentation - #24266

Merged
RulaKhaled merged 12 commits into
developfrom
feat/flue-instrumentation-tests
Sep 17, 2026
Merged

RulaKhaled merged 12 commits into
developfrom
feat/flue-instrumentation-tests

Conversation

@RulaKhaled

@RulaKhaled RulaKhaled commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #24265 — review that first, this is tests only.

Unit coverage for the instrumentation, plus an integration suite that drives an agent through a tool call and asserts the invoke_agentchat / execute_tool hierarchy — including that tool spans are siblings of chat rather than children, matching how Flue's own OpenTelemetry adapter projects them.

The scenario uses pi-ai's faux provider so responses are scripted in-process and no provider key is needed. It is ESM only (@flue/runtime has no require export condition) and installed per-suite, since engines.node >= 22.19 would break yarn install on the Node 20 lane. Guarded by conditionalTest({ min: 22 }), so it skips on the repo's default Node 20.

Each case was mutation-tested rather than just run green, which is what caught the tests that passed vacuously and an unreachable branch in trackSpan that has since been removed.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 29.09 kB - -
@sentry/browser - with treeshaking flags 27.35 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.26 kB - -
@sentry/browser (incl. Tracing) 50.6 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 50.62 kB - -
@sentry/browser (incl. Tracing, Profiling) 53.61 kB - -
@sentry/browser (incl. Tracing, Replay) 90.15 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 79.25 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 94.85 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 107.83 kB - -
@sentry/browser (incl. Feedback) 46.62 kB - -
@sentry/browser (incl. sendFeedback) 34.15 kB - -
@sentry/browser (incl. FeedbackAsync) 39.26 kB - -
@sentry/browser (incl. Metrics) 30.1 kB - -
@sentry/browser (incl. Logs) 30.35 kB - -
@sentry/browser (incl. Metrics & Logs) 31.02 kB - -
@sentry/react 30.84 kB - -
@sentry/react (incl. Tracing) 52.94 kB - -
@sentry/vue 36.34 kB - -
@sentry/vue (incl. Tracing) 52.91 kB - -
@sentry/svelte 29.11 kB - -
CDN Bundle 30.8 kB - -
CDN Bundle (incl. Tracing) 51.15 kB - -
CDN Bundle (incl. Logs, Metrics) 33.06 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 53.14 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 73.75 kB - -
CDN Bundle (incl. Tracing, Replay) 88.69 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 90.63 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 94.73 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 96.78 kB - -
CDN Bundle - uncompressed 91.16 kB - -
CDN Bundle (incl. Tracing) - uncompressed 152.66 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 97.73 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 158.61 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 227.14 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 272.23 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 278.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 285.93 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 291.86 kB - -
@sentry/nextjs (client) 55.27 kB - -
@sentry/sveltekit (client) 51.05 kB - -
@sentry/core/server 39.63 kB - -
@sentry/core/browser 13.66 kB - -
@sentry/node 132.37 kB +0.02% +18 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.03 kB - -
@sentry/node - without tracing 89.82 kB +0.04% +30 B 🔺
@sentry/node - without channel injection 111.23 kB +0.02% +17 B 🔺
@sentry/aws-serverless 98.06 kB +0.03% +24 B 🔺
@sentry/cloudflare (withSentry) - minified 204.52 kB - -
@sentry/cloudflare (withSentry) 508.73 kB - -

View base workflow run

@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from 89239da to deb3330 Compare September 9, 2026 19:00
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from deb3330 to bdda8e5 Compare September 9, 2026 19:08
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from bdda8e5 to 612798a Compare September 10, 2026 07:41
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch 4 times, most recently from 74f4654 to cd565ee Compare September 11, 2026 12:53
@RulaKhaled
RulaKhaled marked this pull request as ready for review September 14, 2026 07:46
@RulaKhaled
RulaKhaled requested review from a team as code owners September 14, 2026 07:46
@RulaKhaled
RulaKhaled requested review from JPeer264 and mydea and removed request for a team September 14, 2026 07:46
Comment thread dev-packages/node-integration-tests/suites/tracing/flue/test.ts Outdated

@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/test/ai/lib/tracing/flue.test.ts
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from d69cb28 to a7e44b7 Compare September 14, 2026 07:50
@RulaKhaled
RulaKhaled added this pull request to stack #24376 September 14, 2026 13:33
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch 2 times, most recently from 965a470 to ace67d3 Compare September 14, 2026 13:53
Comment thread dev-packages/node-integration-tests/suites/tracing/flue/test.ts
@RulaKhaled
RulaKhaled requested a review from a team as a code owner September 14, 2026 14:27
@RulaKhaled
RulaKhaled requested review from s1gr1d and removed request for a team September 14, 2026 14:27
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from ace67d3 to da37aa7 Compare September 14, 2026 14:27
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from da37aa7 to 354d0e9 Compare September 14, 2026 20:04
Base automatically changed from feat/flue-instrumentation-base to develop September 15, 2026 08:29
RulaKhaled added a commit that referenced this pull request Sep 15, 2026
Instruments the Flue agent framework (`@flue/runtime`) through its own
`instrument()` hook, producing the `invoke_agent` → `chat` /
`execute_tool` hierarchy with token usage, Flue-computed cost and
message content.

```ts
import { instrument } from '@flue/runtime';
import * as Sentry from '@sentry/node';

instrument(Sentry.createFlueInstrumentation());
```

Verified against a scaffolded `flue init` app driven over HTTP against a
real provider, on Node and Cloudflare:

```
http.server  POST
  └─ gen_ai.invoke_agent  invoke_agent Hello
     ├─ gen_ai.chat  chat anthropic/claude-haiku-4.5  [988 in / 123 out / $0.001603]
     ├─ gen_ai.execute_tool  execute_tool get_weather
     └─ gen_ai.chat  chat anthropic/claude-haiku-4.5  [1035 in / 27 out / $0.00117]
```

_Root cause of the shape_: Flue is not instrumented at a call site — it
exposes `instrument()`, a registration API whose registry is
module-scope state. An auto-registering integration would need a
reference to that module's own binding, and no channel payload carries
one (`instrument` appears in Flue's build only as the function
definition and in its `export {}` list, never as a property, argument or
return value). Registration is therefore left to the user, which is also
Flue's documented pattern for observability providers, and needs neither
the runtime hook nor a bundler plugin.

The two callbacks own different halves: the interceptor owns the agent
span and the active context, so spans opened underneath parent
correctly; `observe` owns the turn and tool spans, because
`turn_start`/`turn` are the only signal one-to-one with a model call and
`turn` is what carries usage. Spanning the `model` operation instead
does not work — 12 fire per turn, and the first resolves long before
usage is known.

Flue reaches providers through `@earendil-works/pi-ai`, which bundles
the `openai`, `@anthropic-ai/sdk` and `@google/genai` clients, so those
are skipped while Flue is instrumented; the skip lives in
`createFlueInstrumentation` so it applies however the instrumentation is
registered.

Turn and tool spans are tracked in `LRUMap`s rather than plain maps.
Both are keyed off an id that only the matching end observation removes,
and a stream abandoned mid-turn never emits one, so an uncapped map
would grow for the lifetime of the process. Eviction ends the span it
drops instead of letting it disappear unsent, which is the same trade
Mastra's exporter makes.

**On the Sentry bundler plugin**: not required, and not recommended for
Flue's sake. Span trees are identical with and without it on both
targets — HTTP spans come from Node's native `diagnostics_channel`, and
the provider skip fires either way. It is worth adding only if the app
also uses libraries that need orchestrion (pg, redis, kafka…); for a
pure Flue app it force-bundles `@flue/runtime` (55KB → 4.3MB measured)
for no telemetry gain.

The export is added to every runtime that re-exports `@sentry/node`.
`astro` and `elysia` need it named explicitly because they keep
hand-maintained export lists (Vite puts a wildcard re-export under
`default` in Astro prod builds); `nextjs`, `remix` and `sveltekit` use a
real `export *` and pick it up on their own.

Tests are stacked in #24266.

Known gaps: Flue ships its own Sentry blueprint (`flue add tooling
sentry`) targeting `@sentry/node@^10.64.0`, which our docs currently
point at — needs a docs update, same shape as the `@mastra/sentry`
migration.

Fixes #24017

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from 354d0e9 to d2059a7 Compare September 15, 2026 08:29

@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 d2059a7. Configure here.

Comment thread dev-packages/node-integration-tests/suites/tracing/flue/test.ts
import { loggingTransport } from '@sentry-internal/node-integration-tests';

Sentry.init({
traceLifecycle: 'static',

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/q: Any reason static was used? We'd like to move over to stream tests instead

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no real reason, copied the shape from the other gen-ai suites

await flue[Symbol.asyncDispose]?.();
});

await Sentry.flush(2000);

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.

q: Is it required that users have to use Sentry.flush manually too when they use flue?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

nope, nothing flue specific. turn/tool spans end on their own observations and agent spans end when the startSpan callback settles.. it's there because the scenario is a script that exits the moment the run finishes

RulaKhaled and others added 12 commits September 17, 2026 14:09
Unit tests over `createFlueInstrumentation` for the span shapes, the conversation
id lifted off the re-entered agent operation, the usage/cost mapping, the
all-zero-usage guard on failed turns, tool spans, content recording and its
`recordInputs`/`recordOutputs` gating, and dispose.

The integration test drives a real agent through a tool call using `pi-ai`'s
`faux` provider, so the run is deterministic and needs no provider key or mock
server. ESM only: `@flue/runtime` has no `require` export condition, and it is
installed per-suite because its `engines.node >= 22.19` would break `yarn
install` on the Node 20 CI matrix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Concurrent agent runs, subagent delegation (adopted from Isaac's repro, moved
into the suite and asserted through a helper so a span-order assumption cannot
creep back), the agent name arriving via the observations, trace continuation
from the replayed traceparent, the provider skip applying on first use and
re-applying after a registry reset, the recording options following the current
client, and the conventional request attributes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both tests fail against the previous implementation: the first leaves `openai`
registered before the run so the old first-entry-only guard short-circuits, and
the second overflows the turn tracker to prove the evicted span is ended rather
than dropped unsent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `model` and `tool` interceptor branches open no span; they make the span
`observe` already opened active so the provider's HTTP call and the tool's own
work nest inside it. Deleting both branches left all 28 tests green, and the
e2e does not reach it either: its parent assertions come from the observation
stream firing inside the agent operation, and nothing in that scenario opens a
span inside a tool or model operation.

Each case fails when its own branch is removed, and neither fails for the
other's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both cases passed vacuously. `not.toBe(TRACE_ID)` on the malformed carrier also
held when no agent span was opened at all, since `undefined` is not the carrier's
id either; suppressing span creation entirely failed nine other tests and left
that one green. And the active-trace case never checked the span landed on the
surrounding request's trace, so continuing an unrelated new trace passed too.

Now the first asserts a well-formed trace id, and the second asserts the agent
span carries the `incoming request` span's own trace id. Dropping the
`!getActiveSpan()` guard fails the second.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fails without the capture: the span is still errored, so only the error event
distinguishes the two.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Guards the concern raised on the Mastra error-capture PR: if a second capture
path is added, or the error starts reaching the global handlers, this fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`toContain` plus `find` left the integration test blind to duplicate spans and
to everything about the second turn. Count each span kind, assert both `chat`
spans rather than whichever one `find` returned first, and tie the agent span to
the surrounding transaction.

Mutation-checked: dropping the submission wrapper guard, mis-parenting the
second turn, and dropping its usage attributes each fail now; the last two
passed before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tool declared no `input` schema, so Flue validated the model's arguments
against an empty one and rejected them with "must not have additional
properties". The tool body never ran. Nothing caught it: a rejected call still
produces an `execute_tool` span with the right name and parent, and the suite
only asserted those.

Declare the schema, read the arguments from `ctx.data` where Flue passes them,
and assert the span settles `ok` so the same silent failure can't come back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite inherited `traceLifecycle: 'static'` from the other gen-AI suites,
where gen_ai spans reach the assertions by a detour: `extractGenAiSpansFromEvent`
lifts them out of the transaction and re-emits them as a span container. Nothing
in the Flue instrumentation depends on that, and streaming is what users get by
default.

On `stream` there is no transaction envelope — the `flue-test` root arrives in
the same container as a segment span, so the root id comes from there instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@RulaKhaled
RulaKhaled force-pushed the feat/flue-instrumentation-tests branch from 33ed5cf to 1cfa8dc Compare September 17, 2026 11:09
@RulaKhaled
RulaKhaled merged commit 8317107 into develop Sep 17, 2026
241 checks passed
@RulaKhaled
RulaKhaled deleted the feat/flue-instrumentation-tests branch September 17, 2026 11:25
RulaKhaled added a commit that referenced this pull request Sep 17, 2026
Stacked on #24266 — review that first, this is the e2e app only.

The node-integration-test calls `__flueBindAgentModule` by hand,
standing in for what `@flue/vite` does from the `'use agent'` directive,
so it can't show whether a scaffolded app actually works. This is `flue
init` output, built and served the way a user runs it, against a real
provider.

Covered, in both dev and prod and in both the base and orchestrion
variants:

- `invoke_agent` / `chat` / `execute_tool` with usage and cost
- a thrown tool error becoming an issue
- a manual span inside a tool nesting under `execute_tool`
- an orchestrion-instrumented `dataloader` span landing in the agent's
trace
- the provider's `http.client` call nesting inside `chat`

The loader runs inside a tool rather than a route so its span shares the
agent's trace. No build externals are needed unlike `node-eve` — a Flue
node build leaves dependencies as bare specifiers, so `dataloader` stays
a real module for the transform to hook. `@flue/*` is pinned because the
internal registry proxy 403s on releases it hasn't scanned.

_Found_: a thrown tool error produced an errored span and no issue at
all, since Flue catches the throw and hands it back to the model as a
tool result. Fixed in #24265.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.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.

2 participants