Skip to content

refactor: deepen analytics/usage read path and quota normalization - #77

Merged
evenluo merged 11 commits into
mainfrom
refactor/analytics-usage-quota-deepening
Jul 30, 2026
Merged

refactor: deepen analytics/usage read path and quota normalization#77
evenluo merged 11 commits into
mainfrom
refactor/analytics-usage-quota-deepening

Conversation

@evenluo

@evenluo evenluo commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Architecture deepening pass over the analytics/usage read path and quota normalization, resolving the five deepening candidates from the architecture review (deep modules: smaller seams, single definitions, no pass-through layers). Net effect: ~-700 lines with behavior pinned by tests.

  1. Identity display name — the Key Alias display rule lived as verbatim duplicates in internal/api and internal/repository; it is now a single entities.UsageIdentity.DisplayName() with 7 behavior tests (74d3f3e).
  2. Analytics pass-through serviceservice/analytics_service.go deleted; the HTTP layer owns the AnalyticsProvider seam over repository DTOs and repository.AnalyticsReader binds the database handle; shared response mappers copy each payload field once (74d3f3e).
  3. Raw/rollup mirrored aggregates — new analyticsAggregateSource descriptor (table + column mappings) renders summary, trend, breakdown, heatmap and remaining raw queries from single definitions; mirrored SELECTs and table-switch helpers deleted (acd95f1, e57acdf, a3ef25b, 9a6e2fd).
  4. Usage read-path DTO tax — usage pass-through service and the servicedto read-model family deleted; repository.UsageReader produces read DTOs directly and Output TPS derivation moved into the events read model with its tests (74d3f3e, 9cf0db0).
  5. Quota exit type-switch — the 10-arm NormalizeQuotaRows switch replaced by QuotaRows() methods on each provider result behind QuotaRowsProvider, with a typed-nil guard and regression test (b609d41, 1f00462).

Defect-first review findings fixed in-branch: typed-nil panic guard, trend query no longer computes unused cache-savings columns, dead GetUsageWithFilter removed. An intermediate commit clamped rollup total_tokens on write (9a6e2fd), but that made pre-upgrade rollup rows diverge from new ones; it was reverted in e9810ab so both read paths sum total_tokens the same way (see Compatibility).

Verification

  • go build ./... — exit 0
  • go vet ./cmd/... ./internal/... — exit 0, no findings
  • go test -count=1 ./cmd/... ./internal/... — 18 packages ok, 0 FAIL
  • rollup==raw equivalence tests (internal/repository/analytics_test.go) pin the descriptor refactor semantics
  • API contract fixture tests pin the HTTP JSON contract (unchanged)

Frontend untouched (no web/ changes); backend gate is the meaningful surface.

Compatibility

  • total_tokens behavior is unchanged from main: neither the raw nor the rollup read path clamps negative values — both sum the column as-is, which keeps the two paths equivalent (other token columns still clamp to non-negative on both sides). An intermediate commit briefly changed this; the net diff does not.
  • Event-list queries no longer carry the Range label string downstream (the repository only ever used the resolved time window); HTTP behavior unchanged.
  • Everything else is internal restructuring; ADR-0003/ADR-0005 boundaries preserved (no new package, read-model selection stays in internal/repository).

Auto-close: none

evenluo added 8 commits July 29, 2026 15:39
Replace the 10-arm type switch in NormalizeQuotaRows with a
QuotaRowsProvider interface implemented by each provider result,
so adding a provider no longer touches the exit normalization.
- Move the usage identity display-name rule onto entities.UsageIdentity,
  deleting the verbatim duplicate implementations in api and repository.
- Delete the pass-through analytics and usage services; the HTTP layer
  now defines AnalyticsProvider/UsageProvider seams over repository DTOs
  and repository AnalyticsReader/UsageReader bind the database handle.
- Derive Output TPS inside the usage events read model and drop the
  duplicated service-layer read DTO family.
- Extract shared analytics response mappers so each payload field is
  copied once.
…per source descriptor

Introduce analyticsAggregateSource describing the raw usage_events and
usage_rollups_hourly column mappings, so the shared summary and trend
aggregate SELECT is rendered from a single definition instead of two
mirrored copies. Rollup==raw equivalence tests keep pinning behavior.
…gregate source descriptor

Fold the mirrored raw/rollup segment queries for provider options,
model, key alias, API key and identity trend breakdowns into single
definitions parameterized by analyticsAggregateSource, and drop the
table-switch helpers plus rollup-only expression wrappers. The raw-only
fallback path stays intact as the backfill_incomplete owner and as the
independent oracle for rollup==raw equivalence tests.
@cursor

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9cf0db07b4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rollup.CachedTokens += cachedTokens
rollup.TotalTokens += event.TotalTokens
// TotalTokens 与其他 token 列同样钳制负值,与分析读侧的 positive CASE 表达式保持镜像等价。
rollup.TotalTokens += positiveInt64(event.TotalTokens)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 升级时重建含负值的历史 rollup

当数据库在升级前已经完成 backfill,历史 usage_rollups_hourly 不会因这里的写入规则变更而自动重建;这些行仍保存旧逻辑对正、负 total_tokens 的净和,而新的 raw 查询改为逐事件钳制负值。此时 analyticsRollupReadAllowed 仍会接受已完成的 backfill,导致 /analytics/core 对完整小时读取旧 rollup,而 /analytics/summary 读取 raw 数据,两者在同一窗口返回不同的 token 总量。需要使现有 backfill 失效并从 raw events 重建,或提供等价的数据迁移,而不能只修改新写入。

Useful? React with 👍 / 👎.

…de rollup divergence

Codex review P2: clamping total_tokens on write (9a6e2fd) only affects
rollups written after the upgrade, so already-backfilled hours keep the
old net sum while the raw path clamps per event, making /analytics/core
(rollup) and the raw fallback disagree on token totals for the same
covered window. Revert to the original unclamped total_tokens on both
the rollup write and the raw read descriptor; both paths now SUM the
stored/raw value as-is, keeping raw==rollup without rebuilding history.
@evenluo

evenluo commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Addressed Codex review P2 (internal/repository/usage_rollups_hourly.go — pre-upgrade rollup divergence on total_tokens).

Fix: reverted the total_tokens clamp introduced in 9a6e2fd on both sides — the rollup write (rollup.TotalTokens += event.TotalTokens) and the raw read descriptor (totalTokens: "usage_events.total_tokens"). Both the rollup and raw read paths now SUM the stored/raw value as-is, which is the original consistent behavior, so already-backfilled hours and freshly written rollups agree with the raw fallback for the same window — no historical rollup rebuild or data migration required. The other token columns (input/output/reasoning/cached) legitimately clamp on both write and read and are unchanged.

Verification: go build ./... (exit 0), go test -count=1 ./internal/repository/ ./internal/api/ — both packages ok, including the rollup==raw equivalence tests.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: e9810abd8a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

evenluo added 2 commits July 30, 2026 21:19
layout.md now records that analytics/usage read paths flow from
repository readers through the internal/api provider seams, and that
internal/service retains write-path use cases only. ADR-0005 gains an
amendment capturing the same boundary move.
… total-tokens order fragment

Give the analyticsAggregateSource token columns the Expr suffix used by
their sibling fields, rename analyticsAggregateTrendSelect to
analyticsSummaryTrendSelect (it serves both summary and trend), and
render the five duplicated total-tokens DESC order fragments from one
helper. Rendered SQL is unchanged.
@evenluo
evenluo merged commit 6fa5c6e into main Jul 30, 2026
1 check 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.

1 participant