← Files LaunchDarklyARCHIVED FILE

skills/built-in-metrics/references/streaming-tracking.md

6.91 KB · Oct 3, 2026 · 06:25 UTC

↓ Download file

# Streaming Metrics Tracking

**This is Tier 4 — the manual fallback.** Streaming is the one case where no current helper captures everything you need. The Node SDK ships `trackStreamMetricsOf`, which can pull tokens from stream chunks, but it does **not** capture time-to-first-token (TTFT). Python doesn't have a streaming helper at all. So if you want TTFT in the Monitoring tab, you have to wire it manually — and since TTFT is the whole point of streaming observability, this is almost always what you want.

If the app doesn't need TTFT (you just want total duration + tokens + success), you can use Tier 2 / Tier 3 patterns in Node via `trackStreamMetricsOf`, and Tier 3 in Python by consuming the whole stream into a response object and then calling `trackMetricsOf` on the assembled result. TTFT is the tiebreaker that forces Tier 4.

## What you track

- **Time to first token (TTFT)** — measured from "stream request sent" to "first content chunk received."
- **Total duration** — measured from "stream request sent" to "stream fully consumed."
- **Tokens** — read from the final stream event (if the provider includes usage) or from `tiktoken` / provider-native counters if not.
- **Success / error** — explicit calls in the consumer loop.

## Python — OpenAI streaming

```python
import time
import openai
from ldai.tracker import TokenUsage

def call_streaming_with_tracking(ai_config, user_prompt: str) -> str | None:
    if not ai_config.enabled:
        return None

    tracker = ai_config.create_tracker()
    start_time = time.time()
    first_token_time = None

    try:
        stream = openai.chat.completions.create(
            model=ai_config.model.name,
            messages=[
                {"role": "system", "content": ai_config.messages[0].content},
                {"role": "user", "content": user_prompt},
            ],
            stream=True,
            stream_options={"include_usage": True},  # Required to get usage in final chunk
        )

        response_text = ""
        final_usage = None
        for chunk in stream:
            if chunk.choices and chunk.choices[0].delta.content:
                if first_token_time is None:
                    first_token_time = time.time()
                    tracker.track_time_to_first_token(
                        int((first_token_time - start_time) * 1000)
                    )
                response_text += chunk.choices[0].delta.content
            if getattr(chunk, "usage", None):
                final_usage = chunk.usage

        tracker.track_duration(int((time.time() - start_time) * 1000))
        tracker.track_success()

        if final_usage:
            tracker.track_tokens(TokenUsage(
                total=final_usage.total_tokens,
                input=final_usage.prompt_tokens,
                output=final_usage.completion_tokens,
            ))

        return response_text

    except Exception:
        tracker.track_error()
        raise
```

The `stream_options={"include_usage": True}` flag is required — without it, OpenAI streaming does not include usage data and you fall back to `tiktoken` estimation.

## Python — tiktoken fallback

If you can't set `include_usage` (older SDK, Azure OpenAI on an endpoint that doesn't support it), count tokens locally with `tiktoken`:

```python
import tiktoken
from ldai.tracker import TokenUsage

def estimate_tokens(model_name: str, prompt: str, response: str) -> TokenUsage:
    try:
        enc = tiktoken.encoding_for_model(model_name)
    except KeyError:
        enc = tiktoken.get_encoding("cl100k_base")
    input_tokens = len(enc.encode(prompt))
    output_tokens = len(enc.encode(response))
    return TokenUsage(
        total=input_tokens + output_tokens,
        input=input_tokens,
        output=output_tokens,
    )
```

Drop it into the streaming consumer where `final_usage` would have been.

## Node — OpenAI streaming with manual TTFT

```typescript
import { OpenAI } from 'openai';

const client = new OpenAI();

async function callStreamingWithTracking(
  aiConfig: LDAICompletionConfig,
  userPrompt: string,
): Promise<string | null> {
  if (!aiConfig.enabled) return null;

  const tracker = aiConfig.createTracker();
  const startTime = Date.now();
  let firstTokenTime: number | null = null;

  try {
    const stream = await client.chat.completions.create({
      model: aiConfig.model!.name,
      messages: [
        ...aiConfig.messages,
        { role: 'user', content: userPrompt },
      ],
      stream: true,
      stream_options: { include_usage: true },
    });

    let responseText = '';
    let finalUsage: OpenAI.CompletionUsage | undefined;

    for await (const chunk of stream) {
      const delta = chunk.choices[0]?.delta?.content;
      if (delta) {
        if (firstTokenTime === null) {
          firstTokenTime = Date.now();
          tracker.trackTimeToFirstToken(firstTokenTime - startTime);
        }
        responseText += delta;
      }
      if (chunk.usage) {
        finalUsage = chunk.usage;
      }
    }

    tracker.trackDuration(Date.now() - startTime);
    tracker.trackSuccess();

    if (finalUsage) {
      tracker.trackTokens({
        total: finalUsage.total_tokens,
        input: finalUsage.prompt_tokens,
        output: finalUsage.completion_tokens,
      });
    }

    return responseText;
  } catch (err) {
    tracker.trackError();
    throw err;
  }
}
```

## Node — `trackStreamMetricsOf` (no TTFT)

If the app doesn't need TTFT, the Node SDK has a built-in streaming wrapper that handles tokens + success/error + duration:

```typescript
const tracker = aiConfig.createTracker();
const response = await tracker.trackStreamMetricsOf(
  (chunks) => {
    // Extract usage from the final chunk
    const final = chunks[chunks.length - 1];
    return {
      success: true,
      tokens: {
        total: final.usage?.total_tokens ?? 0,
        input: final.usage?.prompt_tokens ?? 0,
        output: final.usage?.completion_tokens ?? 0,
      },
    };
  },
  () => client.chat.completions.create({ /* ... */, stream: true, stream_options: { include_usage: true } }),
);
```

This is cleaner when TTFT doesn't matter (batch processing, log summarization, tasks where latency-to-first-byte isn't user-facing). If the user is going to look at the Monitoring tab's TTFT chart, though, you need the manual pattern above.

## What to avoid

- **Do not wrap `openai.chat.completions.create(stream=True)` with `trackMetricsOf`.** It'll record duration as the time to get the stream *object*, not the time to consume it — and tokens won't be captured at all because the extractor sees a stream object, not a response with `usage`.
- **Do not forget `track_success()` / `trackSuccess()`.** Unlike `trackMetricsOf`, the manual pattern doesn't call it for you. If you skip it, the Monitoring tab won't count the generation.
- **Do not set `first_token_time` on the first *chunk*.** Set it on the first chunk with non-empty `delta.content`. Many providers emit a role/metadata chunk before the first content chunk.

SHA-256: 2af0341b3f388a6bf911565a2336c1dafb8ee2655b2c741096429a96ffb2fb9d