Skip to content

feat(intake): sort span groups by time - #1242

Merged
aleckhoury merged 4 commits into
mainfrom
intake-span-group-time-sort/akhoury
Aug 12, 2026
Merged

feat(intake): sort span groups by time#1242
aleckhoury merged 4 commits into
mainfrom
intake-span-group-time-sort/akhoury

Conversation

@aleckhoury

@aleckhoury aleckhoury commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Span groups could only be sorted by size, and carried no timestamp at all. That left a common question unanswerable in one call: which traces did this agent run lately?

Callers had to work around it by paging spans newest first and collecting distinct trace ids. That is slower, and it is not even correct: it only ever finds the traces that happen to fit inside the scanned window.

This adds:

  • started_at on each group row, the earliest matching span in the group.
  • started_at and -started_at as sort options.

Only matching spans count toward the time, so a filter reports when the filtered work began rather than when the whole trace or session began.

Why this came up

Raised by @BrianNewsom while reviewing #1212, on a helper that had to drain spans to find an agent's recent traces:

I would have expected to do the grouped find here - sounds like this is maybe missing a sort?

He was right. Once this lands, that helper collapses to a single grouped call.

Validation against a running instance

Ran a second Intake on port 8081 against the shared ClickHouse holding real trace data, and compared it with an unpatched instance on 8080.

Patched, sort=-started_at, newest first:

d9f4ca364185803593a6  spans=16   started_at=2026-08-06T21:03:05.958510
55da0f83996415c4005a  spans=18   started_at=2026-08-06T21:03:04.669397
856fc7e9f3f8325f698f  spans=10   started_at=2026-08-06T21:03:01.848539

The span counts are unordered, so time is genuinely driving the sort. The default -span_count still returns biggest first (64, 45, 45, 43, 41) and picks an entirely different set of traces, so the two orders are not accidentally the same.

Unpatched, same request:

HTTP 422  Input should be 'span_count' or '-span_count'

Generated clients

make update-sdk carries the change into the checked-in SDK and CLI, which CI's sync check requires. SpanGroup gains started_at, SpanGroupSortField widens to the four values, and the sort guidance reaches both CLIs and docs/cli/reference.mdx.

started_at is required rather than optional, because every group of spans has an earliest one and an optional field would push a case that cannot happen onto every caller. That does make it a breaking change for anyone constructing a SpanGroup by hand, which in this repo is the Analyst's test fakes. They now pass a stamp, and the Analyst's list_span_groups output carries the new field through to its model.

Test plan

  • test_list_span_groups_sorts_by_time_so_recent_work_is_one_call pins the SQL and the resulting order.
  • test_group_order_by_* pin the whitelist and the rejection of an injected sort key.
  • test_group_sort_enum_matches_repository_sort_fields keeps the API enum and the repository whitelist from drifting.
  • The integration test runs against real ClickHouse. Every span in it shares one trace, yet the two session groups report different start times, which pins the "only matching spans count" semantics.
  • Full services/intake suite passes: 276 unit, 108 spans integration.
  • plugins/nemo-insights: 146 passed. The testbed failures are tar --zstd on macOS, identical on main.
  • tools/lint/lint-python-types.sh, the exact script CI runs: no errors, only pre-existing warnings.
  • ruff check, ruff format --check clean.
  • OpenAPI spec regenerated; the diff is purely additive.

Summary by CodeRabbit

  • New Features

    • Added span-group sorting by earliest matching span start time.
    • Added ascending (started_at) and descending (-started_at) sort options.
    • Span-group results now include the earliest matching span’s start timestamp.
    • Updated CLI support and guidance for time-based sorting.
  • Documentation

    • Updated API and CLI documentation with the new timestamp field and sorting behavior.
  • Tests

    • Added coverage for timestamp values, ordering, filtering, and invalid sort options.

Span groups could only be sorted by size, and carried no timestamp at all, so
"which traces did this agent run lately" had no answer. Callers had to page
spans newest first and collect distinct trace ids, which is slower and only
ever finds the traces that fit inside the scanned window.

Group rows now carry started_at, the earliest matching span in the group, and
sort accepts started_at and -started_at. Only matching spans count toward the
time, so a filter reports when the filtered work began rather than when the
whole trace or session began. The integration test pins that distinction: every
span in it shares one trace, yet the two session groups report different start
times.

The sort whitelist is now a named set that the API enum is tested against, so
the two cannot drift apart.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
@aleckhoury
aleckhoury requested review from a team as code owners August 11, 2026 20:36
@github-actions github-actions Bot added the feat label Aug 11, 2026
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: c82e2666-4cda-4535-91c4-6dbfa34230b2

📥 Commits

Reviewing files that changed from the base of the PR and between 81a083f and 5d72964.

