Skip to content

Bill Pro plans on active time-series#918

Draft
epompeii wants to merge 1 commit into
develfrom
active-series-billing
Draft

Bill Pro plans on active time-series#918
epompeii wants to merge 1 commit into
develfrom
active-series-billing

Conversation

@epompeii

Copy link
Copy Markdown
Member

Add a value-based billing dimension for the Pro plan: monthly-active series,
where a series is a distinct testbed x benchmark x measure an organization
reports to.

  • series_last_seen cache: a migration adds a per-series table holding the
    greatest report.end_time seen for each series, written on ingest in the same
    transaction as the metric inserts (so it cannot drift) and backfilled from
    existing metrics. Reads are a single index range scan over
    (organization_id, last_seen).
  • Billing: a new active_series Stripe meter (last aggregation). The nightly
    credit sweep posts each active Pro org's cumulative period-to-date private
    active-series count. Gated behind an optional series product in config, so it
    ships dark until configured.
  • Active-series billing is Pro-only; legacy Team and Enterprise stay on the
    metrics meter. The ingest upsert is plan-agnostic; the billing read decides
    who is billed.

Includes unit and property tests asserting the cache equals a from-scratch
distinct-series oracle, ingest atomicity, and backfill correctness.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

PR: #918
Base: devel
Head: active-series-billing
Commit: 93f70082af37cbdce11f5f6ecc380c625999f4b3


I've reviewed the full diff. Here is my assessment.

PR Review: Bill Pro plans on active time-series

This is a large, well-structured change that migrates Pro billing from per-metric metering (with monthly credit grants) to a value-based "monthly-active series" model, backed by a new series_last_seen cache. The code quality is high: extensive doc comments explaining the why, thorough unit/property tests, and clean adherence to project conventions.

Strengths

  • Excellent test coverage. series.rs ships a property test (cache_equals_oracle_property) that validates the SQL cache against an independent Rust oracle across 300 randomized rounds, plus targeted tests for iterations, branches, renames, soft-delete, backdating, and transaction rollback. The migration backfill SQL is itself pinned by a test (backfill_equals_oracle).
  • Correct transactional design. upsert_series_last_seen opens no transaction of its own and runs inside the ingest write_transaction!, so the cache cannot drift from the metrics it bills (CLAUDE.md compliant, verified by series_upsert_rolls_back_with_its_transaction).
  • Monotonic last_seen via the new max() SQL function prevents backdated/reprocessed reports from lowering recorded activity or retroactively cutting a bill.
  • Strong typing throughout (MeteredPlan, MeteredPlanBilling, SeriesCacheContext), exhaustive match on PlanStatus/PlanLevel so new variants force a decision, and BillingError::DateTime wraps the original ValidError rather than a string.
  • Re-subscription safety. prune_or_conflict_existing_plan correctly refuses to prune a still-live subscription: a transient Stripe error surfaces as 503 (never mistaken for "gone"), and only terminal/404 states prune. This is the right replacement for the deleted daily sweep.

Issues & concerns

1. Hot-path cost per Pro report (performance). post_series_usage runs synchronously on the request path: it acquires a fresh pooled connection (get_public_conn) and runs count_active for every Pro report before returning. Only the Stripe POST is detached. For chatty orgs this is an extra connection-checkout + indexed scan per report, and one Stripe meter event per report. The design is documented and the count is a single indexed range scan, but it's worth confirming Stripe meter-event rate limits are comfortable for high-frequency reporters, and that the extra connection acquisition won't contend under load.

2. Doc/comment overstates "no join." The migration and series.rs module docs say a billing read is "a single index range scan over (organization_id, last_seen), without joining through the entity tables." count_inner still inner-joins project (for the deleted/visibility filters). The denormalization avoids joining testbed/benchmark/measure, but a project join remains. Minor, but the comment reads as if no join occurs.

3. last-aggregation edge case is real but accepted. The comment in post_series_usage is candid: a failed post on a period's final report (with no later report before period close) under-bills, and same-second concurrent posts can tie non-deterministically. This is an availability-over-accuracy tradeoff now that the reconciliation sweep is gone. Reasonable, but it does mean Pro billing can silently under-bill in these cases with no backstop. Consider whether the ActiveSeriesBilledFailed OTEL counter is monitored/alerted, since it's now the only signal.

4. Lost configurability. PRO_TRIAL_PERIOD_DAYS = 30 is hardcoded, replacing the previously config-driven trial_coupon. Likely intentional, but trial length now requires a code change/redeploy.

5. Live test uses wall-clock. record_series_usage test calls DateTime::now(), against CLAUDE.md's deterministic-time rule. It's justified (live Stripe rejects fixed timestamps outside its window) and gated behind a billing key, and the comment explains it. Acceptable exception, just flagging.

