Skip to content

feat: add iter_observation_pages helper for observations v2 - #2101

Merged
hassiebp merged 4 commits into
langfuse:mainfrom
DavidTraina:feat/iter-observation-pages
Nov 5, 2026
Merged

hassiebp merged 4 commits into
langfuse:mainfrom
DavidTraina:feat/iter-observation-pages

Conversation

@DavidTraina

@DavidTraina DavidTraina commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds iter_observation_pages and aiter_observation_pages, which page through GET /api/public/v2/observations correctly. Since langfuse/langfuse#19043, getting this right by hand takes more care. Pages can be short or empty while meta.cursor is set, and a request can take up to 45 s, well past the SDK's 5 s default timeout. Both loops in this repo stopped on empty pages (#2097, #2099).

from langfuse import get_client, iter_observation_pages

langfuse = get_client()
for page in iter_observation_pages(langfuse.api, trace_id="abc123", from_start_time=since, limit=1000):
    for observation in page.data:
        ...

Behavior

  • Stops only when meta.cursor is absent and yields every page, including empty ones. It yields pages, not items, so page.meta.cursor is the exact resume point.
  • Sends timeout_in_seconds=60 per request unless the caller's request_options sets one. Other options pass through. The global timeout is left alone, because raising it also lengthens OTLP export, flush() and shutdown().
  • With max_items, the last request asks only for the remaining rows, so the last page's cursor resumes right after them.
  • Errors, including 422, propagate and are not retried. The docstring explains how to narrow the time range instead.
  • Takes **params and forwards them to get_many, so it keeps working when the generated client gains parameters.

Considered and not done

  • Fern pagination: config. Fern's Python SyncPager stops on an empty page, and get_many would return a pager instead of ObservationsV2Response, which is a breaking change.
  • Dedupe by id, rewriting from_start_time/to_start_time into a JSON filter, and automatic narrowing after a 422. Each adds request rewriting or depends on server column names or the cursor format. These are documented instead.

run_batched_evaluation and tests/support keep their own loops for now, so this PR merges independently of the sibling PRs. They can move onto iter_observation_pages in a follow-up.

Related PRs

Independent follow-ups to langfuse/langfuse#19043, mergeable in any order: #2097 (batch evaluation stops on empty pages), #2096 (batch evaluation fetch timeout), #2098 (docs), #2099 (test helpers).

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • Tooling, CI, or repo maintenance

Verification

List the main commands you ran:

uv run --frozen ruff check .                                  # All checks passed!
uv run --frozen ruff format --check .                         # 595 files already formatted
uv run --frozen mypy langfuse --no-error-summary              # no issues
uv run --frozen pytest -n auto --dist worksteal tests/unit    # 1013 passed, 2 skipped
uv run --frozen pytest tests/unit/test_pagination.py          # 10 passed (sync + async)

Making the loop stop on empty pages fails 6 of the 10 new tests. One test runs the real LangfuseAPI/AsyncLangfuseAPI over httpx.MockTransport and checks that requests go out with a 60 s read timeout while the client is configured for 5 s.

Checklist

  • I self-reviewed the diff using code_review.md.
  • I added or updated tests for behavior changes.
  • I updated docs, examples, or .env.template if needed.
  • I did not hand-edit generated files; if generated files changed, I used the upstream regeneration path.
  • I did not commit secrets or credentials.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with a non-blocking fix recommended for counting rows before yielding pages.

Summary

Adds public sync and async helpers for paging through observations v2.

  • Callers can fetch observation pages with sync or async helpers.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Start iterator] --> B[Choose request size]
  B --> C[Fetch page with per-request timeout]
  C --> D[Yield page, including empty pages]
  D --> E[Count rows and read cursor]
  E --> F{Cursor exists and item limit not reached?}
  F -->|Yes| B
  F -->|No| G[Stop]
Loading

Reviews (1) · Last reviewed commit: "feat: add iter_observation_pages helper ..." · Reviewed by Greptile

What does this PR do?

Adds Langfuse.iter_observation_pages and Langfuse.aiter_observation_pages, which page through GET /api/public/v2/observations correctly. Since langfuse/langfuse#19043, getting this right by hand takes more care. Pages can be short or empty while meta.cursor is set, and a request can take up to 45 s, well past the SDK's 5 s default timeout. Both loops in this repo stopped on empty pages (#2097, #2099).

