Skip to content

feat(examples): TanStack AI otelMiddleware recipe (TH-8238) - #212

Open
nik13 wants to merge 19 commits into
devfrom
feat/th-8238-tanstack-ai
Open

nik13 wants to merge 19 commits into
devfrom
feat/th-8238-tanstack-ai

Conversation

@nik13

@nik13 nik13 commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Summary

TanStack AI recipe for TH-8238. No traceAI package: TanStack AI's own otelMiddleware creates the spans, and @traceai/fi-core supplies the provider and the exporter. This PR adds a runnable example under typescript/examples/tanstack-ai/ and a shared-harness contract test.

  • @tanstack/ai 0.64.0 is a dependency of the example only. The example has its own install outside the pnpm workspace, so the root lockfile is untouched.
  • Import path: otelMiddleware is imported from @tanstack/ai/middlewares/otel. It is not exported from @tanstack/ai (src/middlewares/index.ts:15-18). The docs draft gets the same fix.
  • Kinds: model-call spans are LLM and tool spans are TOOL. The chat root is AGENT when it ran more than one model call.
    • Kinds are keyed on the middleware's span type rather than the operation name. Only model-call spans carry gen_ai.operation.name (otel.ts:623-628).
    • The root's kind is set in onSpanEnd, which runs before the root ends.
  • Token double-count fixed: TanStack sums every call's usage onto the root (applyRootUsage). Future AGI sums promoted gen_ai.usage.* over the whole trace, so the recipe moves every root usage key (including cache-read and reasoning tokens) to tanstack.ai.root_usage.*. It does so only when the model-call spans carry usage; otherwise the root keeps it as the only copy. The internal sdk-trace span the move edits is pinned (overrides sdk-trace-node 2.11.0) and guarded by tests.
  • Session: the caller's thread id is sent as session.id on every span.
  • Failures: a failed model call still exports its error spans. The flush on the request path is bounded, so a hanging collector no longer holds the response for about 10 s. The README snippet is the tested recipe.
  • Content: captureContent stays at its default (false). Flush and shutdown never throw, and a down collector is logged without failing the request.

Deviations (with evidence)

  • No batch processor. With fi-core 1.0.0 on sdk-trace 2.x, register({batch: true}) does not attach the batch processor. That bug is filed separately; the example flushes in finally.
  • The example is plain .mjs, so node runs it on Node 20 without a TS toolchain.

Tests (run on d871239, clean tree, by the coordinator)

PYTHONPATH="python/tests" uv run --no-project --python 3.11 --with pytest --with protobuf --with opentelemetry-proto pytest typescript/examples/tanstack-ai/tests -q -p no:cacheprovider --noconftest -o addopts= -rs
(cd typescript/examples/tanstack-ai && node --test tests/span_kinds.test.mjs)
Node 26.8.1: contract 15 passed; node --test 10/10
Node 20.20.2: contract 15 passed; node --test 10/10

The implementer also ran Node 22.23.3 (15 passed, 10/10). The root typescript/pnpm-lock.yaml is unchanged, and every change is under typescript/examples/tanstack-ai/.

Limits

  • No live vendor call, and no real fi-collector.
  • gen_ai.usage.cost and cache-creation tokens were not exercised.
  • A docs PR is not opened here; the draft is in company-brain PR Dev #62.

Review

Round 1, an independent fresh-context review of 04409aa (a subagent, not an installed card): CHANGES_REQUESTED. Fixed test first.

ID Finding Resolution Commit
R1 (blocking) A failed model call (stream:false) exported nothing Error spans exported; fake model returns HTTP 500 73d9943
R2 The README snippet double-counts, and its flush can replace the answer README is now the tested recipe (2 failed before) 98f7444
R3 Only 3 root usage keys moved Every usage key moved off the root 4a114d1
R4 No session, and the gap not declared Thread id becomes session.id 48e2af0
R5 A hanging collector delays the response ~10 s Bounded flush (13.5 s → about 2.2 s) c6001eb
R7 Internal-API dependency, pin, missing tests, key hygiene Pinned and guarded (056bb44); usage-branch tests catch mutation M2 (4a114d1); placeholder-key checks (5d175af) 056bb44, 4a114d1, 5d175af