Compliance checks

  • openapi.json was regenerated to drop base/trial_coupon from JsonProduct — consistent with the Rust changes (cargo gen-types appears run). ✓
  • schema.rs updated with the new table, joinables, and allow_tables_to_appear_in_same_query!. ✓
  • No emdashes; no Co-Authored-By; thiserror used; write_transaction!/macro usage correct; #[expect(...)] used over #[allow(...)]. ✓
  • One thing to double check: the new target_os-independent crates are unaffected, but series_last_seen adds a table — confirm no Docker/CI path-filter updates are needed (this is schema-only within existing crates, so likely fine).

Verdict

Solid, carefully-reasoned, and well-tested. No blocking bugs found. My only substantive asks before merge: (a) confirm the per-report synchronous count_active + connection checkout is acceptable under peak Pro load, and (b) ensure ActiveSeriesBilledFailed is alerted on, since it is now the sole guard against the documented under-billing gaps. The "no join" doc wording (#2) is a trivial fix.


Model: claude-opus-4-8

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchactive-series-billing
Testbedintel-v1
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
4.72 µs
(+2.20%)Baseline: 4.62 µs
4.86 µs
(97.10%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
4.58 µs
(+1.95%)Baseline: 4.49 µs
4.70 µs
(97.45%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
26.04 µs
(+2.04%)Baseline: 25.52 µs
26.55 µs
(98.08%)
Adapter::Rust📈 view plot
🚷 view threshold
3.58 µs
(+2.64%)Baseline: 3.48 µs
3.60 µs
(99.43%)
Adapter::RustBench📈 view plot
🚷 view threshold
3.57 µs
(+2.53%)Baseline: 3.48 µs
3.59 µs
(99.51%)
🐰 View full continuous benchmarking report in Bencher

@epompeii epompeii force-pushed the active-series-billing branch from 84c9153 to c1977a7 Compare June 25, 2026 13:26
@epompeii epompeii force-pushed the active-series-billing branch from c1977a7 to 98be1a1 Compare June 25, 2026 13:45
@epompeii epompeii force-pushed the active-series-billing branch from 98be1a1 to d427941 Compare June 25, 2026 14:12
@epompeii epompeii force-pushed the active-series-billing branch from d427941 to 33fd222 Compare June 25, 2026 14:53
@epompeii epompeii force-pushed the active-series-billing branch from 33fd222 to d91be06 Compare June 25, 2026 15:25
@epompeii epompeii force-pushed the active-series-billing branch from d91be06 to 9d3170c Compare June 25, 2026 15:58
@epompeii epompeii force-pushed the active-series-billing branch from 9d3170c to 4d313d0 Compare June 26, 2026 00:24
@epompeii epompeii self-assigned this Jun 26, 2026
@epompeii epompeii force-pushed the active-series-billing branch from 4d313d0 to c7253e9 Compare June 26, 2026 04:21
@epompeii epompeii force-pushed the active-series-billing branch from c7253e9 to 090ef76 Compare June 26, 2026 06:32
@epompeii epompeii force-pushed the active-series-billing branch from 090ef76 to 34f168c Compare June 26, 2026 06:48
@epompeii epompeii force-pushed the active-series-billing branch from 34f168c to eca5974 Compare June 26, 2026 06:57
@epompeii epompeii force-pushed the active-series-billing branch from eca5974 to 7a3b125 Compare June 26, 2026 07:09
@epompeii epompeii force-pushed the active-series-billing branch from 7a3b125 to d555f66 Compare June 26, 2026 07:32
@epompeii epompeii force-pushed the active-series-billing branch from d555f66 to 4c86eb2 Compare June 26, 2026 13:22
@epompeii epompeii force-pushed the active-series-billing branch from 4c86eb2 to 0683e88 Compare June 26, 2026 13:30
@epompeii epompeii force-pushed the active-series-billing branch from 0683e88 to 0efb312 Compare June 26, 2026 13:48
@epompeii epompeii force-pushed the active-series-billing branch from 0efb312 to 776d5e6 Compare June 26, 2026 13:55
@epompeii epompeii force-pushed the active-series-billing branch from 776d5e6 to 3b074f9 Compare June 27, 2026 04:07
@epompeii epompeii force-pushed the active-series-billing branch from 3b074f9 to 18ef1d0 Compare June 27, 2026 04:14
@epompeii epompeii force-pushed the active-series-billing branch from 18ef1d0 to 2e6ed84 Compare June 27, 2026 04:26
Use a single tiered Stripe price on the Bencher Pro product to bill both the monthly base fee (tier 1 flat fee) and the per-series step-ups on the active_series meter. This replaces the separate series product, the flat base price, and the monthly included-usage credit grants.

Pro subscriptions now carry the tiered active-series price plus bare metal, are identified by that price, and no longer bill the per-metric metrics meter. The 30-day free trial uses a native subscription-level Stripe trial (Checkout compatible) instead of a coupon. Active-series usage is posted to Stripe after each report (counted on the request connection, posted in a detached task) rather than by a nightly sweep, which is removed along with the account-credit grants.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant