[backport cloud/1.46] refactor(billing): unify cancel-status polling into billingOperationStore (B8 / FE-970)#13008
Conversation
…tore (B8 / FE-970) (#12788) ## Motivation - **Why B8 exists**: two divergent BillingOp pollers poll the same op-status endpoint with different policies (hand-rolled 2× / 5s cap / 30 attempts vs the store's 1.5× / 8s / 120s). Once B1 (FE-966) routes personal flows through the facade, a single cancel op would be polled by **both** — duplicate requests, with two timeout policies racing on the same state. - **The latent bug — silent failure on a money path**: the bespoke poller treated timeout as a silent return, so `cancelSubscription` resolved and the dialog showed "cancelled successfully" while the backend op could still be pending or fail later. The root cause is structural: the poll outcome was fire-and-forget — no caller consumed it, so there was no channel through which a failure could surface. - **The fix — outcome as an awaited contract**: `startOperation` returns the terminal outcome as a `Promise<BillingOperation>`; `cancelSubscription` awaits it and throws on any non-`succeeded` terminal. Every outcome must now flow through the caller, making silence structurally impossible (timeout/failure → error toast). - **The trade-off this creates**: "the terminal promise always settles" becomes load-bearing — the dialog's loading state hangs on it, and a never-settling path would be worse than the old silence (a permanently locked dialog). The terminal-promise hardening below, and its regression tests, enforce that guarantee. ## Summary Two divergent BillingOp pollers (hand-rolled `useWorkspaceBilling.pollCancelStatus` vs `billingOperationStore`) are unified into one — cancel-status polling now runs through `billingOperationStore`; `pollCancelStatus` and its bespoke backoff/timers/state are removed. ## Changes - **What**: `billingOperationStore` gains `'cancel'` in `OperationType`; `startOperation` now returns `Promise<BillingOperation>` resolving on the terminal outcome (existing subscription / topup callers unaffected — fire-and-forget preserved). `useWorkspaceBilling.cancelSubscription` awaits the shared poller and throws on any non-`succeeded` terminal. One backoff config = store's 1.5× / 8s / 120s. - **Terminal-promise hardening**: the terminal promise always settles — a rejected post-success status/balance refresh no longer leaves `cancelSubscription` hanging with the dialog locked open (`Promise.allSettled`), and a duplicate `startOperation` for an in-flight op joins the same terminal promise instead of resolving instantly with a `pending` snapshot (which the cancel path would read as failure). - **Breaking**: none — the `cancelSubscription(): Promise<void>` contract is unchanged for `CancelSubscriptionDialogContent` / `useBillingContext`. ## Review Focus - **Intentional behavior change**: a cancel **timeout** is now a terminal outcome that **throws** (`billingOperation.cancelTimeout`), so the dialog surfaces an error toast — instead of the old silent success-ish return (which could show success while the op was still pending). Success / failure semantics otherwise preserved (success → status refresh + `isSubscribed: false`; failure → throw with message). - FE-only refactor (B8); cleanest after FE-904 but independent of it. Relates FE-932 (shared status-refresh path). ## Tests 90 unit tests green across the four affected suites (fake timers for all polling): - **`billingOperationStore.test.ts` (33)** — cancel terminal outcomes (succeeded / failed / timeout) resolve the awaited promise with the right status + i18n message; cancel suppresses the store's toasts and settings-dialog side effects; success refreshes status/balance and sets `isSubscribed: false`; backoff progression, 8s cap, 2-min timeout; transient poll errors keep polling. Regression guards for the hardening: post-success refresh failure still settles the terminal promise (reproduced as a hang before the fix), and a duplicate `startOperation` joins the in-flight terminal promise instead of resolving with a `pending` snapshot. - **`useWorkspaceBilling.test.ts`** — `cancelSubscription` drives the shared poller; throws the op error on `failed`, throws on `timeout`, falls back to a generic message when `errorMessage` is absent; a failing cancel API propagates without starting the poller. - **`CancelSubscriptionDialogContent.test.ts` (8)** — locks the dialog half of the behavior change: a rejected `cancelSubscription` shows an error toast and keeps the dialog open; success closes the dialog with a success toast. - **`useSubscriptionCheckout.test.ts`** — unchanged, confirms fire-and-forget callers are unaffected. Both hardening regressions were proven red→green locally: before the fix the terminal-hang test timed out at 5s and the duplicate-start test resolved `pending`. Gates: vue-tsc / oxlint type-aware / eslint / oxfmt clean; full CI green including all Playwright shards and the cloud project. The existing `cancelSubscriptionDialog.spec.ts` e2e (@ui, open/close/escape flows) is unchanged and green; cloud-backend e2e for billing flows is tracked separately in FE-991. Fixes FE-970
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## cloud/1.46 #13008 +/- ##
=============================================
Coverage ? 61.86%
=============================================
Files ? 1455
Lines ? 75268
Branches ? 21241
=============================================
Hits ? 46568
Misses ? 28352
Partials ? 348
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-06-19T13:41:47.392Z",
"gitSha": "8a97bd29809ef524b600af0ab7912b09d5985ae6",
"branch": "backport-12788-to-cloud-1.46",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 1990.8399999999915,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.204,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 398.7579999999999,
"heapDeltaBytes": -1857576,
"heapUsedBytes": 56503056,
"domNodes": 22,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 22.467000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2019.7840000000156,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.252999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 434.004,
"heapDeltaBytes": -2272076,
"heapUsedBytes": 56276964,
"domNodes": 20,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 25.46,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1898.4020000000044,
"styleRecalcs": 74,
"styleRecalcDurationMs": 43.059,
"layouts": 12,
"layoutDurationMs": 3.9880000000000004,
"taskDurationMs": 876.4490000000001,
"heapDeltaBytes": -13411600,
"heapUsedBytes": 55044032,
"domNodes": -239,
"jsHeapTotalBytes": 21426176,
"scriptDurationMs": 122.443,
"eventListeners": -192,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1854.6630000000164,
"styleRecalcs": 72,
"styleRecalcDurationMs": 39.381,
"layouts": 12,
"layoutDurationMs": 3.83,
"taskDurationMs": 813.485,
"heapDeltaBytes": 15299996,
"heapUsedBytes": 65964160,
"domNodes": 54,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 119.95100000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1726.7180000000053,
"styleRecalcs": 30,
"styleRecalcDurationMs": 16.223000000000003,
"layouts": 6,
"layoutDurationMs": 0.548,
"taskDurationMs": 363.513,
"heapDeltaBytes": 1926100,
"heapUsedBytes": 60208064,
"domNodes": 77,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 29.304999999999996,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1732.0480000000202,
"styleRecalcs": 30,
"styleRecalcDurationMs": 18.174999999999997,
"layouts": 6,
"layoutDurationMs": 0.717,
"taskDurationMs": 356.61699999999996,
"heapDeltaBytes": 1482212,
"heapUsedBytes": 60376532,
"domNodes": 76,
"jsHeapTotalBytes": 26738688,
"scriptDurationMs": 23.764,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "dom-widget-clipping",
"durationMs": 662.6009999999951,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.937000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 371.637,
"heapDeltaBytes": 5396200,
"heapUsedBytes": 56010936,
"domNodes": 18,
"jsHeapTotalBytes": 10223616,
"scriptDurationMs": 57.001999999999995,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 603.1860000000506,
"styleRecalcs": 12,
"styleRecalcDurationMs": 9.715000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 387.22,
"heapDeltaBytes": 7073908,
"heapUsedBytes": 65693056,
"domNodes": 20,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 58.91100000000001,
"eventListeners": 0,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666682,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2015.9699999999816,
"styleRecalcs": 12,
"styleRecalcDurationMs": 9.299999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 539.649,
"heapDeltaBytes": -8843312,
"heapUsedBytes": 60684616,
"domNodes": 24,
"jsHeapTotalBytes": 11939840,
"scriptDurationMs": 100.44500000000001,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2034.592000000032,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.172,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 607.269,
"heapDeltaBytes": -9090444,
"heapUsedBytes": 60859576,
"domNodes": 20,
"jsHeapTotalBytes": 11939840,
"scriptDurationMs": 113.93100000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-pan",
"durationMs": 2153.618999999992,
"styleRecalcs": 70,
"styleRecalcDurationMs": 21.014000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1255.15,
"heapDeltaBytes": 10861956,
"heapUsedBytes": 83243012,
"domNodes": 18,
"jsHeapTotalBytes": 12640256,
"scriptDurationMs": 426.2049999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2128.7020000000325,
"styleRecalcs": 70,
"styleRecalcDurationMs": 20.716,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1162.639,
"heapDeltaBytes": 11906200,
"heapUsedBytes": 84687224,
"domNodes": 18,
"jsHeapTotalBytes": 11067392,
"scriptDurationMs": 421.44599999999997,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3188.5389999999916,
"styleRecalcs": 66,
"styleRecalcDurationMs": 20.515999999999995,
"layouts": 60,
"layoutDurationMs": 8.238999999999999,
"taskDurationMs": 1349.0549999999998,
"heapDeltaBytes": 13947620,
"heapUsedBytes": 68850780,
"domNodes": 14,
"jsHeapTotalBytes": 5767168,
"scriptDurationMs": 489.31,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3601.54,
"styleRecalcs": 65,
"styleRecalcDurationMs": 22.905999999999995,
"layouts": 60,
"layoutDurationMs": 9.999999999999998,
"taskDurationMs": 1586.805,
"heapDeltaBytes": 19898388,
"heapUsedBytes": 74554372,
"domNodes": 12,
"jsHeapTotalBytes": 6553600,
"scriptDurationMs": 576.784,
"eventListeners": 10,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2015.6370000000265,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.58,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 544.1639999999999,
"heapDeltaBytes": -9549780,
"heapUsedBytes": 63627748,
"domNodes": 20,
"jsHeapTotalBytes": 8531968,
"scriptDurationMs": 100.98000000000002,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2025.0959999999623,
"styleRecalcs": 9,
"styleRecalcDurationMs": 10.219,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 644.8799999999999,
"heapDeltaBytes": 13663876,
"heapUsedBytes": 80574972,
"domNodes": 18,
"jsHeapTotalBytes": 15523840,
"scriptDurationMs": 113.102,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 584.1400000000476,
"styleRecalcs": 49,
"styleRecalcDurationMs": 12.689,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 384.66499999999996,
"heapDeltaBytes": 6554864,
"heapUsedBytes": 60899460,
"domNodes": 24,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 123.646,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 616.1490000000072,
"styleRecalcs": 49,
"styleRecalcDurationMs": 13.000000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 415.018,
"heapDeltaBytes": 8065556,
"heapUsedBytes": 66782936,
"domNodes": 24,
"jsHeapTotalBytes": 19398656,
"scriptDurationMs": 134.42700000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666682,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-idle",
"durationMs": 2021.6119999999762,
"styleRecalcs": 10,
"styleRecalcDurationMs": 7.430000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 406.25899999999996,
"heapDeltaBytes": -2293860,
"heapUsedBytes": 56482692,
"domNodes": 20,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 18.442,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2000.5489999999782,
"styleRecalcs": 11,
"styleRecalcDurationMs": 9.271999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 461.008,
"heapDeltaBytes": -2475128,
"heapUsedBytes": 56337292,
"domNodes": 22,
"jsHeapTotalBytes": 25952256,
"scriptDurationMs": 23.497,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1702.9890000000023,
"styleRecalcs": 76,
"styleRecalcDurationMs": 38.690000000000005,
"layouts": 16,
"layoutDurationMs": 4.372,
"taskDurationMs": 689.3679999999999,
"heapDeltaBytes": 12586216,
"heapUsedBytes": 66826076,
"domNodes": 62,
"jsHeapTotalBytes": 25165824,
"scriptDurationMs": 90.942,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1739.4750000000272,
"styleRecalcs": 76,
"styleRecalcDurationMs": 43.485,
"layouts": 16,
"layoutDurationMs": 4.727,
"taskDurationMs": 834.043,
"heapDeltaBytes": 15516140,
"heapUsedBytes": 74099728,
"domNodes": 65,
"jsHeapTotalBytes": 25690112,
"scriptDurationMs": 103.409,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-transition-enter",
"durationMs": 948.8089999999829,
"styleRecalcs": 17,
"styleRecalcDurationMs": 25.761,
"layouts": 5,
"layoutDurationMs": 11.730999999999998,
"taskDurationMs": 732.34,
"heapDeltaBytes": 4440716,
"heapUsedBytes": 79647820,
"domNodes": 13833,
"jsHeapTotalBytes": 17301504,
"scriptDurationMs": 28.772999999999993,
"eventListeners": 2527,
"totalBlockingTimeMs": 139,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "viewport-pan-sweep",
"durationMs": 8188.413000000026,
"styleRecalcs": 252,
"styleRecalcDurationMs": 58.69500000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4023.695,
"heapDeltaBytes": -746212,
"heapUsedBytes": 69235916,
"domNodes": 22,
"jsHeapTotalBytes": 19193856,
"scriptDurationMs": 1373.1660000000002,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8148.278000000005,
"styleRecalcs": 250,
"styleRecalcDurationMs": 58.364999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3885.592,
"heapDeltaBytes": -3647048,
"heapUsedBytes": 68184244,
"domNodes": 18,
"jsHeapTotalBytes": 16048128,
"scriptDurationMs": 1282.307,
"eventListeners": 20,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 11180.116999999995,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11165.722,
"heapDeltaBytes": -35749172,
"heapUsedBytes": 169673324,
"domNodes": -3306,
"jsHeapTotalBytes": 21467136,
"scriptDurationMs": 639.0039999999999,
"eventListeners": -16471,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 11858.541999999943,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 11841.465,
"heapDeltaBytes": -37525296,
"heapUsedBytes": 170944188,
"domNodes": -3308,
"jsHeapTotalBytes": 23302144,
"scriptDurationMs": 593.4229999999999,
"eventListeners": -16472,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333237,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 13922.890999999992,
"styleRecalcs": 66,
"styleRecalcDurationMs": 20.598000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13877.077,
"heapDeltaBytes": -21650360,
"heapUsedBytes": 168972676,
"domNodes": -3308,
"jsHeapTotalBytes": 17010688,
"scriptDurationMs": 911.248,
"eventListeners": -16472,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.219999999999953,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 14336.181000000011,
"styleRecalcs": 67,
"styleRecalcDurationMs": 20.51900000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 14313.839,
"heapDeltaBytes": -26112468,
"heapUsedBytes": 187005148,
"domNodes": -3308,
"jsHeapTotalBytes": 19808256,
"scriptDurationMs": 860.81,
"eventListeners": -16468,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.80000000000291
},
{
"name": "workflow-execution",
"durationMs": 477.8140000000235,
"styleRecalcs": 19,
"styleRecalcDurationMs": 29.515000000000004,
"layouts": 6,
"layoutDurationMs": 2.18,
"taskDurationMs": 149.605,
"heapDeltaBytes": 5433636,
"heapUsedBytes": 65134736,
"domNodes": 170,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 21.215,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 510.38600000003953,
"styleRecalcs": 15,
"styleRecalcDurationMs": 36.413000000000004,
"layouts": 5,
"layoutDurationMs": 1.8219999999999998,
"taskDurationMs": 176.01600000000002,
"heapDeltaBytes": 5227080,
"heapUsedBytes": 64955500,
"domNodes": 153,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 35.205,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66666666666665,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
🎭 Playwright: ✅ 1671 passed, 0 failed · 1 flaky📊 Browser Reports
|
Backport of #12788 to
cloud/1.46Automatically created by backport workflow.