⛔ Files ignored due to path filters (4)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/intake/spans/groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/intake/spans/groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/spans/group_list_params.py is excluded by !sdk/**
📒 Files selected for processing (6)
  • docs/cli/reference.mdx
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py
  • services/intake/src/nmp/intake/spans/api/spans.py
🚧 Files skipped from review as they are similar to previous changes (6)
  • services/intake/src/nmp/intake/spans/api/spans.py
  • openapi/ga/individual/platform.openapi.yaml
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • docs/cli/reference.mdx

📝 Walkthrough

Walkthrough

Span-group responses now include the earliest matching span start time. ClickHouse grouped queries support ascending and descending started_at sorting. API schemas, OpenAPI documents, CLI help, documentation, and tests cover the new field and ordering.

Changes

Span-group chronological sorting

Layer / File(s) Summary
Public span-group contracts
services/intake/src/nmp/intake/spans/domain.py, services/intake/src/nmp/intake/spans/api/*, packages/nemo_platform_ext/src/.../groups.py, docs/cli/reference.mdx, openapi/...
SpanGroup now requires started_at. API and CLI contracts support ascending and descending started_at sorting. OpenAPI documents the field and sort values.
Grouped query sorting and validation
services/intake/src/nmp/intake/repository/clickhouse/span.py, services/intake/tests/test_spans_clickhouse_repository.py, services/intake/tests/integration/spans/test_spans_read_filters.py, services/intake/tests/test_spans_schemas.py, plugins/nemo-insights/tests/test_periodic_analysis.py
Grouped queries calculate min(start_time), validate sort fields, apply chronological ordering with tie-breakers, and map timestamps into responses. Tests cover filtered timestamps, newest-first ordering, validation, schema mapping, and serialized output.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant SpanGroupsAPI
  participant ClickHouseSpanRepository
  participant ClickHouse
  Client->>SpanGroupsAPI: Request grouped spans with sort
  SpanGroupsAPI->>ClickHouseSpanRepository: Pass group sort field
  ClickHouseSpanRepository->>ClickHouse: Run min(start_time) and ORDER BY
  ClickHouse-->>ClickHouseSpanRepository: Return grouped rows with started_at
  ClickHouseSpanRepository-->>SpanGroupsAPI: Return sorted span groups
  SpanGroupsAPI-->>Client: Return grouped response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding time-based sorting for Intake span groups.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch intake-span-group-time-sort/akhoury

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
services/intake/tests/integration/spans/test_spans_read_filters.py (1)

11-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Type the group payload by shape.

dict[str, str] is incorrect. The payload contains group: dict[str, str], span_count: int, and started_at: str. Define a TypedDict for this payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@services/intake/tests/integration/spans/test_spans_read_filters.py` around
lines 11 - 14, Define a TypedDict for the group payload with group as dict[str,
str], span_count as int, and started_at as str, then update _group_started_at to
accept that TypedDict instead of dict[str, str].

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@services/intake/tests/integration/spans/test_spans_read_filters.py`:
- Around line 11-14: Define a TypedDict for the group payload with group as
dict[str, str], span_count as int, and started_at as str, then update
_group_started_at to accept that TypedDict instead of dict[str, str].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: fdb40441-812d-4378-9065-af685aada693

📥 Commits

Reviewing files that changed from the base of the PR and between b176087 and a9f7e94.

📒 Files selected for processing (10)
  • openapi/ga/individual/platform.openapi.yaml
  • openapi/ga/openapi.yaml
  • openapi/openapi.yaml
  • services/intake/src/nmp/intake/repository/clickhouse/span.py
  • services/intake/src/nmp/intake/spans/api/spans.py
  • services/intake/src/nmp/intake/spans/api/spans_schemas.py
  • services/intake/src/nmp/intake/spans/domain.py
  • services/intake/tests/integration/spans/test_spans_read_filters.py
  • services/intake/tests/test_spans_clickhouse_repository.py
  • services/intake/tests/test_spans_schemas.py

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 32002/40624 78.8% 63.5%
Integration Tests 18561/38550 48.1% 20.8%

Adding started_at to SpanGroup and to the sort enum changed the published
contract, so the checked-in SDK and CLI went stale and CI's sync check failed.
Regenerating gives SpanGroup a started_at field, widens SpanGroupSortField to
include started_at and -started_at, and carries the sort guidance into both CLIs
and the reference docs.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
@github-actions

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py`:
- Line 96: Update the help text for the --by option in the groups command to
describe -started_at as descending order by each group’s earliest matching span
timestamp, without claiming it always returns the most recently active traces or
sessions first. Then regenerate docs/cli/reference.mdx from the CLI help.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 43d23d81-baa3-4933-a06b-3fead2890088

📥 Commits

Reviewing files that changed from the base of the PR and between a9f7e94 and d4fe314.

⛔ Files ignored due to path filters (7)
  • sdk/python/nemo-platform/.nmpcontext/openapi.yaml is excluded by !sdk/**
  • sdk/python/nemo-platform/pyproject.toml is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/cli/commands/api/intake/spans/groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/resources/intake/spans/groups.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/spans/group_list_params.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/spans/span_group.py is excluded by !sdk/**
  • sdk/python/nemo-platform/src/nemo_platform/types/intake/spans/span_group_sort_field.py is excluded by !sdk/**
📒 Files selected for processing (2)
  • docs/cli/reference.mdx
  • packages/nemo_platform_ext/src/nemo_platform_ext/cli/commands/api/intake/spans/groups.py

SpanGroup now requires started_at, so the fakes that stand in for a server reply
no longer type-check or match. The Analyst dumps group rows straight through, so
its list_span_groups output carries the new field as well.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
A group's time is its earliest matching span, so -started_at returns the groups
that began most recently, not the ones most recently active. A long trace that
started hours ago sorts low even while it is still running. The old wording said
"most recent", which reads as the second meaning.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
@aleckhoury
aleckhoury added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 95f9fb1 Aug 12, 2026
64 checks passed
@aleckhoury
aleckhoury deleted the intake-span-group-time-sort/akhoury branch August 12, 2026 15:14
aleckhoury added a commit that referenced this pull request Aug 12, 2026
Per review on #1212: the workaround should not need to exist, and it no longer
does, so the notes describing it go too.

The docstring no longer warns the agent off the five dataset and prompt span
filters, and _explain no longer has a 500 branch naming them. #1225 unpublished
those fields, so they read as unknown filter fields like any other typo, and the
400 branch already says where the real vocabulary lives. A 500 now falls through
to the generic branch, which still names the status, since an Intake fault leaves
the caller nothing to correct.

The module docstring also still said span groups sort only by count, which #1242
changed and this branch already relies on.

Signed-off-by: Alec Khoury <akhoury@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants