Skip to content

feat: offline license seat-usage reporting#1347

Open
brendan-kellam wants to merge 2 commits into
mainfrom
brendan/offline-usage-reporting-SOU-1380
Open

feat: offline license seat-usage reporting#1347
brendan-kellam wants to merge 2 commits into
mainfrom
brendan/offline-usage-reporting-SOU-1380

Conversation

@brendan-kellam

@brendan-kellam brendan-kellam commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

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

  • Ledger — a new append-only SeatUsageEvent table stores the org's absolute seat count at each change. A recordSeatChange helper 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.
  • Subscription anchor — the offline license payload gains an optional startDate (signed + verified in entitlements.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.
  • Report corecomputeMonthlyUsage() 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).
  • UI — an OfflineUsageReportCard on the license settings page (shown only for offline licenses carrying a startDate) renders the per-month table and a JSON export of completed months.

Notes / decisions

  • Peak (high-water mark) is the billing metric, matching the contract and the existing online yearlyPeakSeats path.
  • Contracted seats are intentionally not baked into the license — Provider holds that baseline and computes overage at invoicing; the deployment only reports the raw peak.
  • The overage model works with existing enforcement by issuing uncapped offline licenses (no 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).
  • startDate is appended last in the signed blob so older keys without it stay valid (the verify side reads it as optional and JSON.stringify omits 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 --noEmit clean across the touched files.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a seat-usage report to the license settings page for offline licenses
    • Tracks seat provisioning history over time
    • Displays peak provisioned user counts per subscription month
    • Enables JSON export of completed monthly usage data

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>
@github-actions

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds an end-to-end offline seat-usage reporting system. A SeatUsageEvent database ledger is introduced, populated by a recordSeatChange helper called in membership mutation transactions. The offline license schema gains an optional startDate field. A computeMonthlyUsage function aggregates ledger events into subscription-anchored monthly windows, and a new OfflineUsageReportCard UI component displays and exports the results on the license settings page.

Changes

Offline Seat Usage Ledger and Reporting

Layer / File(s) Summary
SeatUsageEvent schema and migration
packages/db/prisma/schema.prisma, packages/db/prisma/migrations/.../migration.sql
Adds the SeatUsageEvent Prisma model with orgId/timestamp composite index and CASCADE foreign key. The migration creates the table and backfills one baseline row per existing org using a LEFT JOIN count over UserToOrg.
Offline license startDate extension
packages/shared/src/entitlements.ts
Extends the offline license Zod schema with optional startDate. Updates signature serialization for back-compat, splits decode/expiry into getDecodedOfflineLicense/getValidOfflineLicense, and surfaces startDate in OfflineLicenseMetadata.
recordSeatChange utility and membership wiring
packages/web/src/features/billing/seatUsage.ts, packages/web/src/lib/authUtils.ts, packages/web/src/features/userManagement/actions.ts
Implements recordSeatChange to count org members and append a SeatUsageEvent row within a transaction. Integrates calls into the first-user bootstrap, auto-join, addUserToOrganization, and _removeUserFromOrg flows.
computeMonthlyUsage algorithm and tests
packages/web/src/features/billing/seatUsageReport.ts, packages/web/src/features/billing/seatUsageReport.test.ts
Defines SeatUsageLedgerEntry and MonthlyUsage types, implements UTC month-addition with day clamping, a carry-in seat-count helper, and computeMonthlyUsage that buckets events into subscription-anchored windows with peak/end tracking and completeness flags. Full Vitest suite covers boundary events, carry-forward, and empty-ledger cases.
OfflineUsageReportCard and license page integration
packages/web/src/app/(app)/settings/license/offlineUsageReportCard.tsx, packages/web/src/app/(app)/settings/license/page.tsx, CHANGELOG.md
Adds a client component displaying a reverse-chronological month table with In Progress badges and a JSON export of completed months. The license settings page queries seatUsageEvent rows, runs computeMonthlyUsage, and conditionally renders the card when both an offline license and startDate are present.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • sourcebot-dev/sourcebot#1165: Both PRs modify the same membership mutation flows in authUtils.ts and userManagement/actions.ts to add transactional side effects (audit events vs. seat-usage recording).
  • sourcebot-dev/sourcebot#1168: Both PRs modify the _removeUserFromOrg transaction in userManagement/actions.ts around the same userToOrg deletion step, adding distinct post-deletion side effects.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: offline license seat-usage reporting' directly and accurately summarizes the main change: implementing seat-usage reporting for offline licenses.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch brendan/offline-usage-reporting-SOU-1380

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 and usage tips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 | 🟠 Major

Add Serializable isolation and retry handling to seat-ledger transactions in authUtils.ts.

Three mutation paths in packages/web/src/lib/authUtils.ts record seat snapshots via recordSeatChange() 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). See packages/web/src/features/userManagement/actions.ts:126-129 for 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9320065 and 89c255b.

📒 Files selected for processing (11)
  • CHANGELOG.md
  • packages/db/prisma/migrations/20260618032126_add_seat_usage_event/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/shared/src/entitlements.ts
  • packages/web/src/app/(app)/settings/license/offlineUsageReportCard.tsx
  • packages/web/src/app/(app)/settings/license/page.tsx
  • packages/web/src/features/billing/seatUsage.ts
  • packages/web/src/features/billing/seatUsageReport.test.ts
  • packages/web/src/features/billing/seatUsageReport.ts
  • packages/web/src/features/userManagement/actions.ts
  • packages/web/src/lib/authUtils.ts

Comment thread CHANGELOG.md
## [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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
- [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

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