feat: offline license seat-usage reporting#1347
Conversation
Track org seat count over time and surface a per-month usage report for offline (air-gapped) license Add-On User reconciliation. - Add a SeatUsageEvent ledger (append-only, absolute counts) with a migration that backfills a baseline row per existing org. - Record a row in the same transaction as every membership change via a recordSeatChange helper, wired into all add/remove chokepoints. - Carry a subscription startDate on the offline license payload (signed and verified) to anchor the reporting "Months". - Add computeMonthlyUsage: buckets the ledger into subscription-anchored UTC months and computes each month's peak provisioned seats. - Render an offline usage report card on the license settings page with a per-month table and a JSON export to send for reconciliation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
WalkthroughAdds an end-to-end offline seat-usage reporting system. A ChangesOffline Seat Usage Ledger and Reporting
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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 |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/web/src/lib/authUtils.ts (1)
69-89:⚠️ Potential issue | 🟠 MajorAdd Serializable isolation and retry handling to seat-ledger transactions in authUtils.ts.
Three mutation paths in
packages/web/src/lib/authUtils.tsrecord seat snapshots viarecordSeatChange()without explicit isolation levels. Concurrent membership writes can both read the same intermediate count and skip the actual peak, under-reporting billable usage.Apply this pattern to all three locations (lines 69, 132, and 231):
Suggested fix
-await __unsafePrisma.$transaction(async (tx) => { +await __unsafePrisma.$transaction(async (tx) => { // membership mutation... await recordSeatChange(tx, orgId); -}); +}, { + isolationLevel: Prisma.TransactionIsolationLevel.Serializable, +});Wrap the transaction block with retry logic to handle serialization conflicts (e.g.,
P2034). Seepackages/web/src/features/userManagement/actions.ts:126-129for an example of the correct pattern.🤖 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 `@packages/web/src/lib/authUtils.ts` around lines 69 - 89, Add Serializable isolation level and retry handling for serialization conflicts (like P2034) to all three transaction blocks in authUtils.ts that call recordSeatChange(). The three locations are around lines 69, 132, and 231. Wrap each __unsafePrisma.$transaction() call with retry logic following the pattern demonstrated in packages/web/src/features/userManagement/actions.ts lines 126-129 to ensure concurrent membership writes properly track peak seat usage and don't skip recording actual ledger entries due to isolation conflicts.
🤖 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 `@CHANGELOG.md`:
- Line 11: The CHANGELOG.md entry for PR `#1347` currently contains two sentences
separated by a period, which violates the changelog format requirement of a
single sentence followed by the PR link. Combine the two sentences into one
cohesive sentence that describes the seat-usage report feature while maintaining
all the key information about what it does (tracks seat count over time,
surfaces peak provisioned users, provides JSON export for reconciliation), then
ensure only the PR link follows the sentence.
---
Outside diff comments:
In `@packages/web/src/lib/authUtils.ts`:
- Around line 69-89: Add Serializable isolation level and retry handling for
serialization conflicts (like P2034) to all three transaction blocks in
authUtils.ts that call recordSeatChange(). The three locations are around lines
69, 132, and 231. Wrap each __unsafePrisma.$transaction() call with retry logic
following the pattern demonstrated in
packages/web/src/features/userManagement/actions.ts lines 126-129 to ensure
concurrent membership writes properly track peak seat usage and don't skip
recording actual ledger entries due to isolation conflicts.
🪄 Autofix (Beta)
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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b0fed549-352e-4c97-9325-45e5e00c457a
📒 Files selected for processing (11)
CHANGELOG.mdpackages/db/prisma/migrations/20260618032126_add_seat_usage_event/migration.sqlpackages/db/prisma/schema.prismapackages/shared/src/entitlements.tspackages/web/src/app/(app)/settings/license/offlineUsageReportCard.tsxpackages/web/src/app/(app)/settings/license/page.tsxpackages/web/src/features/billing/seatUsage.tspackages/web/src/features/billing/seatUsageReport.test.tspackages/web/src/features/billing/seatUsageReport.tspackages/web/src/features/userManagement/actions.tspackages/web/src/lib/authUtils.ts
| ## [Unreleased] | ||
|
|
||
| ### Added | ||
| - [EE] Added a seat-usage report to the license settings page for offline license reconciliation. It tracks the org's seat count over time and surfaces the peak number of provisioned users for each subscription month, with a JSON export to send for reconciliation. [#1347](https://github.com/sourcebot-dev/sourcebot/pull/1347) |
There was a problem hiding this comment.
Use a single sentence for the changelog entry.
This entry currently uses two sentences, but the changelog format requires one sentence followed by the PR link.
✏️ Suggested edit
-- [EE] Added a seat-usage report to the license settings page for offline license reconciliation. It tracks the org's seat count over time and surfaces the peak number of provisioned users for each subscription month, with a JSON export to send for reconciliation. [`#1347`](https://github.com/sourcebot-dev/sourcebot/pull/1347)
+- [EE] Added a seat-usage report to the license settings page for offline license reconciliation that tracks org seat counts over time and surfaces each subscription month’s peak provisioned users with a JSON export for reconciliation. [`#1347`](https://github.com/sourcebot-dev/sourcebot/pull/1347)As per coding guidelines, CHANGELOG.md entries must be a single sentence description followed by a PR link.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - [EE] Added a seat-usage report to the license settings page for offline license reconciliation. It tracks the org's seat count over time and surfaces the peak number of provisioned users for each subscription month, with a JSON export to send for reconciliation. [#1347](https://github.com/sourcebot-dev/sourcebot/pull/1347) | |
| - [EE] Added a seat-usage report to the license settings page for offline license reconciliation that tracks org seat counts over time and surfaces each subscription month's peak provisioned users with a JSON export for reconciliation. [`#1347`](https://github.com/sourcebot-dev/sourcebot/pull/1347) |
🤖 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 `@CHANGELOG.md` at line 11, The CHANGELOG.md entry for PR `#1347` currently
contains two sentences separated by a period, which violates the changelog
format requirement of a single sentence followed by the PR link. Combine the two
sentences into one cohesive sentence that describes the seat-usage report
feature while maintaining all the key information about what it does (tracks
seat count over time, surfaces peak provisioned users, provides JSON export for
reconciliation), then ensure only the PR link follows the sentence.
Source: Coding guidelines
Fixes SOU-1380
What
Adds seat-usage tracking over time and an in-app usage report for offline (air-gapped) license Add-On User reconciliation. An admin can see the greatest number of users provisioned during each subscription month and export it to send to
ar@sourcebot.dev.How it works
SeatUsageEventtable stores the org's absolute seat count at each change. ArecordSeatChangehelper writes a row in the same transaction as every membership mutation, wired into all add/remove chokepoints (authUtils.ts×3,userManagement/actions.ts). The migration backfills a baseline row per existing org.startDate(signed + verified inentitlements.ts), since the Order Form defines a "Month" as each successive one-month period from the subscription start date, not the calendar month. Companion signer change: sourcebot-dev/sb-license-key-utils#1.computeMonthlyUsage()buckets the ledger into subscription-anchored UTC months and computes each month's peak provisioned seats, accounting for counts carried in from a prior month. Pure and unit-tested (10 cases).OfflineUsageReportCardon the license settings page (shown only for offline licenses carrying astartDate) renders the per-month table and a JSON export of completed months.Notes / decisions
yearlyPeakSeatspath.seats→ no hard cap). One side effect to keep in mind: an uncapped offline license also makes anonymous access available (still gated by the org setting).startDateis appended last in the signed blob so older keys without it stay valid (the verify side reads it as optional andJSON.stringifyomits the undefined key).Testing
seatUsageReport.test.ts— 10 passing cases: headline 10→15→9, carry-forward across months, subscription-anchoring (non-calendar), end-of-month clamping, boundary-exact events, in-progress month, and empty/pre-ledger.tsc --noEmitclean across the touched files.🤖 Generated with Claude Code
Summary by CodeRabbit