Skip to content

TRACY-111: Parse providers' server-sent events and feed them to tracing adapters#227

Open
Vladislav0Art wants to merge 67 commits intomainfrom
vartiukhov/refactoring/change-tracing-of-event-stream-responses
Open

TRACY-111: Parse providers' server-sent events and feed them to tracing adapters#227
Vladislav0Art wants to merge 67 commits intomainfrom
vartiukhov/refactoring/change-tracing-of-event-stream-responses

Conversation

@Vladislav0Art
Copy link
Copy Markdown
Collaborator

@Vladislav0Art Vladislav0Art commented Mar 19, 2026

Motivation and Context

Closes https://youtrack.jetbrains.com/issue/TRACY-111

  1. Changed API of LLMTracingAdapter:
    1. getSpanName no longer accepts TracyHttpRequest instance (it never used it)
    2. ❗️ isStreamingRequest is removed: Ktor/OkHttp interceptors now decide whether the current response is of a streaming type by looking at its content type (should be text/event-stream)
    3. ❗️ handleStreaming is removed: it handled a fully concatenated string of all events received from the server; removed in favor of per-event handling via registerResponseStreamEvent.
    4. registerResponseStreamEvent is added: one public version called by interceptors (implemented as factory method calling the same-named protected method); the other one protected, intended to be implemented by provider-specific adapters.
  2. Changed API of EndpointApiHandler:
    1. handleStreaming replaced with handleStreamingEvent, which does per-event handling (see the rationale above)
  3. ❗️ Distinguished streaming responses via response content type in OkHttp and Ktor interceptors.
  4. ❗️Introduced SseParser that accepts UTF-8 decoded stream chunks and builds SSE events from them (implementation is SSE spec-compliant) + Wrote tests for the parser.
    1. This SseParser is used in conjunction with a stateful UTF8Decoder (a wrapper over standard java.nio.charset.CharsetDecoder) so that raw stream bytes are correctly converted to UTF8 strings and then passed to the SseParser; it is needed for correct handling of multi-byte UTF-8 sequences split across read boundaries.
  5. Correctly re-implemented handling of SSE events for OpenAI's chat completions and Responses API (see examples of traces below).
  6. Modified the signature of OpenAIApiUtils.setCommonResponseAttributes: now directly accepts response body as JsonObject (previously, accepted TracyHttpResponse)
  7. Fixed SseParser.dispatchEvent() to reset retryValue on early return (empty dataBuffer), preventing a retry: field in a data-less event block from carrying over to the next event.
  8. Close spans on buffered source close instead of the response body close, as the latter may never be closed by a client (e.g., an OpenAI SDK client doesn't close the response body on an erroneous response, yet the span MUST always be closed).
    1. See okhttp change, and Ktor change.

Examples of traces with streaming requests:

  1. OpenAI Responses API: here
  2. OpenAI Chat Completions: here

Affects: #223


TODOs

  1. TODO: test on a real server-sent-events stream (openai, anthropic, gemini - all event types)
  2. TODO: should be protected/internal? (about populateUnmappedAttributes)
  3. Test implementation on streaming tests: OpenAI, Gemini + manually investigate the traces structure.
  4. Manually send traces to Langfuse with SSE: see how it is rendered.
  5. ResponsesOpenAIApiEndpointHandlerTest.'test OpenAI responses API streaming' fails → fixed

Breaking Changes

  • LLMTracingAdapter.isStreamingRequest removed; streaming is now detected automatically from the text/event-stream content type.
  • LLMTracingAdapter.handleStreaming / EndpointApiHandler.handleStreaming removed; replaced by per-event registerResponseStreamEvent / handleStreamingEvent.
  • getSpanName no longer receives a TracyHttpRequest parameter.

@Vladislav0Art Vladislav0Art changed the title TRACY-111: Parse Providers' Server-Sent Events And Feed Them To Tracing Adapters TRACY-111: Parse Providers' Server-Sent Events and Feed Them to Tracing Adapters Mar 19, 2026
@Vladislav0Art Vladislav0Art changed the title TRACY-111: Parse Providers' Server-Sent Events and Feed Them to Tracing Adapters TRACY-111: Parse providers' server-sent events and feed them to tracing adapters Mar 19, 2026
@Vladislav0Art Vladislav0Art marked this pull request as ready for review March 23, 2026 15:23
@Vladislav0Art Vladislav0Art requested review from Copilot and georgiizorabov and removed request for Copilot March 23, 2026 15:23
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates Tracy’s tracing pipeline to correctly support Server-Sent Events (SSE) streaming responses by parsing event-stream chunks incrementally and feeding events into provider adapters, replacing the previous “concatenate all events into one string” approach.

Changes:

  • Introduces an SSE parser (SseParser) + tests, and routes streaming handling through per-event callbacks (SseEvent).
  • Refactors tracing adapter APIs (LLMTracingAdapter, EndpointApiHandler) to support per-event streaming handling and span naming without request input.
  • Updates OkHttp/Ktor instrumentation to detect SSE via text/event-stream and trace events as they arrive; updates OpenAI streaming handlers accordingly.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/clients/OpenAIClient.kt Updates imports after interceptor package move.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/handlers/images/ImagesCreateOpenAIApiEndpointHandler.kt Migrates OpenAI image streaming handling to per-SSE-event processing.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/handlers/images/ImagesCreateEditOpenAIApiEndpointHandler.kt Migrates OpenAI image edit streaming handling to per-SSE-event processing.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/handlers/ResponsesOpenAIApiEndpointHandler.kt Reworks Responses API streaming to handle SSE events and parse final “completed” payload.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/handlers/OpenAIApiUtils.kt Changes common response attribute helper to accept JsonObject directly.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/handlers/ChatCompletionsOpenAIApiEndpointHandler.kt Reworks chat completion streaming to accumulate deltas per SSE event.
tracing/openai/src/jvmMain/kotlin/org/jetbrains/ai/tracy/openai/adapters/OpenAILLMTracingAdapter.kt Adapts OpenAI adapter to new span naming + per-event streaming API.
tracing/ktor/src/jvmMain/kotlin/org/jetbrains/ai/tracy/ktor/KtorProtocolAdapters.kt Adjusts response view to carry TracyHttpResponseBody variants.
tracing/ktor/src/jvmMain/kotlin/org/jetbrains/ai/tracy/ktor/KtorHttpClient.kt Detects SSE by content type and streams events into adapter via SseParser.
tracing/gemini/src/jvmMain/kotlin/org/jetbrains/ai/tracy/gemini/clients/GeminiClient.kt Updates imports after interceptor package move.
tracing/gemini/src/jvmMain/kotlin/org/jetbrains/ai/tracy/gemini/adapters/handlers/GeminiImagenHandler.kt Updates handler API to handleStreamingEvent (unsupported).
tracing/gemini/src/jvmMain/kotlin/org/jetbrains/ai/tracy/gemini/adapters/handlers/GeminiContentGenHandler.kt Updates handler API to handleStreamingEvent (unsupported).
tracing/gemini/src/jvmMain/kotlin/org/jetbrains/ai/tracy/gemini/adapters/GeminiLLMTracingAdapter.kt Adapts Gemini adapter to new span naming + per-event streaming API.
tracing/core/src/jvmTest/kotlin/org/jetbrains/ai/tracy/core/http/parsers/SseParserTest.kt Adds test coverage for SSE parsing, including OpenAI examples.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt Updates OkHttp interceptor to detect SSE responses and parse events incrementally.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/Interceptors.kt Moves interceptor utilities into core.interceptors package.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/http/protocol/TracyHttpResponse.kt Adds EventStream and Empty response body variants.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/http/parsers/SseParser.kt Adds spec-oriented SSE parser and SseEvent model.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/adapters/handlers/sse/SseHandling.kt Adds standardized SSE handling failure Result + exception type.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/adapters/handlers/EndpointApiHandler.kt Replaces handleStreaming with per-event handleStreamingEvent.
tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/adapters/LLMTracingAdapter.kt Refactors adapter API for span naming and per-event SSE streaming registration.
tracing/anthropic/src/jvmTest/kotlin/org/jetbrains/ai/tracy/anthropic/AnthropicTracingTest.kt Updates imports after interceptor package move.
tracing/anthropic/src/jvmMain/kotlin/org/jetbrains/ai/tracy/anthropic/clients/AnthropicAIClient.kt Updates imports after interceptor package move.
tracing/anthropic/src/jvmMain/kotlin/org/jetbrains/ai/tracy/anthropic/adapters/AnthropicLLMTracingAdapter.kt Adapts Anthropic adapter to new span naming + per-event streaming API (unsupported).
examples/src/main/kotlin/org/jetbrains/ai/tracy/examples/clients/OkHttpClientAutotracingExample.kt Updates example imports after interceptor package move.
Comments suppressed due to low confidence (1)

tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:301

  • originalBodyWithTracedSSE.close() only ends the span and never closes the wrapped originalBody / source. This can leak the underlying connection/resources and also leaves SseParser cleanup dependent on the source being closed elsewhere. Ensure the wrapper’s close() closes the underlying body/source (and the parser) before ending the span (ideally by keeping a single BufferedSource instance and closing that, or delegating to originalBody.close()).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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.

let's change it to

body["temperature"]?.jsonPrimitive?.doubleOrNull?.let { span.setAttribute(GEN_AI_REQUEST_TEMPERATURE, it) }

and in similar places too?

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.

addressed in 8e44084.

NOTE: I modified the assignments present ONLY in this file, not everywhere.

@georgiizorabov
Copy link
Copy Markdown
Collaborator

Also, please fix this: Agents.md

event: SseEvent,
index: Long
): Result<Unit> {
return sseHandlingFailure("Unsupported")
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.

will this throw the warning on every streaming event?

Copy link
Copy Markdown
Collaborator Author

@Vladislav0Art Vladislav0Art Mar 25, 2026

Choose a reason for hiding this comment

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

Yes, it will. Should these unimplemented adapters be silenced instead?

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.

I think either once per trace if possible or mb silence, yes. WDYT?
@Vladislav0Art, @slawa4s, @agbragin

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.

I’d prefer logging it once per trace rather than silencing it

@Vladislav0Art Vladislav0Art force-pushed the vartiukhov/refactoring/change-tracing-of-event-stream-responses branch from 2f1b8a8 to 6939d0b Compare March 25, 2026 14:38
@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

Vladislav0Art commented Mar 25, 2026

Also, please fix this: Agents.md

addressed in 57c04b5.

@georgiizorabov hi! I addressed your comments. Please, have a look.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (2)

tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:300

  • withTracedSSE() overrides ResponseBody.close() but never closes the underlying originalBody/source. This can leak connections if the caller closes the response early (and it also prevents SseCapturingSource.close() / parser.close() from running). Ensure close() closes the wrapped source/body (e.g., call originalBody.close() or source().close() in a finally block) before ending the span.
    tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:385
  • SSE bytes are decoded to UTF-8 per read() call via readUtf8(bytesRead). If a multi-byte UTF-8 code point is split across reads, this will decode with replacement characters and corrupt the stream fed into SseParser (potentially breaking JSON parsing for non-ASCII content). Consider using an incremental UTF-8 decoder with carry-over bytes between reads, or feed raw bytes into the parser and decode there with a streaming decoder.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +258 to +260
sseParser.feed(
input = buffer.decodeToString(0, bytesRead)
)
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

buffer.decodeToString(0, bytesRead) decodes each read chunk independently. If a multi-byte UTF-8 sequence is split across reads, decoding will insert replacement characters and corrupt the SSE text fed into SseParser. Consider decoding with a streaming UTF-8 decoder (keeping trailing partial bytes between iterations) before calling sseParser.feed(...).

Copilot uses AI. Check for mistakes.
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.

@copilot apply changes based on this feedback

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in fd2636b. Replaced buffer.decodeToString(0, bytesRead) with a stateful CharsetDecoder (Charsets.UTF_8.newDecoder()) that uses ByteBuffer.compact() to carry over undecoded partial multi-byte sequences between reads. The byteBuffer has DEFAULT_BUFFER_SIZE + 3 capacity to absorb up to 3 leftover bytes from an incomplete 4-byte UTF-8 sequence, and charBuffer is sized identically (worst-case all-ASCII input produces as many chars as bytes).

@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

Vladislav0Art commented Mar 25, 2026

TODO(@Vladislav0Art): refactor TracingPlugin in ktor module.

Addressed

}

override fun getSpanName(request: TracyHttpRequest) = "Anthropic-generation"
override fun getSpanName() = "Anthropic-generation"
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.

Could you, please, align span naming with GenAI conventions
Span names now follow {gen_ai.operation.name} {gen_ai.request.model}:
https://opentelemetry.io/docs/specs/semconv/gen-ai/anthropic/

Copy link
Copy Markdown
Collaborator Author

@Vladislav0Art Vladislav0Art Mar 30, 2026

Choose a reason for hiding this comment

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

Addressed in 9fe5749

I didn't modify this "Anthropic-generation" name here because I have no information about the model and operation (both of which are only known at the stage of parsing the response body).

Notes:

  1. Now, it looks like:

See the trace.

Screenshot 2026-03-30 at 12 14 57
  1. Pointed to naming problem in: TRACY-114 Audit attribute names used in Tracy to comply with GenAI Spec

event: SseEvent,
index: Long
): Result<Unit> {
return sseHandlingFailure("Unsupported")
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.

I’d prefer logging it once per trace rather than silencing it

@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

@slawa4s's comment, addressed in 4d0582d

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (6)

tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:333

  • asResponseView() dereferences response.body without a safe call (response.body.contentType()), but body is nullable in OkHttp. This is a compile-time error and should be changed to response.body?.contentType() with appropriate handling when the body is absent.
    tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:298
  • The wrapped ResponseBody overrides close() but doesn't close the underlying originalBody (nor the SseCapturingSource / parser). This can leak the connection and also prevents SseParser.close() from flushing the final buffered event when the consumer closes the response without fully reading it. Delegate close to originalBody.close() (or the wrapped BufferedSource) and ensure span.end() happens exactly once (e.g., in a finally).
    tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:384
  • readUtf8(bytesRead) decodes each read chunk independently. If a multi-byte UTF-8 sequence is split across Source.read() boundaries, this can introduce replacement characters / corrupted text before feeding it to SseParser. Consider using a stateful CharsetDecoder (similar to the Ktor implementation) or buffering trailing bytes between reads so SSE parsing remains correct for arbitrary chunking.
    tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/adapters/LLMTracingAdapter.kt:66
  • sseHandlingUnsupportedWarningPrinted is mutable adapter-wide state that is reset in registerRequest. If a single LLMTracingAdapter instance is used by multiple concurrent requests (typical for a shared HTTP client), this becomes racy and can suppress/duplicate warnings across unrelated spans. Store the "warning already printed" flag on the Span (e.g., a boolean attribute) or in a per-span structure instead of a shared field.
abstract class LLMTracingAdapter(private val genAISystem: String) {
    private var sseHandlingUnsupportedWarningPrinted = false

    fun registerRequest(span: Span, request: TracyHttpRequest): Unit = runCatching {
        // new request -> new trace
        sseHandlingUnsupportedWarningPrinted = false

        // Pre-allocate in case the span reaches the limit
        span.setAttribute(DROPPED_ATTRIBUTES_COUNT_ATTRIBUTE_KEY, 0L)

        getRequestBodyAttributes(span, request)
        span.setAttribute("gen_ai.api_base", "${request.url.scheme}://${request.url.host}")

tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:14

  • JsonPrimitive and KotlinLogging imports appear to be unused in this file (JsonPrimitive only appears in KDoc). If the build enables "warnings as errors" / linting, this can fail CI; consider removing unused imports.
    tracing/core/src/jvmMain/kotlin/org/jetbrains/ai/tracy/core/interceptors/OpenTelemetryOkHttpInterceptor.kt:254
  • response.body is nullable (see val originalBody = originalResponse.body ?: ... below), but here it's dereferenced without a safe call. This will not compile and can also crash at runtime if a response has no body. Use response.body?.contentType() (and treat null as non-streaming).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@Vladislav0Art Vladislav0Art requested a review from slawa4s March 30, 2026 12:50
span: Span,
event: SseEvent,
index: Long,
): Result<Unit> = runCatching {
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.

Is there any point in outer catching?

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.

In case of any exceptions that I don't catch explicitly. For instance, .jsonArray throws if the JsonElement is not an array; Below, I don't treat this error explicitly.

Safety-wise, it's reasonable to have it.

): Result<Unit> = runCatching {
val data = runCatching {
Json.parseToJsonElement(event.data).jsonObject
}.getOrNull() ?: return@runCatching sseHandlingFailure("Cannot parse event data as JSON")
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.

Please use direct return instead of return@runCatching. Also, sseHandlingFailure("Cannot parse event data as JSON") swallows the original exception. Use getOrElse { } (it is Throwable ) to preserve the
JsonDecodingException details.

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.

addressed in c4c3d9a

// peek at the bytes just written to sink
val text = sink.peek().apply {
skip(sink.size - bytesRead)
}.readUtf8(bytesRead)
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.

readUtf8 is stateless. If it reads (multiple) one long UTF character (like emoji) it could be splitted across bytes, and symbol would be wrong. Please, use stateful decoder, like you did in Ktor

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.

Addressed in:

  1. 52e0bc0: create UTF8Decoder.
  2. f0ad2e0: use the decoder in Ktor tracing plugin.
  3. db91929: use the decoder here in OpenTelemetryOkHttpInterceptor.

* @param genAISystem The name of the GenAI system (e.g., "openai", "anthropic", "gemini")
*/
abstract class LLMTracingAdapter(private val genAISystem: String) {
private var sseHandlingUnsupportedWarningPrinted = false
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.

Mutable state is shared across concurrent (possible) requests. SSE event index is stored in the span itself, so concurrent SSE streams on different spans are fine, but the sseHandlingUnsupportedWarningPrinted flag is adapter-level.

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.

addressed in 655486a

byteBuffer.flip()
charBuffer.clear()

val endOfInput = originalBody.isClosedForRead
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.

endOfInput is read from isClosedForRead inside the loop, but channel state can change between the loop condition check and this check. Mb pass false in utf8Decoder.decode and do final decode + flush?

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.

Addressed in both Ktor and OkHttp interceptors: 5687846

// NOTE: we must first peek and only then await.
// otherwise there are cases when an empty body gets peeked
val peeked = response.rawContent.readBuffer.peek()
response.rawContent.awaitContent(Int.MAX_VALUE)
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.

Let's document this ordering. It's non-obvious and relies on undocumented Ktor internals, so it'lt silently break if swapped.

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.

How exactly do you mean to document? I generated a descriptive comment in f0befc2

@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

Vladislav0Art commented Apr 2, 2026

TODOs(@Vladislav0Art):

  1. [+] See how SseCapturingSource and OpenTelemetryOkHttpInterceptor.intercept are refactored in TRACY-87: Automatically collect test fixtures for OpenAI tests #224:
    1. A single buffered source created.
    2. Span is ended on the buffered source close.

…E events & create `SseCapturingSource` once

NOTES:
1. `UTF8Decoder` is needed to correctly parse raw bytes into UTF-8 chunks.
2. `OkHttpResponseBody.source` MUST return the same instance on multiple invocations;
    before, it was creating `SseCapturingSource` anew on each call.
@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

Vladislav0Art commented Apr 3, 2026

TODO (@Vladislav0Art):

  1. Write tests for the UTF8Decoder (there is already UTF8DecoderTest).

Addressed in 0c9fe5e.

@Vladislav0Art Vladislav0Art requested a review from slawa4s April 7, 2026 15:24
@Vladislav0Art
Copy link
Copy Markdown
Collaborator Author

@georgiizorabov Gosha, please also have a look, as the PR is quite big, so that not only Slawa reviews it.

tracingChannel.close(e)
}
} finally {
span.end()
Copy link
Copy Markdown
Collaborator

@georgiizorabov georgiizorabov Apr 7, 2026

Choose a reason for hiding this comment

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

wdyt about reordering these end() and close() calls? Im not sure if it really can create some problem (like close triggers adding an attribute to a closed span) but just in case + for consistency with okhttp?

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.

In this case, there is no effect regardless of the order. Still addressed in a010de8.

georgiizorabov
georgiizorabov previously approved these changes Apr 7, 2026
Copy link
Copy Markdown
Collaborator

@georgiizorabov georgiizorabov left a comment

Choose a reason for hiding this comment

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

LGTM after fixing the comments

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.

5 participants