Skip to content

feat(balances): measure usage alerts per basis and fire on usage limits - #3248

Merged
charlietlamb merged 4 commits into
charlie/alert-basis-movefrom
charlie/alert-basis-firing
Sep 3, 2026
Merged

feat(balances): measure usage alerts per basis and fire on usage limits#3248
charlietlamb merged 4 commits into
charlie/alert-basis-movefrom
charlie/alert-basis-firing

Conversation

@charlietlamb

@charlietlamb charlietlamb commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Layer 4 of 6 (firing). Base: #3259, the pure move; this diff is the logic change on the moved file.

checkUsageAlerts moves to balances/usageAlerts/check/ as resolve → measure → wasThresholdCrossed → send.

check/
├── resolve/   customer scope (plan fallback), entity scope, org scope
├── measure/   balance bases from breakdown; usage_limit from the window counter
├── send/      idempotency key, payload, svix
└── wasThresholdCrossed.ts   pure over { usage, denominator, remaining }
  • One measurement per basis feeds a single crossing check; denominator: null (unlimited, included = 0, limit 0) skips percentage thresholds.
  • usage_limit reads both subjects' window counters at one now, so a rolled window measures 0 before the event and never fires a false remaining alert; it re-fires each window.
  • Webhook carries usage_alert.basis, usage_alert.filter, and a balance or usage_limit block. Idempotency key adds basis, filter key and window start.
  • Legacy V1 Postgres deductions pass no FullSubject; usage_limit alerts log and skip there.
  • Bug fix: applyUsageWindowUpdatesToFullSubject dropped customer-scope rows from entity subjects, so a cap inherited from the customer could never fire on an entity track.
  • New shared helpers: apiBalanceV1ToIncludedGrant, apiBalanceV1ToRecurringGrant (one-off grants report reset.interval: "one_off", not null, so recurring excludes that interval).

Tests: usage-alert-basis.test.ts (green locally), usage-alert-usage-limit.test.ts (13 cases; not yet run clean, see below), unit tests for the crossing check.

Local runs of the usage-limit file were blocked by the dev API resolving its secret-key cache through the shared cloud Dragonfly, where another stack on a different DB branch had cached the test org's feature ids; every product create then failed on entitlements_internal_feature_id_fkey. That is dev infra, not this change.


Summary by cubic

Usage alerts now measure against the configured basis—balance, included, recurring, or usage_limit—instead of always the aggregate balance, so windowed caps can trigger and re-fire alerts each window. The webhook payload carries the basis plus exactly one matching balance or usage_limit block, built as a typed union so consumers can narrow safely.

  • Alert checks resolve customer, plan fallback, entity, and org scopes before measuring.
  • usage_limit alerts match the enforced cap by feature and filter, stay dormant when the cap is missing, disabled, unlimited, or zero, and measure both window counters at the request timestamp so a rolled window never fires a false remaining alert.
  • Idempotency keys include the basis, filter, and window start.
  • Entity subjects retain customer-scoped usage-window rows so inherited customer caps can fire on entity tracks.
  • Balance breakdowns exclude one-off grants from recurring denominators, and deduction paths without full subjects skip usage_limit alerts.

Written for commit e26c006. Summary will update on new commits.

Review in cubic

Greptile Summary

The PR restructures usage-alert processing into scope resolution, basis-specific measurement, threshold evaluation, and webhook delivery.

  • [Improvements] Measures alerts independently against balance, included, recurring, or usage-limit bases and adds per-window idempotency.
  • [API changes] Adds the alert basis and a matching balance or usage-limit block to webhook payloads.
  • [Bug fixes] Preserves inherited customer usage-window rows on entity subjects, but the alert lookup currently filters out an entity-owned cap for customer- or plan-scoped alerts.

Confidence Score: 4/5

The PR is not yet safe to merge because customer- or plan-scoped alerts can be silently skipped when an entity-owned cap gates the tracked event.

The usage-limit lookup filters customer and plan alerts to customer-scoped counters even though entity tracking can be gated by an entity-scoped cap, leaving no measurement and therefore no webhook when that cap crosses the configured threshold.

Files Needing Attention: server/src/internal/balances/usageAlerts/check/measure/findUsageWindowLimitForAlert.ts

Important Files Changed

