Skip to content

feat(usage-collector): BigDecimal aggregates, deterministic record ids, op-per-kind guard#4187

Open
capybutler wants to merge 1 commit into
constructorfabric:mainfrom
capybutler:usage-collector/aggregation-and-id-fixes
Open

feat(usage-collector): BigDecimal aggregates, deterministic record ids, op-per-kind guard#4187
capybutler wants to merge 1 commit into
constructorfabric:mainfrom
capybutler:usage-collector/aggregation-and-id-fixes

Conversation

@capybutler

@capybutler capybutler commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Consolidates three related changes to the usage-collector gateway and SDK, plus one fix.

Arbitrary-precision aggregate values. AggregationBucket.value (SDK) and
AggregationBucketDto.value (gateway) move from fixed-precision Option<Decimal>
to Option<BigDecimal>, so aggregates no longer lose precision on large or
high-scale sums. Values round-trip over the wire as plain-decimal strings (never
scientific notation, never a bare float) via a new bigdecimal_str_option serde
codec.

Deterministic, gateway-derived record id. The usage-record id is now derived
on the gateway as a UUIDv5 over the dedup key (tenant_id, gts_id, idempotency_key) (derive_usage_record_id) and is no longer accepted in the
request body. The record-identity field is renamed uuidid across the SDK,
DTOs, and OpenAPI schema. ADR-0013 documents the derivation scheme and why the
namespace must never change.

Per-kind aggregation-op restriction. Aggregation operations are constrained
by record kind: an AggregationOp::is_allowed_for matrix, an
OP_NOT_ALLOWED_FOR_KIND validation reason and matching SDK error, and a
require_op_allowed_for_kind query guard that rejects a disallowed op/kind
combination before the request is dispatched to a plugin.

Fix: correct aggregation orderby handling.

Design docs, feature specs, domain model, plugin SPI, SDK trait, and the OpenAPI
schema (usage-collector-v1.yaml) are updated to match.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update

Testing

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed
  • New tests added for new functionality

Documentation

  • Code is documented with rustdoc comments
  • README updated (if applicable) — n/a
  • API documentation updated (OpenAPI usage-collector-v1.yaml + gear design/feature docs, ADR-0013)

Checklist

  • Code follows project style guidelines
  • Self-review completed
  • No linting errors (cargo clippy)
  • Code is properly formatted (cargo fmt)
  • Tests pass (cargo test)

Summary by CodeRabbit

  • New Features

    • Usage records now use deterministic IDs, improving retry behavior and reducing duplicate-conflict errors.
    • Aggregation results support higher-precision numeric values.
    • Unsupported aggregation options now return clearer validation errors.
  • Bug Fixes

    • Record creation no longer accepts client-supplied IDs, preventing invalid request payloads.
    • Pagination and sorting now consistently use the record ID field.
  • Documentation

    • Updated usage collector docs to match the new ID and aggregation behavior.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR replaces the caller-supplied UsageRecord.uuid with a gateway-derived, deterministic id (UUIDv5 of tenant/gts/idempotency-key), introduces an identity-free CreateUsageRecord create surface, adds per-kind aggregation-op validation, and migrates aggregation bucket values from Decimal to BigDecimal across SDK, service, REST, plugins, and docs.

Changes

Deterministic Identity and Aggregation Overhaul

