fix: recover metering events after delivery failures - #1639
Conversation
|
Warning Review limit reached
Next review available in: 1 minute Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (45)
📝 WalkthroughWalkthrough메터링 처리가 delivery 상태 머신과 멱등성 계약을 사용하도록 변경되었습니다. 이벤트 ID는 결정적으로 생성되며, Redis 사용량 저장소와 인메모리 어댑터는 quota 결과와 delivery를 재시도 시 복원합니다. 관련 공개 API, 문서, 테스트 및 smoke 검증도 갱신되었습니다. Changes메터링 재시도 및 이벤트 계약
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 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 |
efdfcd0 to
28e369b
Compare
📊 Benchmark Results✅ All benchmarks passed
Updated: 2026-07-30T18:33:38.745Z · Commit: e4484d2 |
1356fe1 to
8ee1f1b
Compare
There was a problem hiding this comment.
Actionable comments posted: 18
🤖 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 `@examples/quick-start-lambda/src/integrations/inMemoryMetering.ts`:
- Around line 108-158: Replace the unconditional fallback in
InMemoryRedisClient.eval() with an explicit throw that includes a stable
diagnostic message identifying the unsupported script, rather than returning
[1]. Apply the same fallback change in
examples/quick-start-lambda/src/integrations/inMemoryMetering.ts lines 108-158
and
packages/create-croco-app/templates/saas/apps/api-server/src/inMemoryAdapters.ts
lines 350-393; preserve all existing recognized-script branches.
- Around line 20-29: Separate the key sets used for usage-record deduplication
and idempotency reservation. In
examples/quick-start-lambda/src/integrations/inMemoryMetering.ts lines 20-29,
update record() to use a dedicated record-dedupe Set while leaving
isIdempotent() on its reservation Set; apply the same change in
packages/create-croco-app/templates/saas/apps/api-server/src/inMemoryAdapters.ts
lines 209-214 so record(), including calls from checkAndRecordWithinQuota(),
uses the dedicated Set.
In
`@packages/create-croco-app/templates/saas/apps/api-server/src/tests/SaasDemo.spec.ts`:
- Around line 73-75: Update the asynchronous rejection assertion for
manager.claimMeteringProcessingOrThrow in SaasDemo.spec.ts to also use
rejects.toThrow, while preserving the existing metering/duplicate-record code
check so the test verifies both the thrown error type and its code.
In
`@packages/docs/src/content/docs/api/metering-core/src/classes/QuotaExceededEvent.md`:
- Around line 17-25: Update the QuotaExceededEvent JSDoc `@example` in
QuotaExceededEvent.ts to instantiate the event using positional arguments
matching its constructor signature (tenantId, meterId, currentUsage, quota, with
optional idempotencyKey and operationId), and remove the unsupported timestamp
field. Regenerate the TypeDoc output rather than editing the generated Markdown
directly, then verify with pnpm docs:api:check.
In
`@packages/docs/src/content/docs/api/metering-core/src/classes/RedisUsageStorage.md`:
- Around line 53-54: Update the original TypeScript JSDoc or contract text that
generates RedisUsageStorage.md so the idempotency statement explicitly scopes
deduplication and quota reuse to the composite key of tenantId, meterId, and
idempotencyKey, matching record() documentation. Regenerate the TypeDoc output
instead of editing the generated Markdown directly.
In
`@packages/docs/src/content/docs/api/metering-core/src/classes/UsageRecordedEvent.md`:
- Around line 17-25: Update the UsageRecordedEvent JSDoc `@example` and generated
UsageRecordedEvent.md example to call the positional constructor signature
(tenantId, meterId, value, idempotencyKey, optional metadata, optional
operationId). Add a valid idempotencyKey, remove the unsupported recordedAt
field, and regenerate the documentation while preserving the intended metadata
example.
In `@packages/metering-core/src/libs/IdempotencyManager.ts`:
- Around line 288-306: Update both IdempotencyManager Redis scripts at
packages/metering-core/src/libs/IdempotencyManager.ts:288-306 and :347-364 so
their return values distinguish an actual state transition or deletion from a
status/token mismatch or missing record. Preserve the successful result only
when the operation occurs, have callers inspect the result and recognize
fencing, and record diagnostic evidence at the retry/Problem boundary; the
second site requires the same applied/deletion-result handling as the
PUBLISHING-to-EVENTS_PENDING transition.
- Around line 262-268: Update the delivery handling in the claim Lua script and
its corresponding retry/claim path to preserve the original JSON string from
ARGV[2] without cjson.decode or cjson.encode round-tripping. Store the raw
delivery payload in state and return that same raw string when restoring it,
while keeping state-machine fields encoded normally.
In `@packages/metering-core/src/libs/MeteringService.ts`:
- Around line 131-148: Update the catch handling in MeteringService’s metering
flow so failures after persistenceCompleted but before publishingClaimed do not
leave the PROCESSING lease blocking retries until expiry. Add the appropriate
idempotency-manager transition that immediately invalidates or expires the lease
while preserving the PROCESSING state, and keep the existing
releaseMeteringEvents and abortMeteringProcessing paths unchanged for their
respective conditions.
In `@packages/metering-core/src/libs/RedisUsageStorage.ts`:
- Around line 103-110: Move the dedupeKey Redis GET and valid recorded-result
return ahead of the ZRANGEBYSCORE query and parsing loop in the surrounding Lua
logic. Ensure valid cached quota results return immediately, while sorted-set
aggregation runs only for legacy fallback or new-record paths.
- Around line 103-110: Update the recordedResult parsing in
checkAndRecordWithinQuota so recordedUsage is accepted only when it is a valid
numeric value before returning it via the Redis Lua result. When parsing or
numeric conversion fails, use the existing legacy fallback return { 0,
currentUsage } so the response always contains both quota values and cannot
propagate undefined or NaN.
In `@packages/metering-core/src/libs/UsageStorage.ts`:
- Around line 66-70: UsageStorage의 checkAndRecordWithinQuota 계약을 선택 사항으로 두지 말고
필수 메서드로 변경하여, 모든 구현이 동일 idempotencyKey의 최초 quota 결과를 복원하도록 타입 수준에서 요구하세요. 기존
UsageStorage 호환성이 필요한 경우 해당 메서드를 필수로 포함하는 별도 quota 저장소 계약을 정의하고 quota meter 경로가
그 계약을 사용하도록 수정하세요.
In `@packages/metering-core/src/tests/events/Events.spec.ts`:
- Around line 108-118: QuotaExceededEvent 테스트가 idempotency key만 검증하고
operationId가 eventId 생성에 반영되는지 확인하지 않습니다. 기존 테스트에 동일한 idempotency key와 서로 다른
operationId로 생성한 이벤트를 추가하고 eventId가 서로 다른지 검증하세요. 동일 operationId와 키의 재시도는 기존처럼
동일한 eventId를 유지하며, eventId 형식 및 민감한 키 비노출 검증도 유지하세요.
In `@packages/metering-core/src/tests/IdempotencyManager.spec.ts`:
- Around line 440-443: Update the asynchronous rejection assertions in
IdempotencyManager.spec.ts, including the tests around
claimMeteringProcessingOrThrow, to use rejects.toThrow(DuplicateRecordProblem)
instead of rejects.toBeInstanceOf(DuplicateRecordProblem) at all identified
occurrences.
In `@packages/metering-core/src/tests/RedisUsageStorage.spec.ts`:
- Line 126: Remove the Redis Lua source-string assertion around mockRedis.eval
in RedisUsageStorage tests, including the check for "EXISTS". Retain the
behavioral assertion that verifies duplicate record operations do not call zadd.
- Around line 958-1019: Extend the RedisUsageStorage regression coverage around
checkAndRecordWithinQuota to simulate a legacy dedupe entry containing the
string "1", where the Lua script must return the fallback result {0,
currentUsage}. Verify the request is treated as successful with the current
usage and that the usage record is not written again, using the existing Redis
mock and assertions in the rejected quota replay test as a guide.
- Around line 982-985: Replace the source-text assertions in
packages/metering-core/src/tests/RedisUsageStorage.spec.ts:982-985 with
behavioral assertions that verify SET receives dedupeKey value quota:1:13 and
usageRecordCount remains 0, avoiding expect calls inside the mock that become
RedisProblem errors. At
packages/metering-core/src/tests/RedisUsageStorage.spec.ts:126, remove the
EXISTS string assertion and rely on the existing zadd-not-called assertion at
line 257.
In `@scripts/quick-start-lambda-smoke.mts`:
- Around line 217-258: Update the async probe IIFE in the “metering replay”
phase to attach a catch handler that prints the failure error and sets
process.exitCode to 1. Preserve the existing successful execution flow and probe
assertions, and ensure rejected promises are explicitly propagated to the
process status before runPhase completes.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: b5c4d46c-350c-44d9-9bdb-9d858e78aa19
📒 Files selected for processing (37)
.changeset/calm-geckos-recover.mdexamples/quick-start-lambda/src/integrations/inMemoryMetering.tspackages/create-croco-app/templates/saas/apps/api-server/src/inMemoryAdapters.tspackages/create-croco-app/templates/saas/apps/api-server/src/tests/SaasDemo.spec.tspackages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.mdpackages/docs/src/content/docs/api/metering-core/src/classes/IdempotencyManager.mdpackages/docs/src/content/docs/api/metering-core/src/classes/QuotaExceededEvent.mdpackages/docs/src/content/docs/api/metering-core/src/classes/RedisUsageStorage.mdpackages/docs/src/content/docs/api/metering-core/src/classes/UsageRecordedEvent.mdpackages/docs/src/content/docs/api/metering-core/src/interfaces/RedisClient.mdpackages/docs/src/content/docs/api/metering-core/src/interfaces/UsageStorage.mdpackages/docs/src/content/docs/api/metering-core/src/type-aliases/MeteringProcessingClaim.mdpackages/docs/src/content/docs/api/metering-core/src/type-aliases/PendingMeteringDelivery.mdpackages/docs/src/content/docs/api/metering-upstash/src/classes/UpstashRedisClient.mdpackages/events-core/src/libs/DomainEvent.tspackages/events-core/src/tests/DomainEvents.spec.tspackages/metering-core/README.mdpackages/metering-core/src/index.tspackages/metering-core/src/libs/IdempotencyManager.tspackages/metering-core/src/libs/MeteringService.tspackages/metering-core/src/libs/RedisClient.tspackages/metering-core/src/libs/RedisUsageStorage.tspackages/metering-core/src/libs/UsageStorage.tspackages/metering-core/src/libs/events/QuotaExceededEvent.tspackages/metering-core/src/libs/events/UsageRecordedEvent.tspackages/metering-core/src/libs/events/eventIdentity.tspackages/metering-core/src/tests/IdempotencyManager.spec.tspackages/metering-core/src/tests/MeteringService.spec.tspackages/metering-core/src/tests/QuotaManager.spec.tspackages/metering-core/src/tests/RedisMetering.integration.spec.tspackages/metering-core/src/tests/RedisUsageStorage.spec.tspackages/metering-core/src/tests/UsageAggregator.spec.tspackages/metering-core/src/tests/UsageStorage.spec.tspackages/metering-core/src/tests/events/Events.spec.tspackages/metering-upstash/src/libs/UpstashRedisClient.tspublic-api-surface.snapshot.jsonscripts/quick-start-lambda-smoke.mts
8ee1f1b to
e82af59
Compare
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/metering-core/src/libs/RedisUsageStorage.ts (1)
97-119: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winlegacy dedupe 값 폴백이 quota 초과를 항상
exceeded: false로 보고합니다.
record()/isIdempotent()(및 이전 배포 버전)가 남긴 dedupe 값은'1'이므로 100–104행 파싱이 실패하고, 117–119행에서 무조건{ 0, currentUsage }를 반환합니다. 롤링 배포 중 첫 시도가 legacy 형식을 남긴 뒤 재시도가 들어오면 실제로 quota를 초과한 요청이 정상 처리로 보고되어QuotaExceededProblem과QuotaExceededEvent가 모두 누락됩니다. 저장된 사용량 기준으로 초과 여부를 재판정하면 legacy 경로에서도 quota 계약이 유지됩니다.🐛 제안 수정
if recordedResult then - return { 0, currentUsage } + local legacyExceeded = currentUsage > quota + return { legacyExceeded and 1 or 0, currentUsage } end코딩 가이드라인의 “silent fallback으로 실패를 숨기지 말고 ... 실패와 복구 경로를 모델링한다”에 근거합니다.
🤖 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/metering-core/src/libs/RedisUsageStorage.ts` around lines 97 - 119, Update the legacy dedupe fallback in the Lua logic used by RedisUsageStorage so a stored value such as "1" does not unconditionally return exceeded=false. When recordedResult is present but the quota-formatted parse fails, recompute the exceeded flag from currentUsage using the same quota limit and return that result with currentUsage, preserving the existing behavior for valid quota-formatted dedupe values.Source: Coding guidelines
packages/metering-core/src/tests/RedisMetering.integration.spec.ts (2)
232-235: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win비동기 실패 검증에
rejects.toThrow를 추가하세요.구조 검증은 유지하되, 동일 Promise에
rejects.toThrow()를 먼저 적용해 비동기 오류 검증 계약을 충족하세요.
packages/metering-core/src/tests/RedisMetering.integration.spec.ts#L232-L235: 제한 Redis client 실패 assertion에rejects.toThrow()를 추가하세요.packages/metering-core/src/tests/RedisMetering.integration.spec.ts#L261-L264: ambiguous response 실패 assertion에 동일하게 추가하세요.As per coding guidelines, “비동기 오류는
rejects.toThrow로 검증한다.”🤖 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/metering-core/src/tests/RedisMetering.integration.spec.ts` around lines 232 - 235, Update both failure assertions in RedisMetering.integration.spec.ts at lines 232-235 and 261-264 to first apply rejects.toThrow() to the same Promise, while preserving the existing rejects.toMatchObject structure checks for the error code and operation.Source: Coding guidelines
221-225: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win재시도와 실패 시도에 동일한 Redis key prefix를 사용하세요.
keyPrefix: ""때문에 제한 클라이언트의 실패 시도는 일반 클라이언트가 재시도·검증하는${connection.keyPrefix}키와 다른 물리 키를 사용합니다. 따라서 실패 후 marker가 남아도 이 테스트가 통과할 수 있습니다.수정안
- keyPrefix: "", + keyPrefix: connection.keyPrefix,🤖 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/metering-core/src/tests/RedisMetering.integration.spec.ts` around lines 221 - 225, Update the restrictedClient setup in the Redis metering integration test to use the same key prefix as connection.client, specifically connection.keyPrefix, instead of an empty prefix. Keep the retry and verification paths unchanged so both clients target the identical physical Redis key.
🤖 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 `@packages/metering-core/src/libs/events/eventIdentity.ts`:
- Around line 3-13: Update createMeteringEventId so the SHA-256 input excludes
operationId and hashes only eventName, tenantId, meterId, and idempotencyKey.
Keep the existing argument and return structure otherwise unchanged, ensuring
identical idempotency inputs always produce the same event ID.
In `@packages/metering-core/src/libs/IdempotencyManager.ts`:
- Around line 242-279: Update all five metering transition methods, including
markMeteringEventsPublishing and the corresponding release/claim transition
methods, so each Redis Lua script returns a failure reason code alongside its
success flag, distinguishing missing keys, unexpected status, and token fencing
mismatches. Propagate the returned status and reason through
requireStagedTransition so transition failures retain actionable evidence about
the current state and expected token/status before throwing.
- Around line 448-452: Update requireStagedTransition and the
releaseMeteringEvents catch path so a zero-row transition after processing
completion is treated as an idempotent no-op rather than throwing
DuplicateRecordProblem. Preserve and rethrow the original QuotaExceededProblem,
while distinguishing lease/token fencing or KV-mismatch failures with a separate
diagnostic Problem or code.
In `@packages/metering-core/src/libs/MeteringService.ts`:
- Around line 233-247: Update the publishDelivery/recordUsage error path so that
once completeMeteringProcessing succeeds, a later quotaManager.validateOrThrow
failure is recognized as completed rather than still claimed. Propagate
completion status from publishDelivery or detect the COMPLETED state before
cleanup, and prevent releaseMeteringEvents from attempting cleanup or reverting
the delivery to PUBLISHING in this case.
In `@packages/metering-core/src/tests/IdempotencyManager.spec.ts`:
- Around line 152-167: Update the script handler in InMemoryRedisClient so
unsupported Lua scripts throw an error instead of falling through to the final
successful [1] response. Preserve the existing returns for recognized DEL and
other supported script branches, but remove or replace the unconditional
fallback success path.
In `@packages/metering-core/src/tests/MeteringService.spec.ts`:
- Around line 674-683: Strengthen the retry deduplication regression tests in
packages/metering-core/src/tests/MeteringService.spec.ts: for lines 674-683,
increment the checkAndRecordWithinQuota call counter on every invocation and
separately assert total calls versus first-time records per
usageRecord.idempotencyKey; for lines 585-613, key persistedOperations by
usage.idempotencyKey, ignore repeated keys while tracking total record calls,
and assert both values; for lines 625-660, key records by
usageRecord.idempotencyKey and verify the second call reuses the first quota
result.
In `@scripts/quick-start-lambda-smoke.mts`:
- Around line 199-213: Update assertServerOutput to briefly poll for
expectedText after the HTTP response, allowing pending child-process stdout data
events to arrive before failing. Preserve the existing label-specific error and
success logging, and time out with the current assertion error if the text never
appears.
---
Outside diff comments:
In `@packages/metering-core/src/libs/RedisUsageStorage.ts`:
- Around line 97-119: Update the legacy dedupe fallback in the Lua logic used by
RedisUsageStorage so a stored value such as "1" does not unconditionally return
exceeded=false. When recordedResult is present but the quota-formatted parse
fails, recompute the exceeded flag from currentUsage using the same quota limit
and return that result with currentUsage, preserving the existing behavior for
valid quota-formatted dedupe values.
In `@packages/metering-core/src/tests/RedisMetering.integration.spec.ts`:
- Around line 232-235: Update both failure assertions in
RedisMetering.integration.spec.ts at lines 232-235 and 261-264 to first apply
rejects.toThrow() to the same Promise, while preserving the existing
rejects.toMatchObject structure checks for the error code and operation.
- Around line 221-225: Update the restrictedClient setup in the Redis metering
integration test to use the same key prefix as connection.client, specifically
connection.keyPrefix, instead of an empty prefix. Keep the retry and
verification paths unchanged so both clients target the identical physical Redis
key.
🪄 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: ASSERTIVE
Plan: Pro Plus
Run ID: c4bfcbe0-107a-45c8-adb5-058a3525e090
📒 Files selected for processing (41)
.changeset/calm-geckos-recover.mdexamples/quick-start-lambda/src/integrations/inMemoryMetering.tspackages/create-croco-app/templates/saas/apps/api-server/src/inMemoryAdapters.tspackages/create-croco-app/templates/saas/apps/api-server/src/tests/SaasDemo.spec.tspackages/docs/src/content/docs/api/credits-core/src/classes/CreditLedgerStore.mdpackages/docs/src/content/docs/api/events-core/src/classes/DomainEvent.mdpackages/docs/src/content/docs/api/metering-core/src/classes/IdempotencyManager.mdpackages/docs/src/content/docs/api/metering-core/src/classes/QuotaExceededEvent.mdpackages/docs/src/content/docs/api/metering-core/src/classes/RedisUsageStorage.mdpackages/docs/src/content/docs/api/metering-core/src/classes/UsageRecordedEvent.mdpackages/docs/src/content/docs/api/metering-core/src/interfaces/RedisClient.mdpackages/docs/src/content/docs/api/metering-core/src/interfaces/UsageStorage.mdpackages/docs/src/content/docs/api/metering-core/src/type-aliases/MeteringProcessingClaim.mdpackages/docs/src/content/docs/api/metering-core/src/type-aliases/PendingMeteringDelivery.mdpackages/docs/src/content/docs/api/metering-upstash/src/classes/UpstashRedisClient.mdpackages/docs/src/content/docs/api/problems-core/src/classes/Problem.mdpackages/docs/src/content/docs/api/problems-core/src/variables/CROCO_PROBLEM_CODE_REGISTRY.mdpackages/events-core/src/libs/DomainEvent.tspackages/events-core/src/tests/DomainEvents.spec.tspackages/metering-core/README.mdpackages/metering-core/src/index.tspackages/metering-core/src/libs/IdempotencyManager.tspackages/metering-core/src/libs/MeteringService.tspackages/metering-core/src/libs/QuotaManager.tspackages/metering-core/src/libs/RedisClient.tspackages/metering-core/src/libs/RedisUsageStorage.tspackages/metering-core/src/libs/UsageStorage.tspackages/metering-core/src/libs/events/QuotaExceededEvent.tspackages/metering-core/src/libs/events/UsageRecordedEvent.tspackages/metering-core/src/libs/events/eventIdentity.tspackages/metering-core/src/tests/IdempotencyManager.spec.tspackages/metering-core/src/tests/MeteringService.spec.tspackages/metering-core/src/tests/QuotaManager.spec.tspackages/metering-core/src/tests/RedisMetering.integration.spec.tspackages/metering-core/src/tests/RedisUsageStorage.spec.tspackages/metering-core/src/tests/UsageAggregator.spec.tspackages/metering-core/src/tests/UsageStorage.spec.tspackages/metering-core/src/tests/events/Events.spec.tspackages/metering-upstash/src/libs/UpstashRedisClient.tspublic-api-surface.snapshot.jsonscripts/quick-start-lambda-smoke.mts
💤 Files with no reviewable changes (1)
- packages/metering-core/src/libs/QuotaManager.ts
e82af59 to
2174216
Compare
2174216 to
607041e
Compare
Outcome
Verification
pnpm testpnpm typecheckpnpm lintpnpm checkpnpm public-api:checkpnpm docs:api:checkpnpm quick-start-lambda:smokepnpm create-croco-app:smoke -- goal-saas-apiReview notes
The metering delivery scripts intentionally require multi-key Redis script support. Redis Cluster deployments that cannot keep these keys in one script execution remain unsupported and are documented explicitly.
Fixes #1574.
Summary by CodeRabbit