Conversation
Organizations billed through an external provider such as the Vercel Marketplace pay through that provider. The billing page still linked their open-invoice banner, unsubscribe errors and credit dialogs to Stripe's hosted invoice page or portal, where they could pay the same invoice a second time. Link to external_billing_provider_invoices_url instead whenever it is set, and wait for billing to load before building the open-invoice banner so an early invoices response can't fall back to the Stripe link. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
🤖 CI report✅ Trunk lane — non-backend laneThis PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes.
|
| Function | Location | Complexity | Limit |
|---|---|---|---|
UnsubscribeSurveyModal |
frontend/src/scenes/billing/UnsubscribeSurveyModal.tsx:40 |
23 | 10 |
determineBillingAlert |
frontend/src/scenes/billing/billingLogic.tsx:1481 |
22 | 10 |
CreditCTAHero |
frontend/src/scenes/billing/CreditCTAHero.tsx:16 |
17 | 10 |
deactivateProduct |
frontend/src/scenes/billing/billingLogic.tsx:881 |
14 | 10 |
registerInstrumentationProps |
frontend/src/scenes/billing/billingLogic.tsx:1661 |
14 | 10 |
<anonymous> |
frontend/src/scenes/billing/billingLogic.tsx:1253 |
11 | 10 |
✅ Duplication (Python) — clean
New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.
✅ Duplication (TypeScript) — clean
New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.
⚠️ Bundle size — 🔺 +6.9 KiB (+0.0%)
Uncompressed size of every built .js bundle, compared against the base branch.
Total: 68.86 MiB · 🔺 +6.9 KiB (+0.0%)
| File | Size | Δ vs base |
|---|---|---|
render-query/src/render-query/render-query.js |
20.15 MiB | 🔺 +1.9 KiB (+0.0%) |
posthog-app/src/scenes/session-recordings/playlist/SessionRecordingsPlaylistScene.js |
22.4 KiB | 🔺 +1.4 KiB (+6.8%) |
posthog-app/_parent/products/workflows/frontend/Broadcasts/BroadcastScene.js |
63.6 KiB | 🔺 +1.3 KiB (+2.1%) |
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js |
180.9 KiB | 🔺 +1.1 KiB (+0.6%) |
Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report
✅ Eager graph — within budget
How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.
| Root | Eager (shipped) | Δ vs base | Budget |
|---|---|---|---|
entry (logged-out pages, app bootstrap)src/index.tsx |
1.57 MiB · 22 files | 🔺 +88 B (+0.0%) | █████████░ 85.5% of 1.84 MiB |
logged-out boot: index + App + bootApp (preloaded by every page, including /login)src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts |
3.51 MiB · 629 files | 🔺 +1.1 KiB (+0.0%) | █████████░ 87.2% of 4.03 MiB |
authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
7.33 MiB · 2,332 files | 🔺 +1.9 KiB (+0.0%) | █████████░ 87.9% of 8.34 MiB |
🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
Largest files eagerly shipped from src/index.tsx
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.3 KiB | src/index.tsx |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
| 854 B | src/scenes/ChunkLoadErrorBoundary.tsx |
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
| Size | File |
|---|---|
| 301.8 KiB | ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 216.0 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 100.4 KiB | src/lib/api.ts |
| 88.4 KiB | src/products.tsx |
| 69.4 KiB | src/lib/lemon-ui/icons/icons.tsx |
| 40.1 KiB | src/lib/utils/eventUsageLogic.ts |
| 38.7 KiB | ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js |
| 33.9 KiB | ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js |
| 28.4 KiB | src/scenes/scenes.ts |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 301.8 KiB | ../node_modules/.pnpm/posthog-js@1.434.14_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 271.7 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 216.0 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 100.4 KiB | src/lib/api.ts |
| 98.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
| 90.6 KiB | ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js |
| 88.4 KiB | src/products.tsx |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479
✅ Toolbar bundle — eager 2.16 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.16 MiB · 19 files | 🔺 +88 B (+0.0%) | ████░░░░░░ 37.7% of 5.72 MiB |
| Deferred (lazy) | 2.10 MiB · 44 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.2 KiB | no change | █░░░░░░░░░ 6.0% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 800.3 KiB | dist/toolbar/toolbar-app-BB53FERC.css |
| 651.5 KiB | dist/toolbar/chunk-chunk-I5EINXHJ.js |
| 259.4 KiB | dist/toolbar/chunk-chunk-FDSGHPDO.js |
| 138.3 KiB | dist/toolbar/chunk-chunk-52D5E4WE.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-FDH2IBXT.js |
| 75.2 KiB | dist/toolbar/toolbar-app-37VQQU5C.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-TSAL54PB.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-UCXWWLV3.js |
| 21.0 KiB | dist/toolbar/chunk-chunk-IH7RHM2D.js |
| 6.8 KiB | dist/toolbar/chunk-chunk-DV7IWQNF.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +136.1 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 945.99 MiB · 🔺 +136.1 KiB (+0.0%)
|
[Critical risk] Routes externally billed organizations to their provider's invoice pages. The PR appears safe to merge based on the changes reviewed. Reviews (2) · Last reviewed commit: "fix(billing): hide Stripe payment links ..." |
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: PostHog/posthog/.coderabbit.yaml Review profile: QUIET Plan: Enterprise Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughBilling logic now stores open-invoice data and derives invoice warnings from billing state. Invoice destinations depend on the billing provider and available URLs. Billing UI links use the selected destination and are omitted when no URL is available. Credit-purchase confirmations also use billing-provider-specific destinations. Tests cover invoice loading order, open-invoice warnings, unsubscribe errors, and portal-button destinations. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to Externally billed organizations use their provider’s invoice page when configured, while direct billing retains its existing invoice destinations. No actionable merge-blocking risk is evident in the supplied implementation and test coverage. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Provider-billed organizations are less likely to be sent to Stripe to pay twice. The remaining risk is that an organization with open invoices but no provider invoices page loses the warning associated with adding subscription items. Whether the billing service independently enforces that restriction is not established. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (2)
frontend/src/scenes/billing/CreditCTAHero.tsx-139-139 (1)
139-139: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the selected invoice URL for both rendering and navigation.
If a pending credit overview has
invoice_url: nullwhilebilling.external_billing_provider_invoices_urlis set, the current guard and click handler both block the external destination. Derive the fallback URL once, then use it for the button condition andwindow.open.Suggested fix
+ const invoiceUrl = billing?.external_billing_provider_invoices_url || creditOverview.invoice_url + return ( ... - {creditOverview.status === 'pending' && creditOverview.invoice_url && ( + {creditOverview.status === 'pending' && invoiceUrl && ( <LemonButton type="primary" - onClick={() => - creditOverview.invoice_url && - window.open( - billing?.external_billing_provider_invoices_url || creditOverview.invoice_url, - '_blank' - ) - } + onClick={() => window.open(invoiceUrl, '_blank')}frontend/src/scenes/billing/billingLogic.tsx-1002-1003 (1)
1002-1003: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winPreserve the open-invoice warning when billing loading fails.
When open invoices load before billing,
loadInvoicesawaitsasyncActions.loadBilling()inside the sametryblock as the invoice request. A billing request failure rejects this async action, reaches the outercatch, and returnsnull. This removes the warning even though the invoice response already contains open invoices.Handle billing-load failure separately so the warning can still use the hosted-invoice link.
🐛 Suggested fix
if (!values.billing) { - await asyncActions.loadBilling() + try { + await asyncActions.loadBilling() + } catch (error) { + console.error(error) + } }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
Review profile: QUIET
Plan: Enterprise
Run ID: 622be79a-a54d-4feb-ab6e-460323243502
📒 Files selected for processing (4)
frontend/src/scenes/billing/CreditCTAHero.tsxfrontend/src/scenes/billing/UnsubscribeSurveyModal.tsxfrontend/src/scenes/billing/billingLogic.test.tsfrontend/src/scenes/billing/billingLogic.tsx
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.
|
👋 Visual changes detected for this PR. Review and approve in PostHog Visual Review If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix. Install the Visual Review Chrome extension to see visual review results at the top of your pull requests. |
Apply the rule to any org billed through an external provider, using billing_provider when the billing API sends it and the provider invoices URL otherwise. An externally billed org with no invoices page gets no Stripe link, no open-invoice banner and no portal button. Build the banner in a selector over the open invoices and billing, so the page no longer requests billing a second time when invoices load first, and label provider links "View invoices". Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
🦔 Hogbox preview · ✅ ready▶ Open the preview
commit |
The pending credit button needed a Stripe hosted invoice even when it opens the provider's invoices page, so it could stay hidden. The credit dialog told externally billed orgs that their card would be charged and linked "card on file" to the provider's invoices. It now says the provider collects the payment. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.
🟡 Other comments (1)
frontend/src/scenes/billing/StripePortalButton.tsx-8-17 (1)
8-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winScope the
customer_idguard to direct PostHog billing.The billing API permits a missing or null
customer_idand addsexternal_billing_provider_invoices_urlindependently for Vercel integrations.StripePortalButtonreturns before it selects that URL, so externally billed organizations can lose their invoice link. Keep the guard for direct PostHog billing.Suggested fix
- if (!billing?.customer_id) { + if (!billing || (!isExternallyBilled && !billing.customer_id)) { return null }
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: PostHog/posthog/.coderabbit.yaml
Review profile: QUIET
Plan: Enterprise
Run ID: 8d1e273d-f6d3-4a57-a8c7-6c29f130d53f
📒 Files selected for processing (7)
frontend/src/scenes/billing/CreditCTAHero.tsxfrontend/src/scenes/billing/StripePortalButton.test.tsxfrontend/src/scenes/billing/StripePortalButton.tsxfrontend/src/scenes/billing/UnsubscribeSurveyModal.tsxfrontend/src/scenes/billing/billingLogic.test.tsfrontend/src/scenes/billing/billingLogic.tsxfrontend/src/types.ts
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review.
The portal button returned nothing when billing had no customer id, before it looked at the provider invoices URL. Keep that check for direct billing only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
Changes
billing.billing_provideris anything other thanposthog, or when a provider invoices URL is set. The billing API starts sendingbilling_providerin a companion change. Until then, the URL check covers Vercel orgs./api/billingrequest instead of two.How did you test this code?
billingLogic.test.tstables cover direct billing, a provider with an invoices page, and a provider without one, for the banner and the unsubscribe links./api/billingopen until invoices load, then asserts a single request. The previous version of this PR made two.StripePortalButton.test.tsxcovers the portal link, the provider link, and rendering nothing.Release status
Automatic notifications
Docs update
None. The billing page behavior for provider-billed orgs has no docs page. Labeled
skip-inkeep-docs.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5), including the subagents that wrote and reviewed the change.external billing provider invoices,stripe_portal_url vercel billing) found no other open PR./writing-pr-descriptions(repo), plus the author's own PR-readiness command./api/billingrequest when invoices loaded first.🤖 Generated with Claude Code