Layer / File(s) Summary
Deterministic ID derivation core
Cargo.toml, usage-collector-sdk/src/id.rs, id_tests.rs, lib.rs
Adds uuid v5 feature, derive_usage_record_id UUIDv5 derivation from (tenant_id, gts_id, idempotency_key), fixed namespace constant, and crate re-exports.
CreateUsageRecord contract & BigDecimal aggregation
usage-collector-sdk/src/models.rs, models_tests.rs, serde_helpers*.rs, error.rs, reason*.rs, Cargo.toml
Introduces CreateUsageRecord::into_usage_record, AggregationOp::is_allowed_for, migrates AggregationBucket.value to BigDecimal with string-based serde, and adds aggregation_op_not_allowed_for_kind/OpNotAllowedForKind validation.
SDK trait & error rename
usage-collector-sdk/src/api.rs, plugin_api.rs, error.rs
Updates UsageCollectorClientV1/plugin trait signatures to CreateUsageRecord/id, and renames existing_uuid to existing_id in idempotency conflict errors.
Domain service wiring & enforcement
usage-collector/src/domain/*.rs
Service derives/stamps ids via into_usage_record(), enforces aggregation op-vs-kind before dispatch, propagates existing_id rename, and updates local client, test fixtures, and dedup/authz tests.
REST DTOs, handlers & wire contract
usage-collector/src/api/rest/dto*.rs, handlers/usage_records*.rs, infra/sdk_error_mapping_tests.rs
Renames DTO uuidid, rejects client-supplied id, migrates AggregationBucketDto to BigDecimal, and normalizes $orderby to (created_at, id).
Noop plugin identity rename
plugins/noop-usage-collector-plugin/src/plugin.rs, plugin_tests.rs
Renames get_usage_record parameter and test fixture field from uuid to id.
ADR and documentation updates
docs/ADR/0013-*.md, docs/DECOMPOSITION.md, docs/DESIGN.md, docs/domain-model.md, docs/features/*.md, docs/plugin-spi.md, docs/sdk-trait.md, docs/usage-collector-v1.yaml
Documents the deterministic id derivation, (created_at, id) keysets, and BigDecimal aggregation contracts, plus changelog entries.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant Handler
participant Service
participant Plugin
Client->>Handler: POST /records (CreateUsageRecordRequest, no id)
Handler->>Service: create_usage_record(CreateUsageRecord)
Service->>Service: into_usage_record() -> derive_usage_record_id(tenant_id, gts_id, idempotency_key)
Service->>Plugin: create_usage_record(UsageRecord{id,...})
Plugin-->>Service: persisted UsageRecord
Service-->>Handler: UsageRecord
Handler-->>Client: 201 { id, ... }
Loading
sequenceDiagram
participant Client
participant Handler
participant Service
participant Query
participant Plugin
Client->>Handler: GET /records/aggregate?op=SUM
Handler->>Service: query_aggregated_usage_records(op, gts_id)
Service->>Plugin: fetch UsageType(gts_id)
Service->>Query: require_op_allowed_for_kind(op, kind)
Query-->>Service: Ok / aggregation_op_not_allowed_for_kind
Service->>Plugin: query_aggregated_usage_records()
Plugin-->>Service: AggregationBucket{value: BigDecimal}
Service-->>Handler: aggregated result
Handler-->>Client: 200 { value: "string" }
Loading

Possibly related PRs

Suggested reviewers: Artifizer

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: BigDecimal aggregates, deterministic record IDs, and per-kind aggregation validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@capybutler capybutler force-pushed the usage-collector/aggregation-and-id-fixes branch from db0ecc2 to 10af4df Compare July 7, 2026 14:55
…s, op-per-kind guard

- Aggregate value is now arbitrary-precision BigDecimal end to end:
  widen AggregationBucket.value (SDK) and AggregationBucketDto.value
  (gateway), with a bigdecimal_str_option serde codec so values
  round-trip as strings.

- Record id can no longer be caller-supplied: the create surface across
  the SDK trait, domain service, local client, and gateway takes a new
  identity-free CreateUsageRecord (mirrors UsageRecord minus id +
  status). CreateUsageRecord::into_usage_record is the single choke
  point that stamps the deterministic UUIDv5 dedup-key derivation and
  the initial Active status. The record-identity field is renamed
  uuid -> id. Adds derive_usage_record_id plus ADR-0013 describing the
  scheme.

- Aggregation ops are restricted per record kind: AggregationOp::
  is_allowed_for matrix, OP_NOT_ALLOWED_FOR_KIND validation reason and
  matching error, a require_op_allowed_for_kind query guard, and
  enforcement before plugin dispatch.

- Fix aggregation orderby.

Design docs, feature specs, domain model, plugin SPI, SDK trait, and the
OpenAPI schema are updated to match.

Signed-off-by: capybutler <capybutler@gmail.com>
@capybutler capybutler force-pushed the usage-collector/aggregation-and-id-fixes branch from 10af4df to 2562796 Compare July 7, 2026 15:53
@capybutler capybutler marked this pull request as ready for review July 7, 2026 17:07

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
gears/system/usage-collector/docs/ADR/0013-deterministic-usage-record-id.md (1)

117-121: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Traceability list omits two files changed by this same PR layer.

The "Amends" list names plugin-spi.md, sdk-trait.md, domain-model.md, features/usage-emission.md, PRD, DESIGN, usage-collector-v1.yaml, but DECOMPOSITION.md and features/event-deactivation.md are also updated as part of this cohort (per the PR's file list) and aren't listed here.

📝 Proposed fix
 - Amends: `plugin-spi.md`, `sdk-trait.md`, `domain-model.md`,
-  `features/usage-emission.md`, PRD, DESIGN, `usage-collector-v1.yaml`.
+  `features/usage-emission.md`, `features/event-deactivation.md`,
+  `DECOMPOSITION.md`, PRD, DESIGN, `usage-collector-v1.yaml`.
🤖 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 `@gears/system/usage-collector/docs/ADR/0013-deterministic-usage-record-id.md`
around lines 117 - 121, The Traceability “Amends” list in the ADR is incomplete
because it omits two files updated in the same change set. Update the list in
the Traceability section of the deterministic-usage-record-id ADR to include
DECOMPOSITION.md and features/event-deactivation.md alongside the existing
references so the documented traceability matches the full PR cohort.
🧹 Nitpick comments (1)
gears/system/usage-collector/usage-collector/src/domain/service.rs (1)

1201-1212: 🚀 Performance & Scalability | 🔵 Trivial

New per-request plugin round-trip for op/kind validation.

query_aggregated_usage_records now always issues an extra get_usage_type SPI call before dispatch, solely to resolve kind for the require_op_allowed_for_kind guard. This is a deliberate trade-off to reject invalid (op, kind) pairs before the plugin sees them, and is correctly implemented (single plugin handle reused, correct error lift to NotFound/400). Flagging only as an operational note: if aggregation query volume is high, consider whether the resolved UsageType catalog could eventually be cached (mirroring the batch-create catalog-dedup pattern already in this file) to avoid a network round-trip per aggregate call.

🤖 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 `@gears/system/usage-collector/usage-collector/src/domain/service.rs` around
lines 1201 - 1212, `query_aggregated_usage_records` adds a per-request
`get_usage_type` SPI round-trip before `require_op_allowed_for_kind`, which is
intentional but may hurt high-volume aggregation paths. If you want to address
this note, reuse the existing `plugin` flow in `service.rs` and add a small
`UsageType` lookup cache for the resolved catalog entry, following the same
dedup/caching approach already used elsewhere in this file for batch-create, so
repeated aggregate calls can avoid the extra network call while preserving the
current `NotFound`/`400` behavior.
🤖 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.

Outside diff comments:
In `@gears/system/usage-collector/docs/ADR/0013-deterministic-usage-record-id.md`:
- Around line 117-121: The Traceability “Amends” list in the ADR is incomplete
because it omits two files updated in the same change set. Update the list in
the Traceability section of the deterministic-usage-record-id ADR to include
DECOMPOSITION.md and features/event-deactivation.md alongside the existing
references so the documented traceability matches the full PR cohort.

---

Nitpick comments:
In `@gears/system/usage-collector/usage-collector/src/domain/service.rs`:
- Around line 1201-1212: `query_aggregated_usage_records` adds a per-request
`get_usage_type` SPI round-trip before `require_op_allowed_for_kind`, which is
intentional but may hurt high-volume aggregation paths. If you want to address
this note, reuse the existing `plugin` flow in `service.rs` and add a small
`UsageType` lookup cache for the resolved catalog entry, following the same
dedup/caching approach already used elsewhere in this file for batch-create, so
repeated aggregate calls can avoid the extra network call while preserving the
current `NotFound`/`400` behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1c721577-f8f7-4067-b5ec-ef333d439ef8

📥 Commits

Reviewing files that changed from the base of the PR and between 4d786ab and 2562796.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (42)
  • Cargo.toml
  • gears/system/usage-collector/docs/ADR/0013-deterministic-usage-record-id.md
  • gears/system/usage-collector/docs/DECOMPOSITION.md
  • gears/system/usage-collector/docs/DESIGN.md
  • gears/system/usage-collector/docs/domain-model.md
  • gears/system/usage-collector/docs/features/event-deactivation.md
  • gears/system/usage-collector/docs/features/usage-emission.md
  • gears/system/usage-collector/docs/features/usage-query.md
  • gears/system/usage-collector/docs/plugin-spi.md
  • gears/system/usage-collector/docs/sdk-trait.md
  • gears/system/usage-collector/docs/usage-collector-v1.yaml
  • gears/system/usage-collector/plugins/noop-usage-collector-plugin/src/plugin.rs
  • gears/system/usage-collector/plugins/noop-usage-collector-plugin/src/plugin_tests.rs
  • gears/system/usage-collector/usage-collector-sdk/Cargo.toml
  • gears/system/usage-collector/usage-collector-sdk/src/api.rs
  • gears/system/usage-collector/usage-collector-sdk/src/error.rs
  • gears/system/usage-collector/usage-collector-sdk/src/id.rs
  • gears/system/usage-collector/usage-collector-sdk/src/id_tests.rs
  • gears/system/usage-collector/usage-collector-sdk/src/lib.rs
  • gears/system/usage-collector/usage-collector-sdk/src/models.rs
  • gears/system/usage-collector/usage-collector-sdk/src/models_tests.rs
  • gears/system/usage-collector/usage-collector-sdk/src/plugin_api.rs
  • gears/system/usage-collector/usage-collector-sdk/src/reason.rs
  • gears/system/usage-collector/usage-collector-sdk/src/reason_tests.rs
  • gears/system/usage-collector/usage-collector-sdk/src/serde_helpers.rs
  • gears/system/usage-collector/usage-collector-sdk/src/serde_helpers_tests.rs
  • gears/system/usage-collector/usage-collector/Cargo.toml
  • gears/system/usage-collector/usage-collector/src/api/rest/dto.rs
  • gears/system/usage-collector/usage-collector/src/api/rest/dto_tests.rs
  • gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records.rs
  • gears/system/usage-collector/usage-collector/src/api/rest/handlers/usage_records_tests.rs
  • gears/system/usage-collector/usage-collector/src/domain/authz_tests.rs
  • gears/system/usage-collector/usage-collector/src/domain/error.rs
  • gears/system/usage-collector/usage-collector/src/domain/error_tests.rs
  • gears/system/usage-collector/usage-collector/src/domain/local_client.rs
  • gears/system/usage-collector/usage-collector/src/domain/query.rs
  • gears/system/usage-collector/usage-collector/src/domain/query_tests.rs
  • gears/system/usage-collector/usage-collector/src/domain/service.rs
  • gears/system/usage-collector/usage-collector/src/domain/service_tests.rs
  • gears/system/usage-collector/usage-collector/src/domain/test_support.rs
  • gears/system/usage-collector/usage-collector/src/domain/validation_tests.rs
  • gears/system/usage-collector/usage-collector/src/infra/sdk_error_mapping_tests.rs

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