fix(claude-team): read seat spend before the billing month closes - #2526
Conversation
The seat endpoint reports the month in progress and carries no history, so a month's spend freezes at the last read inside it and everything after that read is lost permanently — a later sync answers for the new month and cannot go back. Reading at 04:00 UTC therefore dropped the last twenty hours of every month by construction, and a failed run on the last day dropped a day more. Measured on a populated instance: July's final read landed 2026-07-30 04:02 UTC, leaving 44 hours unread, and the month came out $634.52 short of the reference implementation's $13,415.12 — 13 of the 57 seats with a non-zero amount, every one of them short rather than over. June's final read was 2026-06-24, leaving 164 hours. 23:50 leaves ten minutes instead of twenty hours, and stays off-peak: the other connectors occupy 02:00 to 06:00 and 11:00, and the customer's proxy runs a single browser instance. Both of this connector's streams tolerate the move — the code-metrics stream walks a trailing window and re-reads it. The check names the same failure as a warning on the month that just closed, because losing the tail is silent otherwise: nothing else notices that a figure is short, and no re-sync can fix it once the month is over. Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech>
|
Important Review available on request
Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
|
@coderabbitai review |
|
|
@coderabbitai review |
|
|
failed checks |
…LECT
The comment explaining why `closed_month` is not aliased sat between two SQL
tokens and used trimming Jinja delimiters. `{#-` eats the whitespace before the
tag and `-#}` the whitespace after, so the rendered SQL read
`closed_monthSELECT` and ClickHouse rejected the check with SYNTAX_ERROR.
An erroring data test fails the whole dbt build, so the ai shard lost the
ai_cost and seat-history fixtures with it — three red tests, one cause. The
source file looks correct; only the compiled SQL shows the defect.
Untrimmed delimiters keep the newline. The comment stays where it is: it
explains the line below it, and moving it would cost the reader the reason.
Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech>
…h-tail Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech> # Conflicts: # src/ingestion/connectors/ai/claude-team/descriptor.yaml
…h-tail Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech> # Conflicts: # src/ingestion/connectors/ai/claude-team/descriptor.yaml
…h-tail Signed-off-by: Gregory Gogin <grigoriy.gogin@constructor.tech>
Closes #2525. Part of #2479.
/api/organizations/{org}/overage_spend_limitsreports the billing month in progress and carries no history. A month's spend is therefore frozen at the last read that happened inside it, and everything after that read is lost for good — the next sync answers for the new month and cannot go back.Reading at 04:00 UTC dropped the last twenty hours of every month by construction. A failed or missing run on the last day dropped a day more.
Measured against the reference implementation
July, 145 seats, compared per address:
Our final read of July landed 2026-07-30 04:02 UTC — 44 hours unread. June's landed 2026-06-24, leaving 164 hours.
The transformation is exact: joining bronze to silver over the same month gives zero mismatches on both the amount and the ceiling. Nothing is miscomputed; the rows simply stop before the month does.
What lands
descriptor.yaml—50 23 * * *instead of0 4 * * *, leaving ten minutes of a month unread instead of twenty hours. Still off-peak: the other connectors occupy 02:00-06:00 and 11:00, and the customer's proxy runs a single browser instance. Both streams tolerate the move — the code-metrics stream walks a trailing window and re-reads it, so it is unaffected by the hour.assert_ai_overage_read_before_the_month_closed— a warning when the month that just closed was last read more than six hours before it ended. The schedule puts the final read ten minutes before the boundary; one failed run there makes it a day. Only the just-closed month is judged: an open month is still being read, and an older one can be neither re-read nor acted on.No descriptor version bump: the reconciler applies the CronWorkflow unconditionally (
kubectl apply, no-op when unchanged —lib/reconcile.sh), and ADR-0015's semver governs the connector's spec and data shape, neither of which moves here.Test plan
dbt parse— clean; the new test is in the manifest astest.ingestion.assert_ai_overage_read_before_the_month_closed.hours_unread = 44. It catches the defect that prompted it.pre-commit run --fileson both paths — yamlfmt and the file-hygiene hooks pass.An earlier draft of the check aliased the closed month to
period_monthin its SELECT; the alias shadowed the column, the WHERE compared the alias with itself, and every month collapsed into one group whose latest read belonged to the open month — inverting the result to a silent pass. The comment in the file records it so the shape is not reintroduced.