R6 was out of scope for this round, as the review noted. Round 2: installed pr-reviewer, then pr-verifier, on 5d175af. Verdicts will be added here.

Round 2 follow-ups (5d175af..d871239)

The installed review passed this PR at 5d175af: pr-reviewer t_f2e3000c APPROVE, pr-verifier t_5ae10b65 VERIFIED. Its findings were fixed before the demo. Run against 5d175af, the new tests give 3 contract failures and 2 node-test failures.

ID Finding Resolution
N1 The sdk-trace pin does not travel with a copied tracing.mjs README: copy the overrides too (87a19fb)
N2 Serverless flush and in-flight limits undocumented README Notes (d871239)
N3 A disconnect without an abortController leaves spans open README route and chat.mjs wire the request signal to chat({abortController}). A new contract journey with a real client disconnect passes for both. Before the fix: "route still running 6 s after the client disconnected; exported spans: []" (a9555c4)
N4 engines >=20 admits Node versions the SDK does not support >=20.6.0; the README lists only Node versions that ran (186831f)
N5 threadId is copied verbatim to session.id README advises an opaque id (9bf9cf8)
N6 The recipe replaced the global tracer provider setGlobalTracerProvider: false, tested (cbb2aa0)
N7 Error text is exported even with captureContent off README privacy; test pins it (43f4e23)

Round 3 (installed pr-verifier t_f65677a1 at d871239): VERIFIED, ready to merge.

  • N1-N7 are confirmed fixed.
  • No regressions:
    • Promoted usage still lands once per model call, including on the new abort path.
    • No key material appears in spans or stdout.
    • The flush bound cannot hang or throw.

Review is complete at d871239. One P3 follow-up for after merge. It is pre-existing, not introduced here:

  • In an app with its own OpenTelemetry context manager (the setup N6 now allows), the TanStack chat root span inherits the app's active span as its parent. The Future AGI trace then shows a parent that was exported elsewhere. Two possible mitigations: return { ...options, root: true } for chat spans from onBeforeSpanStart, or document it. Spans still export and are counted once.

Video demo

Narrated terminal demo, 6:00, recorded at head d871239 (the reviewed head), 1080p H.264/AAC, 9 chapters. sha256 b920099b53d9c28b29745c18e6f36df3a0761764e1c695c86e0c84fc3583582f.

The video, captions, transcript, chapter list, preview, recorded terminal output and media check are attached privately to Linear TH-8238 (Future AGI workspace access needed). Every run uses a loopback fake OpenAI-compatible model, placeholder keys and the shared harness receiver: no vendor call and no live fi-collector.