from langfuse import get_client

langfuse = get_client()
for page in langfuse.iter_observation_pages(trace_id="abc123", from_start_time=since, limit=1000):
    for observation in page.data:
        ...

Behavior

  • Stops only when meta.cursor is absent and yields every page, including empty ones. It yields pages, not items, so page.meta.cursor is the exact resume point.
  • Sends timeout_in_seconds=60 per request unless the caller's request_options sets one. Other options pass through. The global timeout is left alone, because raising it also lengthens OTLP export, flush() and shutdown().
  • With max_items, the last request asks only for the remaining rows, so the last page's cursor resumes right after them.
  • Errors, including 422, propagate and are not retried. The docstring explains how to narrow the time range instead.
  • Lives on the Langfuse client next to get_dataset and get_prompt, using api and async_api. The paging loop is private in langfuse/_client/pagination.py.
  • Takes typed keyword arguments that mirror observations.get_many (except the deprecated parse_io_as_json), so IDEs complete them and mypy checks them. A new get_many parameter has to be added here too.

Considered and not done

  • Fern pagination: config. Fern's Python SyncPager stops on an empty page, and get_many would return a pager instead of ObservationsV2Response, which is a breaking change.
  • Dedupe by id, rewriting from_start_time/to_start_time into a JSON filter, and automatic narrowing after a 422. Each adds request rewriting or depends on server column names or the cursor format. These are documented instead.

run_batched_evaluation and tests/support keep their own loops for now, so this PR merges independently of the sibling PRs. They can move onto iter_observation_pages in a follow-up.

Related PRs

Independent follow-ups to langfuse/langfuse#19043, mergeable in any order: #2097 (batch evaluation stops on empty pages), #2096 (batch evaluation fetch timeout), #2098 (docs), #2099 (test helpers).

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Refactor
  • Documentation update
  • Tooling, CI, or repo maintenance

Verification

List the main commands you ran:

uv run --frozen ruff check .                                  # All checks passed!
uv run --frozen ruff format --check .                         # 595 files already formatted
uv run --frozen mypy langfuse --no-error-summary              # no issues
uv run --frozen pytest -n auto --dist worksteal tests/unit    # 1015 passed, 2 skipped
uv run --frozen pytest tests/unit/test_pagination.py          # 10 passed (sync + async)

Making the loop stop on empty pages fails 6 of the 10 new tests. The tests call the Langfuse methods. One runs the real LangfuseAPI/AsyncLangfuseAPI over httpx.MockTransport and checks that requests carry traceId and go out with a 60 s read timeout while the client is configured for 5 s.

Checklist

  • I self-reviewed the diff using code_review.md.
  • I added or updated tests for behavior changes.
  • I updated docs, examples, or .env.template if needed.
  • I did not hand-edit generated files; if generated files changed, I used the upstream regeneration path.
  • I did not commit secrets or credentials.
Open in Web Open in Cursor 

Since langfuse/langfuse#19043, GET /api/public/v2/observations can return
short or empty pages while meta.cursor is set, and a request can take up to
45 s, well past the SDK's 5 s default timeout. Add iter_observation_pages
and aiter_observation_pages, which page until the cursor is absent and send
a 60 s per-request timeout, so callers don't have to hand-roll the loop.
@CLAassistant

CLAassistant commented Nov 5, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ DavidTraina
❌ hassiebp
You have signed the CLA already but the status is still pending? Let us recheck it.

@DavidTraina
DavidTraina marked this pull request as ready for review November 5, 2026 07:03

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Comment thread langfuse/_client/pagination.py Resolved
DavidTraina and others added 3 commits November 5, 2026 01:07
A caller that mutates page.data could otherwise make max_items undercount.
Expose iter_observation_pages and aiter_observation_pages as Langfuse
methods with typed parameters mirroring observations.get_many, instead
of top-level functions that take the API client as an argument. The
paging loop stays in langfuse/_client/pagination.py.

Co-authored-by: Hassieb Pakzad <hassiebp@users.noreply.github.com>
@hassiebp

hassiebp commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

@claude review

@claude claude 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.

Code review found no issues

No high-confidence issues detected in this change.

@hassiebp
hassiebp merged commit 65cff16 into langfuse:main Nov 5, 2026
20 of 22 checks passed
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