Skip to content

fix(attribution): workspace id attr should be best-effort for self hosted users#4953

Merged
icecrasher321 merged 1 commit into
stagingfrom
fix/update-cost-500s
Jun 10, 2026
Merged

fix(attribution): workspace id attr should be best-effort for self hosted users#4953
icecrasher321 merged 1 commit into
stagingfrom
fix/update-cost-500s

Conversation

@icecrasher321

Copy link
Copy Markdown
Collaborator

Summary

Make workspace attribution best-effort on /api/billing/update-cost — workspaceId is now optional and resolved against this deployment before stamping (recording unattributed when foreign/missing), so self-hosted and headless flushes no longer die on usage_log FK violations and strand real cost in the dead-letter queue; failure logs now carry the Postgres SQLSTATE/constraint.

Type of Change

  • Bug fix

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@vercel

vercel Bot commented Jun 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 10, 2026 9:46pm

Request Review

@cursor

cursor Bot commented Jun 10, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches the internal billing ledger path and changes validation/attribution behavior, but billing still keys on the user and the change mainly reduces failure modes rather than altering charge logic.

Overview
/api/billing/update-cost no longer requires workspaceId and treats workspace attribution as best-effort instead of failing validation or ledger writes.

Before stamping usage_log, the route resolves the ID against this deployment’s workspace table via resolveAttributableWorkspaceId. Missing IDs (headless clients) and foreign IDs (self-hosted) are recorded unattributed (workspaceId: undefined), avoiding FK violations that previously failed the flush and stranded cost in the caller’s dead-letter queue. Known workspaces still get attribution unchanged.

On failure, error logs now include Postgres SQLSTATE and constraint (pgCode, pgConstraint) for easier diagnosis. Tests were updated/added for omitted and unknown workspace cases, with @sim/db mocked for the lookup.

Reviewed by Cursor Bugbot for commit 673f036. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 673f036. Configure here.

Comment thread apps/sim/app/api/billing/update-cost/route.ts
@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes workspace attribution on POST /api/billing/update-cost best-effort, fixing FK violations that caused self-hosted and headless billing flushes to fail and strand cost in the Go dead-letter queue. The route now resolves the request-supplied workspaceId against the local workspace table before writing to usage_log, silently falling back to unattributed when the workspace is absent or belongs to a foreign deployment.

  • workspaceId in the Zod contract is changed from required to optional, with an updated comment explaining the design rationale.
  • A new resolveAttributableWorkspaceId helper performs a PK lookup; unknown or missing workspace IDs yield undefined with a warning log rather than a 500 from the FK constraint.
  • Error logging now surfaces the PostgreSQL SQLSTATE code and constraint name, making similar incidents diagnosable from logs without a Drizzle query trace.
  • Tests are updated to cover the two new unattributed paths (omitted workspaceId and a foreign workspace ID that doesn't exist locally).

Confidence Score: 4/5

Safe to merge; the change narrows the blast radius of a known production failure and billing correctness (keyed on userId) is unaffected by workspace attribution.

The core billing path is well-tested and the fallback to unattributed is safe by design. Two minor observations remain: every hosted billing flush now incurs an extra DB round-trip for workspace validation, and the silent attribution downgrade leaves no signal in the trace span, which could make future regressions hard to detect. Neither blocks merging.

apps/sim/app/api/billing/update-cost/route.ts — the new workspace lookup is on the hot billing path and lacks a span attribute to track attribution outcomes.

Important Files Changed

Filename Overview
apps/sim/app/api/billing/update-cost/route.ts Core billing route updated to make workspace attribution best-effort: adds a DB pre-check (resolveAttributableWorkspaceId) that silently drops foreign/unknown workspace IDs to undefined, preventing FK violations; improved error logging now surfaces PostgreSQL error codes and constraint names.
apps/sim/app/api/billing/update-cost/route.test.ts Test suite updated to cover the new best-effort attribution logic: replaces the 400-rejection test with two new cases (omitted workspaceId → unattributed, foreign workspaceId → unattributed) using the shared DB chain mock.
apps/sim/lib/api/contracts/subscription.ts Contract updated to make workspaceId optional with a clear comment explaining the best-effort design; comment on the old "fail loud" contract correctly updated to match new semantics.

Sequence Diagram

sequenceDiagram
    participant Go as Go Mothership / Self-hosted Client
    participant Route as POST /api/billing/update-cost
    participant WS as workspace table (DB)
    participant UL as usage_log (DB)

    Go->>Route: "POST { userId, cost, workspaceId? }"
    Route->>Route: checkInternalApiKey()
    Route->>Route: parseRequest (Zod — workspaceId now optional)

    alt workspaceId provided
        Route->>WS: "SELECT id WHERE id = workspaceId LIMIT 1"
        alt workspace exists in this deployment
            WS-->>Route: "[{ id: workspaceId }]"
            Note over Route: attributedWorkspaceId = workspaceId
        else workspace NOT found (self-hosted / foreign)
            WS-->>Route: []
            Note over Route: WARN + attributedWorkspaceId = undefined
        end
    else workspaceId omitted (headless)
        Note over Route: attributedWorkspaceId = undefined (no DB query)
    end

    alt idempotencyKey present
        Route->>UL: "recordCumulativeUsage({ ..., workspaceId: attributedWorkspaceId })"
    else no idempotencyKey
        Route->>UL: "recordUsage({ ..., workspaceId: attributedWorkspaceId })"
    end
    UL-->>Route: success (no FK violation possible)
    Route->>Route: checkAndBillOverageThreshold(userId)
    Route-->>Go: 200 OK
Loading

Reviews (1): Last reviewed commit: "fix(attribution): workspace id attr shou..." | Re-trigger Greptile

Comment thread apps/sim/app/api/billing/update-cost/route.ts
Comment thread apps/sim/app/api/billing/update-cost/route.ts
@icecrasher321 icecrasher321 merged commit 1a5cf49 into staging Jun 10, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/update-cost-500s branch June 10, 2026 22:32
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