feat(observability): extend audit log, PostHog, and storage metering coverage#5269
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Billing webhooks gain idempotent instrumentation where needed, best-effort actor resolution, and threshold-billing refactors so overage settlement can be audited after commit without blocking blocks. Org seat changes audit/analytics move into Knowledge base documents now enforce storage quota on ingest and adjust usage counters on upload; hard deletes decrement storage inside the same transaction via new Reviewed by Cursor Bugbot for commit 914e2ee. Configure here. |
Greptile SummaryThis PR extends three observability layers — audit log, PostHog analytics, and storage metering — to cover resources and operations that were previously uninstrumented. It fills gaps identified in a codebase-wide coverage audit across billing webhooks, file downloads, copilot tool handlers, knowledge-base documents, and admin API surfaces.
Confidence Score: 5/5Safe to merge — all instrumentation is fire-and-forget, actor FK handling is correct, and all billing-critical operations remain protected from instrumentation failures. Every new billing webhook instrumentation block uses functions that are definitively fire-and-forget (
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Stripe Webhook Event] --> B{Event Type}
B --> C[invoice.payment_succeeded]
B --> D[invoice.payment_failed]
B --> E[customer.subscription.created]
B --> F[customer.subscription.deleted]
B --> G[charge.dispute.created/closed]
B --> H[manual_enterprise_subscription]
C --> C1[executeWithIdempotency]
C1 --> C2[Business logic renewal / usage reset]
C2 --> C3[resolveBillingActorId internal catch]
C3 --> C4[recordAudit fire-and-forget captureServerEvent fire-and-forget]
D --> D1[executeWithIdempotency]
D1 --> D2[Business logic]
D2 --> D3[try/catch guard]
D3 --> D4[resolveBillingActorId recordAudit captureServerEvent]
D2 --> D5[User blocking IF attemptCount ge 1]
E --> E1[executeWithIdempotency via Better Auth hook]
E1 --> E2[free to paid usage reset]
E2 --> E3[try/catch guard]
E3 --> E4[resolveSubscriptionActorId recordAudit captureServerEvent]
F --> F1[executeWithIdempotency]
F1 --> F2[Overage billing Org dormancy Pro restore]
F2 --> F3[resolveSubscriptionActorId internal catch]
F3 --> F4[recordAudit fire-and-forget captureServerEvent fire-and-forget]
G --> G1[executeWithIdempotency]
G1 --> G2[Block / unblock user or org]
G2 --> G3[getOrganizationOwnerId internal catch]
G3 --> G4[recordDisputeInstrumentation fire-and-forget]
H --> H1[executeWithIdempotency for audit only]
H1 --> H2[resolveActor via stripeCustomerId]
H2 --> H3[recordAudit captureServerEvent]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[Stripe Webhook Event] --> B{Event Type}
B --> C[invoice.payment_succeeded]
B --> D[invoice.payment_failed]
B --> E[customer.subscription.created]
B --> F[customer.subscription.deleted]
B --> G[charge.dispute.created/closed]
B --> H[manual_enterprise_subscription]
C --> C1[executeWithIdempotency]
C1 --> C2[Business logic renewal / usage reset]
C2 --> C3[resolveBillingActorId internal catch]
C3 --> C4[recordAudit fire-and-forget captureServerEvent fire-and-forget]
D --> D1[executeWithIdempotency]
D1 --> D2[Business logic]
D2 --> D3[try/catch guard]
D3 --> D4[resolveBillingActorId recordAudit captureServerEvent]
D2 --> D5[User blocking IF attemptCount ge 1]
E --> E1[executeWithIdempotency via Better Auth hook]
E1 --> E2[free to paid usage reset]
E2 --> E3[try/catch guard]
E3 --> E4[resolveSubscriptionActorId recordAudit captureServerEvent]
F --> F1[executeWithIdempotency]
F1 --> F2[Overage billing Org dormancy Pro restore]
F2 --> F3[resolveSubscriptionActorId internal catch]
F3 --> F4[recordAudit fire-and-forget captureServerEvent fire-and-forget]
G --> G1[executeWithIdempotency]
G1 --> G2[Block / unblock user or org]
G2 --> G3[getOrganizationOwnerId internal catch]
G3 --> G4[recordDisputeInstrumentation fire-and-forget]
H --> H1[executeWithIdempotency for audit only]
H1 --> H2[resolveActor via stripeCustomerId]
H2 --> H3[recordAudit captureServerEvent]
Reviews (32): Last reviewed commit: "fix(billing): guard the new subscription..." | Re-trigger Greptile |
|
@cursor review |
|
@cursor review |
|
@cursor review |
|
@cursor review |
|
@cursor review |
Address final audit flags: - Table import: move the 'columns added' audit OUT of the import transaction into a post-commit auditTableColumnsAdded() helper called by the three tx-owning callers, so a mid-import row-batch rollback no longer logs a false 'added N columns' (matches the PR's success-only discipline). - Omit empty-string analytics dimensions on workflow_lock_toggled (workspace_id) and organization_created (name), consistent with file_downloaded/copilot_chat_sent. - Restore an unrelated capacity-check comment removed incidentally. - Move copilot_chat_sent emit above the traceparent comment so the comment sits with the code it documents.
Address Cursor review: addImportColumns (async createColumns import path) now threads the importing userId into auditTableColumnsAdded instead of falling back to table.createdBy, so column additions are attributed to the actual member who ran the import rather than the table creator.
Per design decision: copilot files are working/conversational artifacts, so gating their materialization on storage quota would fail an agent operation mid-flow when a user is over limit, and metering them would inflate usage enough to block KB/workspace uploads indirectly. Remove copilot quota gates + copilot ingest metering entirely (revert copilot-file-manager, metadata, and the upload route's copilot branch to baseline) and drop the now-unused releaseDeletedFileStorage. KB document metering + quota enforcement (the deliberate, persistent storage path) is unchanged.
Address Cursor review: gate the group-sync effect on workspace metadata being loaded (activeWorkspace present) so the workspace and organization groups always update atomically. Acting during the load window paired the new workspace group with the previous workspace's org group; until metadata loads, events stay consistently attributed to the previous workspace.
Address Cursor review: async exports only emitted TABLE_EXPORTED when the background job reached 'ready', so an authorized export whose job later failed or was abandoned left no audit trail — inconsistent with the sync export route, which audits before streaming. Move the audit + analytics to the async route's authorization point (after the job is claimed/dispatched) and remove it from the runner. Drop the now-unused userId from TableExportPayload.
…king Address Greptile P1: the payment_failed audit hoisted an unguarded isSubscriptionOrgScoped DB read (for entity_type) directly before the attempt-count user-blocking block. A transient failure of that read would throw out of the handler and skip blocking. Wrap the whole audit/analytics block in try/catch (best-effort), and let the blocking compute its own isSubscriptionOrgScoped as it did originally — instrumentation can no longer abort payment processing.
…n in Stripe webhook idempotency recordAudit/captureServerEvent calls added for charge disputes, enterprise subscription provisioning, and free->paid subscription creation ran unconditionally with no idempotency guard, unlike their sibling handlers in the same files. Stripe redelivers webhooks at-least-once, so a retry would double-record the audit row and PostHog event even though the underlying DB writes were already idempotent.
…, guard concurrent table delete The org-scoped self-service audit-log endpoint matches org-level rows via metadata.organizationId (or resourceType=organization). Six recordAudit call sites (subscription create/cancel, admin credit issuance, threshold overage billing, charge disputes, credit purchase, invoice payment succeeded/failed) tagged org-scoped events with a differently-named key (referenceId/entityId/targetOrgId), making them invisible to org admins querying their own audit trail despite being stored in the DB. Add the missing organizationId key everywhere the pattern was missed. Also guard deleteTable's archive UPDATE with isNull(archivedAt) so a concurrent duplicate delete request is a no-op instead of re-archiving and re-firing a duplicate TABLE_DELETED audit row. Adds test coverage for the ORG_MEMBER_ADDED audit/analytics emission in acceptInvitation, which previously had none.
2b7ae68 to
409676e
Compare
|
@greptileai review |
|
@cursor review |
…ure email test handleInvoicePaymentFailed's new payment_failed audit instrumentation resolves the billing actor via an extra db.select before sendPaymentFailureEmails runs. The test's fixed select-response queue didn't account for it, so the org-admin lookup consumed the wrong queued row and the assertion saw zero email sends. Production behavior is unaffected — each query is independent; this was a mock-queue ordering issue only.
…ss the seat audit Cursor Bugbot flagged: reconcileOrganizationSeats committed the seat change and Stripe outbox enqueue in a transaction, then called syncSubscriptionUsageLimits outside it before recording the audit/ PostHog events. A thrown sync left a genuinely-changed seat count with no ORG_SEAT_PROVISIONED/DEPROVISIONED trail. Wrap the sync in its own try/catch (log + continue) so the events always fire for a committed change, matching the fire-and-forget instrumentation pattern used elsewhere in this PR.
|
@greptileai review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit e4cc7da. Configure here.
Greptile P1: the org-scope check I added for the audit-metadata fix (isSubscriptionOrgScoped) was a raw DB call with no error guard, unlike resolveSubscriptionActorId next to it. Since it ran inside the idempotency lambda with an unconditional rethrow, a transient DB error would abort and retry the whole webhook after the free -> paid usage reset had already committed. Wrap the actor/org-scope resolution + audit + analytics block in try/catch, matching the same guarded instrumentation pattern already used in handleInvoicePaymentFailed.
|
@greptileai review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 914e2ee. Configure here.
Summary
Extends the three observability layers — audit log, PostHog product analytics, and usage metering — to cover resources and actions that were previously uninstrumented. Found via a codebase-wide coverage audit; every gap below was a real blind spot.
Audit log
CREDENTIAL_ACCESSED), success-only.v1/adminprogrammatic surface (member/role/export ops) and copilot tool handlers (skills, custom tools, tables) — both previously bypassed audit entirely.AuditActionconstants (lock/unlock, table update/delete, etc.).PostHog
Metering
Hardening
admin-api) with a readable label instead of silently FK-failing; adds an awaitablerecordAuditNowfor pre-delete hooks.Type of Change
Testing
check:api-validationpass.Checklist