Time Chapter
00:00 The problem
00:53 Head d871239: branch, 0 uncommitted files, 16 commits on 3eaadc8
01:18 Chat (Node 26): 4 spans in 1 trace (AGENT / LLM / TOOL / LLM), 4 exports to /tracer/v1/traces with both auth headers (values not shown); gen_ai.usage.input_tokens over the trace 11 + 23 = 34 with none on the root (TanStack's sum kept as tanstack.ai.root_usage.input_tokens=34); session.id from chat()'s threadId on 4/4 spans; no content markers, keys or span events by default, and the capture-on control fixture shows the check catches them; a failing model call exits 1 with two ERROR spans
02:30 Disconnect: a real HTTP client disconnects mid-stream; the README route and src/chat.mjs stop within 1 s and export both spans ended as cancelled; at cbb2aa0 (before the fix) the route is still running after 6 s and nothing is exported
03:03 Global provider: with setGlobalTracerProvider: false the app keeps its own provider; at 5d175af fi-core took the global and the app's span went to Future AGI
03:37 Node 20.20.2: engines >=20.6.0; the 15-test contract suite passed on the first take, including test_silent_collector_does_not_hold_the_response
04:19 The change: N3 abort wiring, N6 register options, N4 engines, N7 README privacy note
04:58 Tests (Node 26): contract 15 passed, node --test span kinds 10/10, 0 uncommitted files after
05:27 Review and limits

The host was loaded during recording, so VHS compresses idle time; printed timings are real and no output was removed. Checked by the coordinator: sha256, full decode, chapter markers, no pause of 2.5 s or more, frames per chapter against the driver output and the recorded terminal output, no keys or header values in frames, captions or transcript.

Stacked on #203. Not merged.

Linear: TH-8238. approved: Nikhil 2026-10-03 blanket

nik13 added 5 commits October 3, 2026 18:58
The Node OTLP exporter streams with Transfer-Encoding: chunked and sends
no Content-Length. The Receiver read Content-Length only, so every Node
export came back 400 with zero spans; two TH-8103 children had to put a
de-chunking relay in front of it. Receiver now accepts both framings.

It also records one entry per accepted export (path, lower-cased
headers, flattened resource attributes) via requests(), so a contract
test can assert X-Api-Key/X-Secret-Key and project_name through the
shared harness instead of a private recorder.

Verified: 6 harness tests pass, and the TanStack example's real Node
exporter delivered 4 chunked exports (4 spans, collector path, both
auth headers, project_name/project_type) with no relay.

Refs: TH-8339, TH-8103
No @traceai/tanstack-ai package. The example registers a Future AGI
tracer provider with @traceai/fi-core register() (OTLP/HTTP to
/tracer/v1/traces, X-Api-Key and X-Secret-Key, project_name and
project_type=observe), passes its tracer to TanStack AI's otelMiddleware
from @tanstack/ai/middlewares/otel, and leaves captureContent at its
default of false. Span kinds are set from the middleware's span scope:
model call LLM, tool TOOL, multi-call root AGENT. The route flushes in
finally and an unreachable collector never fails chat().

@tanstack/ai 0.64.0 and @tanstack/ai-openai 0.26.0 are pinned as
dependencies of the example only. It has its own npm install and is
outside the pnpm workspace, so the root lockfile is unchanged.

The contract test runs the example with node against a loopback fake
OpenAI server and the shared harness Receiver. It asserts model, usage,
span kinds and nesting, the absence of prompt, completion and tool
content (with a capture-on control), auth headers and project resource
attributes, a refused collector port, and abort. A recording relay
de-chunks the exporter's chunked body, which the Receiver cannot read.

Refs: TH-8238
otelMiddleware sums every model call's usage onto the chat root
(@tanstack/ai 0.64.0 otel.ts applyRootUsage, set just before onSpanEnd).
fi-collector promotes gen_ai.usage.* into the token columns on any span
(adapter.go) and Observe sums total_tokens over a trace, so totals
doubled. The recipe's onSpanEnd now moves the root's sum to
tanstack.ai.root_usage.* when the model-call spans carry usage, and
leaves it when they do not (then the root is the only copy).

Test first: the contract test now asserts the summed promoted input
tokens equal the two model calls. 7 passed.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
nik13 added 14 commits October 4, 2026 16:44
The route called chat({ stream: false }). At @tanstack/ai 0.64.0 that
collects text with streamToText, which throws at the first RUN_ERROR
and stops reading (stream-to-response.ts). The engine's finally only
fires onAbort when the run was cancelled, so onError never ran and
otelMiddleware never ended the chat or model-call span. A model HTTP
500 failed the request and exported nothing.

answer() now reads chat()'s stream to the end, keeps the first
RUN_ERROR, and throws its message after the stream ends. The CLI
prints "chat failed: <message>" and exits 1, so the caller still gets
an error response.

Test first: the fake OpenAI server gains fail_status=500. The new
contract test failed with no export at all, and now sees the root and
model-call spans exported with status ERROR and message "500 boom".

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
applyRootUsage (@tanstack/ai 0.64.0 otel.ts) sums every gen_ai.usage.*
key onto the root: cost, cache read/creation and reasoning tokens as
well as input/output/total. The recipe moved only the three token
keys, so cache, reasoning and cost were still counted twice per trace.

When the model-call spans carry usage, every gen_ai.usage.<suffix> key
on the root now moves to tanstack.ai.root_usage.<suffix> (full suffix,
so cache_read.input_tokens and reasoning.output_tokens do not collide)
and every gen_ai.cost.<suffix> key to tanstack.ai.root_usage.cost.<suffix>.

Test first: the contract test asserts no gen_ai.usage.*/gen_ai.cost.*
key on the root and trace sums equal to the per-call sums for input,
output, total, cache_read and reasoning tokens; it failed on the root's
cache_read and reasoning keys. New tests/span_kinds.test.mjs (node
--test, run from pytest) drives futureAgiSpanKinds() with fake spans:
no model call reported usage (root keeps it; kills reviewer mutation
M2), one model call (no kind, nothing promoted on the root, trace sum
equals the call), two calls with cost and cache keys (failed before
this change).

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
moveRootUsage deletes keys from the SDK span's attributes object, which
is not public API. It now names that dependency and the tested version
(@opentelemetry/sdk-trace 2.11.0 SpanImpl), deletes before it sets, and
package.json "overrides" pins @opentelemetry/sdk-trace-node 2.11.0,
which pins sdk-trace-base and sdk-trace 2.11.0 exactly (fi-core asks
for ^2.0.1). npm ls shows sdk-trace-node@2.11.0 overridden.

Delete-then-set does not by itself save the copy under a tight
attributeCountLimit: delete does not lower SpanImpl's _attributesCount,
so the tanstack.ai.root_usage.* copy can still be dropped. The comment
and README say so. The promoted keys always leave the root, so usage is
never counted twice.

Tests (tests/span_kinds.test.mjs): the sdk-trace-node, sdk-trace-base
and sdk-trace versions fi-core resolves are 2.11.0; the move reaches a
span exported by that SDK; with attributeCountLimit 5 no promoted key
stays on the root and per-call input and output sums still hold. These
guard existing behaviour, so they passed before the change; with the
delete removed (mutation) four of six unit tests fail.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
The architecture says the recipe shows the traceAI session helper, but
@traceai/fi-core setSession() only sets an OpenTelemetry context value.
otelMiddleware never reads it, and register()'s provider hands out the
plain SDK tracer, so no session reached the spans.

futureAgiOtelMiddleware(tracer, { threadIdAsSession: true }) now adds
session.id = ctx.threadId (ChatMiddlewareContext.threadId, @tanstack/ai
0.64.0 middleware/types.ts:207) to every span from attributeEnricher.
It is opt-in because chat() generates thread-<ms>-<random> when the
caller passes no threadId, which would make each request a session.
chatRoute and answer accept { threadId }; the CLI takes it as argv[3].
The README documents this as a deviation from the spec's helper.

Test first: the contract run now passes a thread id and asserts
session.id on all four spans (KeyError before), and the failed-call run
(no thread id) asserts none. Unit tests cover the enricher with and
without threadIdAsSession, including tool spans.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
flushTraces() awaited forceFlush() with no bound. A collector that
accepts the connection and never answers held the route until the
OTLP/HTTP exporter's 10 s timeout (otlp-exporter-base 0.202.0).

flushTraces(tracerProvider, { timeoutMs = FLUSH_TIMEOUT_MS = 2000 })
races the flush against an unref'd timer, logs "[futureagi] span export
still pending after 2000 ms; not waiting", lets the export finish in the
background, and still never throws. The README and the doc comment say
the per-request flush is for serverless routes and that a long-lived
server should not await a flush on the request path (the
SimpleSpanProcessor already exports each span as it ends) and should
call shutdownTraces() at exit.

Test first: tests/timed_route.mjs prints the route's duration, and the
new contract test points the exporter at a socket that accepts and never
replies. It failed with routeMs 13504 and now passes (routeMs 2157 in a
manual run). The refused-port test still passes.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
…sted

The README's "The recipe" snippet passed a bare otelMiddleware to chat():
no span kinds and no root usage move, so Future AGI would count every
model call's usage twice. It also told the reader to await
tracerProvider.forceFlush() in the route's finally, which rejects when
the collector is down and then replaces the route's answer, and can
wait for the exporter's 10 s timeout.

The snippet is now a complete route on futureAgiOtelMiddleware (span
kinds, usage move, optional session.id from threadId) and flushTraces
(bounded at 2 s, never throws), and it drains chat()'s stream instead of
using the non-streaming mode, so a failed model call still exports. The
README lists the new test fixtures.

Test first: test_readme_recipe_uses_the_recipe_middleware_and_bounded_flush
checks the snippet uses futureAgiOtelMiddleware and flushTraces, has no
bare otelMiddleware({, forceFlush or stream: false, and imports only
names src/tracing.mjs exports. test_readme_recipe_runs_and_exports_the_contract_spans
runs the snippet as written against the fake model and the Receiver
(one model call: LLM kind, no kind and no promoted usage on the root,
session.id on both spans, no prompt text). On the old README both
failed: the first on 'futureAgiOtelMiddleware(' not in the snippet, the
second with "ReferenceError: chatRoute is not defined".

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
…tput

Review asked for proof that FI_API_KEY, FI_SECRET_KEY and OPENAI_API_KEY
never reach an exported span, stdout or stderr. The contract test now
checks every run for the three placeholder keys: the shipped route, the
failed model call (which prints the provider error), the refused and the
silent collector (which print exporter errors) and the README snippet.
A positive control first shows the keys were in use: x-secret-key on
every export and "Bearer placeholder-openai-key" on every model call
(the fake now records Authorization; the fixture keeps it).

The recipe already keeps the keys out, so these pass on c6001eb by
design. Mutations show they are load-bearing, each reverted with git
checkout: the enricher copying FI_SECRET_KEY onto spans fails 2 tests
('placeholder-fi-secret-key', 'spans'), and the CLI error line printing
OPENAI_API_KEY fails test_failed_model_call_exports_error_spans
('placeholder-openai-key', 'stderr').

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
…ider

Installed review N6: registerFutureAgiTracing() called register() with
its default setGlobalTracerProvider: true, so the Future AGI provider
took the process's global OpenTelemetry slot. The recipe never uses the
global: it passes tracerProvider.getTracer() to otelMiddleware. In an
app with its own OpenTelemetry setup that either fails to register the
app's provider or sends the app's other spans to Future AGI, and removing
the recipe takes more than dropping the middleware.

register() now gets setGlobalTracerProvider: false (an option of
@traceai/fi-core 1.0.0). The exported spans are unchanged: the contract
run still exports the same 4 spans.

Test first: span_kinds.test.mjs "registerFutureAgiTracing() leaves the
global tracer provider alone" registers with placeholder settings and
asserts the global proxy's delegate is not the returned provider and that
trace.setGlobalTracerProvider() still succeeds for the app's own
provider. Before the change it failed: "Expected "actual" not to be
reference-equal to "expected"" (node --test 8 pass, 1 fail).

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
Installed review N3 (PRD J3 / AC-05): neither the README route nor
src/chat.mjs told chat() when the client went away. TanStack ends the
otelMiddleware spans only when the run finishes, fails or is aborted
through chat()'s own abortController (@tanstack/ai 0.64.0
activities/chat/index.ts 1470-1480, isCancelled at 3732-3734). A route
that stops reading, or a server whose client disconnects without that
controller aborting, leaves every span open and exports nothing.

Both routes now take the request's abort signal (`signal`: request.signal
in a Fetch API handler, or a controller aborted on res.on("close") in
node:http) and pass chat() an AbortController that the signal aborts. The
README says never to leave the stream without aborting that controller,
and to pass the same controller to toServerSentEventsResponse when
streaming to the client. answer() in src/chat.mjs takes the controller.

Test first: test_client_disconnect_mid_stream_ends_and_exports_every_span
[readme] and [chat.mjs]. tests/disconnect_route.mjs serves one HTTP
request with the route and aborts the signal when the connection closes;
the test sends a request, closes the socket once the stalled fake model
has sent its first chunks (FakeOpenAI.stalled), and then expects the
server to exit within 6 s with ROOT and ITERATION_0 exported, both ERROR
"cancelled" with tanstack.ai.completion.reason=cancelled, and no
placeholder key in spans or output. Before the change both failed:
"route still running 6 s after the client disconnected; exported spans:
[]" (2 failed). After: 2 passed; full contract suite 15 passed and
node --test 9/9 on Node 26.8.1.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
Installed review N4: package.json allowed Node ">=20", but the pinned
@opentelemetry/sdk-trace-node, sdk-trace-base and sdk-trace 2.11.0
declare "^18.19.0 || >=20.6.0", so Node 20.0 to 20.5 was outside their
support. The README also claimed "Node 20, 22 and 26" without saying
which versions ran.

engines.node is now ">=20.6.0". No other installed package needs more on
the Node 20 line (the highest other floor is @tanstack/ai's ">=18"). The
README states the floor and lists the Node versions the suites ran on:
20.20.2, 22.23.3 and 26.8.1.

Test first: span_kinds.test.mjs "package.json engines starts at a Node
version the pinned SDK supports" reads the example's engines floor and
checks it against the engines range of each package in fi-core's SDK
chain. Before the change it failed: "engines.node >=20 allows Node
20.0.0, outside @opentelemetry/sdk-trace-node's ^18.19.0 || >=20.6.0"
(node --test 9 pass, 1 fail). After: 10/10 on Node 20.20.2, 22.23.3 and
26.8.1; contract suite 15 passed on each.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
…captureContent

Installed review N7: the README's privacy line said no prompt,
completion, system prompt or tool content reaches a span because
captureContent defaults to false. That holds for content, but on a
failure otelMiddleware sets the error's message as the span status and
records an exception event with message and stack trace, whatever
captureContent is (@tanstack/ai 0.64.0 middlewares/otel.ts 980-985 for
tools, 1061-1065 and 1100-1104 for model calls). That text comes from the
provider or the tool, so a provider error that repeats request content
reaches Future AGI.

The README Privacy section now says so.

test_failed_model_call_exports_error_spans pins the behaviour the README
describes: ROOT and ITERATION_0 each carry an exception event whose
exception.message contains the provider's "boom" and that has a stack
trace, and the prompt, city and system prompt markers are absent from
the error-path spans. It passes on the previous head by design: it pins
existing TanStack behaviour so an upgrade that changes it is noticed.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
Installed review N1: the README told readers to copy src/tracing.mjs, but
the @opentelemetry/sdk-trace-node 2.11.0 pin the root usage move was
tested on lives only in this example's package.json overrides. A copied
recipe resolves the SDK through @traceai/fi-core 1.0.0's ^2.0.1 range.
If an SDK version stopped exposing the span's plain attributes object,
the move (tracing.mjs moveRootUsage and the usage check in onSpanEnd)
would do nothing and every model call's usage would be counted twice,
with no warning.

"The recipe" now says the pin does not travel with the file, to copy the
overrides entry too (npm overrides; pnpm.overrides or resolutions for
pnpm and Yarn), and what goes wrong without it.

Documentation only; the README snippet is unchanged and its two tests
still pass.

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
Installed review N5: with threadIdAsSession the caller's thread id is
copied verbatim to session.id on every span (tracing.mjs attributeEnricher)
and so leaves the process, but the README did not say what the id should
be. chat() also takes the legacy conversationId as the thread id when
threadId is unset (@tanstack/ai 0.64.0 activities/chat/index.ts
1029-1035), so that value becomes the session too.

The README Session section and the futureAgiSpanKinds doc comment now
say to use an opaque id (a random UUID per conversation), never an email
address or user name, and that the same applies to conversationId.

Documentation only (a comment in src/tracing.mjs, no code change).

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
Installed review N2: the README gave the 2 s flush bound but not its
consequences.

- Serverless: flushTraces() stops waiting at the bound; it does not stop
  the export (tracing.mjs flushTraces leaves `flushed` running). Spans
  still pending then are lost if the runtime freezes or exits after the
  response. The `timeoutMs` option was not documented.
- Long-lived servers: register()'s default SimpleSpanProcessor
  (@traceai/fi-core 1.0.0 otel.js 356-358) exports each span in its own
  OTLP request. The OTLP/HTTP exporter (otlp-exporter-base 0.202.0
  shared-configuration.js 54-55) allows 30 exports in flight with a 10 s
  timeout each and rejects the rest with "Concurrent export limit
  reached" (otlp-export-delegate.js 43-46), so with a slow collector
  spans drop from about 8 concurrent chats of 4 spans.

The Notes section now says both and documents
flushTraces(tracerProvider, { timeoutMs }).

Documentation only. The slow-collector concurrency case is described,
not tested (no fixture for 10 concurrent routes against a delaying
collector).

approved: Nikhil 2026-10-03 blanket
Refs: TH-8238
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.

1 participant