Filename Overview
server/src/internal/balances/usageAlerts/check/checkUsageAlerts.ts Coordinates the new resolve-measure-cross-send pipeline across customer, entity, and organization alert scopes.
server/src/internal/balances/usageAlerts/check/measure/findUsageWindowLimitForAlert.ts Selects usage-window limits by filter but incorrectly excludes an enforced entity cap when processing customer- or plan-scoped alerts.
server/src/internal/balances/usageAlerts/check/wasThresholdCrossed.ts Centralizes absolute and percentage threshold-crossing behavior over normalized measurements.
shared/api/webhooks/balances/balancesUsageAlertTriggered.ts Defines basis-specific webhook variants with required matching balance or usage-limit blocks.
server/src/internal/balances/utils/deductionV2/applyUsageWindowUpdatesToFullSubject.ts Retains both entity-owned and inherited customer usage-window rows on entity subjects.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  T[Track usage] --> R[Resolve alert scopes]
  R --> M[Measure configured basis]
  M --> C[Check threshold crossing]
  C -->|crossed| W[Build and send webhook]
  C -->|not crossed| S[Skip]
Loading
Prompt To Fix All With AI
### Issue 1
server/src/internal/balances/usageAlerts/check/measure/findUsageWindowLimitForAlert.ts:25-35
**Entity cap alerts are skipped**

When an entity has its own usage limit but the applicable alert is configured at customer or plan scope, `readsCustomerCountersOnly` rejects the entity-scoped limit that gated the track. For example, crossing a customer alert at 8 uses against an entity cap of 10 produces no webhook because the lookup returns no measurement.

```suggestion
	const filterKey = usageLimitFilterKey(alert.filter);
	return resolveUsageWindowLimits({
		ctx,
		fullSubject,
		featureIds: [feature.id],
	}).find((limit) => (limit.filter_key ?? "") === filterKey);
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (17): Last reviewed commit: "refactor(balances): basis default and ba..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Context used:

@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated
checkout Ignored Ignored Sep 3, 2026 5:38pm UTC
landing-page Ignored Ignored Sep 3, 2026 5:38pm UTC

Request Review

Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts Outdated
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from f1e5758 to 8886845 Compare September 3, 2026 12:40
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 8886845 to 4b28aad Compare September 3, 2026 12:41
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 4b28aad to 3629496 Compare September 3, 2026 12:43

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts Outdated
Comment thread shared/api/customers/cusFeatures/utils/convert/apiBalanceV1ToRecurringGrant.ts Outdated
Comment thread server/src/internal/balances/trackWebhooks/fireTrackWebhooks.ts Outdated
Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts Outdated
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 3629496 to 474aac6 Compare September 3, 2026 12:44
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 474aac6 to 47304e5 Compare September 3, 2026 12:51
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 47304e5 to d9a52c1 Compare September 3, 2026 12:53
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from d9a52c1 to a21c8fa Compare September 3, 2026 13:04
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from a21c8fa to 3c3af43 Compare September 3, 2026 13:20
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite September 3, 2026 13:22 Inactive
Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts Outdated
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 204541d to b8228d4 Compare September 3, 2026 14:08

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts
Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 329902c to b273ae7 Compare September 3, 2026 14:57
Comment thread shared/api/webhooks/balances/balancesUsageAlertTriggered.ts
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite September 3, 2026 15:37 Inactive

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread server/src/internal/balances/utils/usageWindows/resolveUsageWindowLimits.ts Outdated
Comment thread server/src/internal/balances/usageAlerts/check/checkUsageAlerts.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite September 3, 2026 15:46 Inactive
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite September 3, 2026 15:49 Inactive
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 7751383 to 85248f1 Compare September 3, 2026 15:58
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch 2 times, most recently from ee008e9 to 845177c Compare September 3, 2026 16:15
@charlietlamb
charlietlamb changed the base branch from charlie/alert-basis-validate to charlie/alert-basis-move September 3, 2026 16:15
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from 845177c to a166e1c Compare September 3, 2026 16:25
@vercel
vercel Bot temporarily deployed to Preview – autumn-vite September 3, 2026 17:17 Inactive

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 23 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

checkUsageAlerts becomes resolve → measure → wasThresholdCrossed → send,
one decision per file under usageAlerts/check. A measurer per basis
turns the tracked subjects into { usage, denominator, remaining }:
balance bases read the breakdown, usage_limit reads the window counter
of the cap sharing the alert's feature and filter, both sides at the
request clock so a rolled window reads 0 before the event.

The webhook is a typed union on usage_alert.basis carrying a balance or
usage_limit block; the idempotency key adds basis, filter and window
start. Entity subjects keep customer-scope window rows after deduction
so an inherited cap can fire on an entity track.
@charlietlamb
charlietlamb force-pushed the charlie/alert-basis-firing branch from e23f808 to e26c006 Compare September 3, 2026 17:38
@charlietlamb
charlietlamb merged commit 5395836 into dev Sep 3, 2026
19 checks passed
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