feat: make subscription success backend-driven#11285
feat: make subscription success backend-driven#11285benceruleanlu wants to merge 1 commit intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🎨 Storybook: ✅ Built — View Storybook |
📝 WalkthroughWalkthroughThe changes refactor subscription success tracking from manual polling to event-driven synchronization on window focus. New endpoints and storage-backed mechanisms track "pending subscription success" transactions, preventing duplicate telemetry events via cross-tab awareness. Telemetry APIs now accept optional metadata containing transaction, tier, and ecommerce details. Changes
Sequence DiagramsequenceDiagram
participant Dialog as SubscriptionDialog
participant Composer as useSubscription
participant API as Cloud API
participant Storage as localStorage
participant Telemetry as TelemetryRegistry
Dialog->>Composer: syncStatusAfterCheckout()
Composer->>API: GET /customers/pending-subscription-success
alt 204 No Content
API-->>Composer: (no pending)
else 200 OK + event data
API-->>Composer: { transaction_id, tier, ... }
Composer->>Storage: Check if delivered?
alt Already Delivered
Storage-->>Composer: true
Composer->>API: POST /customers/.../consume
else First Time
Storage-->>Composer: false
Composer->>Telemetry: trackMonthlySubscriptionSucceeded(metadata)
Telemetry-->>Composer: ✓
Composer->>Storage: Mark as delivered
Composer->>API: POST /customers/.../consume
end
API-->>Composer: 200 OK / 404 ignored
end
Composer-->>Dialog: Complete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 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 |
🎭 Playwright: ✅ 1135 passed, 0 failed · 2 flaky📊 Browser Reports
|
📦 Bundle: 5.16 MB gzip 🔴 +657 BDetailsSummary
Category Glance App Entry Points — 22.3 kB (baseline 22.3 kB) • ⚪ 0 BMain entry bundles and manifests
Status: 1 added / 1 removed Graph Workspace — 1.22 MB (baseline 1.22 MB) • ⚪ 0 BGraph editor runtime, canvas, workflow orchestration
Status: 1 added / 1 removed Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 BTop-level views, pages, and routed surfaces
Status: 9 added / 9 removed / 2 unchanged Panels & Settings — 482 kB (baseline 482 kB) • ⚪ 0 BConfiguration panels, inspectors, and settings screens
Status: 10 added / 10 removed / 11 unchanged User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 BAuthentication, profile, and account management bundles
Status: 5 added / 5 removed / 2 unchanged Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 BModals, dialogs, drawers, and in-app editors
Status: 2 added / 2 removed UI Components — 60.3 kB (baseline 60.3 kB) • ⚪ 0 BReusable component library chunks
Status: 5 added / 5 removed / 8 unchanged Data & Services — 3.01 MB (baseline 3 MB) • 🔴 +4.02 kBStores, services, APIs, and repositories
Status: 13 added / 13 removed / 4 unchanged Utilities & Hooks — 344 kB (baseline 344 kB) • ⚪ 0 BHelpers, composables, and utility bundles
Status: 13 added / 13 removed / 14 unchanged Vendor & Third-Party — 9.86 MB (baseline 9.86 MB) • ⚪ 0 BExternal libraries and shared vendor chunks Status: 16 unchanged Other — 8.57 MB (baseline 8.57 MB) • 🟢 -556 BBundles that do not match a named category
Status: 56 added / 56 removed / 79 unchanged ⚡ Performance Report
All metrics
Historical variance (last 15 runs)
Trend (last 15 commits on main)
Raw data{
"timestamp": "2026-04-15T22:50:43.326Z",
"gitSha": "1bc18d26d17d3e71bd61f6c4822ea2fd9340fb9d",
"branch": "bl/subscription-backend-gtm",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2026.6860000000122,
"styleRecalcs": 7,
"styleRecalcDurationMs": 5.295000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 334.63,
"heapDeltaBytes": 19982744,
"heapUsedBytes": 63832600,
"domNodes": 14,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 15.003999999999996,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-idle",
"durationMs": 2013.5700000000156,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.640999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 343.42699999999996,
"heapDeltaBytes": 19924296,
"heapUsedBytes": 63774912,
"domNodes": 16,
"jsHeapTotalBytes": 22544384,
"scriptDurationMs": 15.482,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2021.4330000000018,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.605999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 364.12199999999996,
"heapDeltaBytes": 20663876,
"heapUsedBytes": 65526368,
"domNodes": 17,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 18.109000000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2055.8619999999905,
"styleRecalcs": 84,
"styleRecalcDurationMs": 47.733,
"layouts": 12,
"layoutDurationMs": 3.974,
"taskDurationMs": 1014.037,
"heapDeltaBytes": 16494980,
"heapUsedBytes": 60292452,
"domNodes": 65,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 122.12299999999998,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 2049.3749999999977,
"styleRecalcs": 84,
"styleRecalcDurationMs": 41.794,
"layouts": 12,
"layoutDurationMs": 3.9290000000000003,
"taskDurationMs": 942.5709999999999,
"heapDeltaBytes": 15626416,
"heapUsedBytes": 59487684,
"domNodes": 66,
"jsHeapTotalBytes": 23592960,
"scriptDurationMs": 118.12100000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1891.6909999999234,
"styleRecalcs": 77,
"styleRecalcDurationMs": 45.079,
"layouts": 12,
"layoutDurationMs": 3.8659999999999997,
"taskDurationMs": 826.195,
"heapDeltaBytes": 15950304,
"heapUsedBytes": 59492300,
"domNodes": 61,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 135.70600000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1729.4469999999933,
"styleRecalcs": 31,
"styleRecalcDurationMs": 18.281999999999996,
"layouts": 6,
"layoutDurationMs": 0.7919999999999999,
"taskDurationMs": 298.64399999999995,
"heapDeltaBytes": 24472328,
"heapUsedBytes": 68180708,
"domNodes": 79,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 20.116999999999997,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1714.7320000000263,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.527,
"layouts": 6,
"layoutDurationMs": 0.697,
"taskDurationMs": 292.57599999999996,
"heapDeltaBytes": 24330144,
"heapUsedBytes": 68177228,
"domNodes": 77,
"jsHeapTotalBytes": 21233664,
"scriptDurationMs": 18.299,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1749.9709999999595,
"styleRecalcs": 31,
"styleRecalcDurationMs": 19.271000000000004,
"layouts": 6,
"layoutDurationMs": 0.7719999999999999,
"taskDurationMs": 352.28800000000007,
"heapDeltaBytes": 24332856,
"heapUsedBytes": 68180248,
"domNodes": 76,
"jsHeapTotalBytes": 21495808,
"scriptDurationMs": 25.752,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "dom-widget-clipping",
"durationMs": 566.3189999999929,
"styleRecalcs": 12,
"styleRecalcDurationMs": 8.348,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 326.692,
"heapDeltaBytes": 6732000,
"heapUsedBytes": 50553060,
"domNodes": 19,
"jsHeapTotalBytes": 12320768,
"scriptDurationMs": 54.319,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 581.0570000000439,
"styleRecalcs": 11,
"styleRecalcDurationMs": 10.454,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 333.48299999999995,
"heapDeltaBytes": 6209812,
"heapUsedBytes": 49531896,
"domNodes": 18,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 58.822,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "dom-widget-clipping",
"durationMs": 596.2200000000166,
"styleRecalcs": 11,
"styleRecalcDurationMs": 11.145000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 388.07199999999995,
"heapDeltaBytes": 6798536,
"heapUsedBytes": 50624316,
"domNodes": 18,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 65.884,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "large-graph-idle",
"durationMs": 2048.218999999989,
"styleRecalcs": 10,
"styleRecalcDurationMs": 8.067000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 544.1370000000001,
"heapDeltaBytes": 4394796,
"heapUsedBytes": 56618760,
"domNodes": -258,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 87.87,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2040.9839999999804,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.558,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 608.117,
"heapDeltaBytes": 4780728,
"heapUsedBytes": 56976756,
"domNodes": -259,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 106.91400000000002,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2061.680000000024,
"styleRecalcs": 10,
"styleRecalcDurationMs": 10.386,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 677.756,
"heapDeltaBytes": 3644872,
"heapUsedBytes": 56727548,
"domNodes": -257,
"jsHeapTotalBytes": 16703488,
"scriptDurationMs": 113.27400000000002,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2133.974999999992,
"styleRecalcs": 69,
"styleRecalcDurationMs": 16.244,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1045.317,
"heapDeltaBytes": 16165508,
"heapUsedBytes": 70821632,
"domNodes": -260,
"jsHeapTotalBytes": 18743296,
"scriptDurationMs": 364.702,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-pan",
"durationMs": 2122.645000000034,
"styleRecalcs": 67,
"styleRecalcDurationMs": 15.801999999999996,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1088.158,
"heapDeltaBytes": -7933704,
"heapUsedBytes": 47817544,
"domNodes": -249,
"jsHeapTotalBytes": 20955136,
"scriptDurationMs": 378.876,
"eventListeners": -122,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2156.380000000013,
"styleRecalcs": 68,
"styleRecalcDurationMs": 17.435,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1239.402,
"heapDeltaBytes": 19308440,
"heapUsedBytes": 74074396,
"domNodes": -262,
"jsHeapTotalBytes": 19267584,
"scriptDurationMs": 469.124,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3132.8580000000275,
"styleRecalcs": 65,
"styleRecalcDurationMs": 16.028999999999996,
"layouts": 60,
"layoutDurationMs": 7.953999999999999,
"taskDurationMs": 1314.277,
"heapDeltaBytes": 6146764,
"heapUsedBytes": 62350136,
"domNodes": -266,
"jsHeapTotalBytes": 16965632,
"scriptDurationMs": 481.64099999999996,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "large-graph-zoom",
"durationMs": 3180.4349999999886,
"styleRecalcs": 65,
"styleRecalcDurationMs": 17.273,
"layouts": 60,
"layoutDurationMs": 8.426,
"taskDurationMs": 1329.256,
"heapDeltaBytes": 9006984,
"heapUsedBytes": 64546564,
"domNodes": -265,
"jsHeapTotalBytes": 16179200,
"scriptDurationMs": 488.696,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3147.4600000000237,
"styleRecalcs": 62,
"styleRecalcDurationMs": 15.371000000000002,
"layouts": 60,
"layoutDurationMs": 8.71,
"taskDurationMs": 1433.699,
"heapDeltaBytes": 4267104,
"heapUsedBytes": 62372728,
"domNodes": -269,
"jsHeapTotalBytes": 16154624,
"scriptDurationMs": 524.1589999999999,
"eventListeners": -123,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2033.0079999999953,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.7219999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 531.4559999999999,
"heapDeltaBytes": 2795996,
"heapUsedBytes": 58502676,
"domNodes": -263,
"jsHeapTotalBytes": 15917056,
"scriptDurationMs": 83.253,
"eventListeners": -125,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "minimap-idle",
"durationMs": 2020.2269999999771,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.683000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 517.0379999999999,
"heapDeltaBytes": -1343872,
"heapUsedBytes": 54068300,
"domNodes": -263,
"jsHeapTotalBytes": 15130624,
"scriptDurationMs": 78.776,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2067.3889999999346,
"styleRecalcs": 9,
"styleRecalcDurationMs": 11.369999999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 615.65,
"heapDeltaBytes": 3620216,
"heapUsedBytes": 59787000,
"domNodes": -263,
"jsHeapTotalBytes": 16285696,
"scriptDurationMs": 97.861,
"eventListeners": -127,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 578.5919999999578,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.709999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.974,
"heapDeltaBytes": 6778728,
"heapUsedBytes": 50563476,
"domNodes": 21,
"jsHeapTotalBytes": 13369344,
"scriptDurationMs": 130.162,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 540.0710000000117,
"styleRecalcs": 48,
"styleRecalcDurationMs": 11.533,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 358.21099999999996,
"heapDeltaBytes": 7077956,
"heapUsedBytes": 50892096,
"domNodes": 22,
"jsHeapTotalBytes": 12582912,
"scriptDurationMs": 125.99,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 594.0770000000839,
"styleRecalcs": 47,
"styleRecalcDurationMs": 12.365999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 385.634,
"heapDeltaBytes": 7383328,
"heapUsedBytes": 51080560,
"domNodes": 20,
"jsHeapTotalBytes": 13107200,
"scriptDurationMs": 130.33099999999996,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.669999999999998,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-idle",
"durationMs": 2020.184999999998,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.244999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 328.908,
"heapDeltaBytes": 19751784,
"heapUsedBytes": 63659716,
"domNodes": 18,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 12.889,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 1990.9509999999955,
"styleRecalcs": 8,
"styleRecalcDurationMs": 6.994,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 360.12799999999993,
"heapDeltaBytes": 19690428,
"heapUsedBytes": 63699932,
"domNodes": 16,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 16.115000000000002,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2007.8640000000405,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.910000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 443.95000000000005,
"heapDeltaBytes": 20098524,
"heapUsedBytes": 65193304,
"domNodes": 18,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 20.343999999999994,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1962.9219999999918,
"styleRecalcs": 83,
"styleRecalcDurationMs": 41.565999999999995,
"layouts": 16,
"layoutDurationMs": 4.42,
"taskDurationMs": 859.21,
"heapDeltaBytes": 11662504,
"heapUsedBytes": 55331296,
"domNodes": 71,
"jsHeapTotalBytes": 22806528,
"scriptDurationMs": 92.36999999999999,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1972.2019999999816,
"styleRecalcs": 84,
"styleRecalcDurationMs": 47.116,
"layouts": 16,
"layoutDurationMs": 4.952,
"taskDurationMs": 935.6719999999998,
"heapDeltaBytes": 12234788,
"heapUsedBytes": 55995984,
"domNodes": 72,
"jsHeapTotalBytes": 23068672,
"scriptDurationMs": 106.551,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1986.7949999999155,
"styleRecalcs": 84,
"styleRecalcDurationMs": 49.67699999999999,
"layouts": 16,
"layoutDurationMs": 5.278,
"taskDurationMs": 999.7109999999999,
"heapDeltaBytes": 11777764,
"heapUsedBytes": 55690028,
"domNodes": 67,
"jsHeapTotalBytes": 23330816,
"scriptDurationMs": 108.148,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "viewport-pan-sweep",
"durationMs": 8176.901999999984,
"styleRecalcs": 250,
"styleRecalcDurationMs": 44.125,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3625.835,
"heapDeltaBytes": 26381460,
"heapUsedBytes": 78747020,
"domNodes": -261,
"jsHeapTotalBytes": 21626880,
"scriptDurationMs": 1238.224,
"eventListeners": -109,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8168.3750000000255,
"styleRecalcs": 251,
"styleRecalcDurationMs": 46.95199999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 3598.53,
"heapDeltaBytes": 26417012,
"heapUsedBytes": 78870344,
"domNodes": -258,
"jsHeapTotalBytes": 18481152,
"scriptDurationMs": 1159.6419999999998,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8259.873999999967,
"styleRecalcs": 252,
"styleRecalcDurationMs": 52.321,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 4213.086,
"heapDeltaBytes": 24684336,
"heapUsedBytes": 77812000,
"domNodes": -259,
"jsHeapTotalBytes": 18481152,
"scriptDurationMs": 1308.935,
"eventListeners": -111,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 12430.475000000002,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12414.638,
"heapDeltaBytes": -44636220,
"heapUsedBytes": 167683212,
"domNodes": -9850,
"jsHeapTotalBytes": 25255936,
"scriptDurationMs": 589.289,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 12893.96899999997,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 12879.486,
"heapDeltaBytes": -31895156,
"heapUsedBytes": 181802364,
"domNodes": -9848,
"jsHeapTotalBytes": 22634496,
"scriptDurationMs": 589.1949999999999,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.77333333333336,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 13289.853999999992,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 13257.122000000003,
"heapDeltaBytes": -49424444,
"heapUsedBytes": 166915800,
"domNodes": -9848,
"jsHeapTotalBytes": 21848064,
"scriptDurationMs": 656.9929999999999,
"eventListeners": -23957,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333358,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 15205.727999999965,
"styleRecalcs": 70,
"styleRecalcDurationMs": 14.84399999999997,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15173.141999999998,
"heapDeltaBytes": -36602704,
"heapUsedBytes": 174134148,
"domNodes": -9848,
"jsHeapTotalBytes": -24113152,
"scriptDurationMs": 858.4699999999998,
"eventListeners": -23953,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.780000000000047,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 15354.165999999965,
"styleRecalcs": 75,
"styleRecalcDurationMs": 16.34600000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15322.631999999998,
"heapDeltaBytes": -59917256,
"heapUsedBytes": 162347140,
"domNodes": -9848,
"jsHeapTotalBytes": -10219520,
"scriptDurationMs": 911.9909999999999,
"eventListeners": -23951,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.220000000000073,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 15347.533,
"styleRecalcs": 76,
"styleRecalcDurationMs": 21.165000000000045,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 15317.328,
"heapDeltaBytes": -49138720,
"heapUsedBytes": 162082468,
"domNodes": -9850,
"jsHeapTotalBytes": -14938112,
"scriptDurationMs": 908.592,
"eventListeners": -23985,
"totalBlockingTimeMs": 46,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 462.78999999998405,
"styleRecalcs": 17,
"styleRecalcDurationMs": 24.151000000000003,
"layouts": 5,
"layoutDurationMs": 1.56,
"taskDurationMs": 128.69600000000003,
"heapDeltaBytes": 5361988,
"heapUsedBytes": 50136708,
"domNodes": 167,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 29.674000000000003,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 466.3140000000112,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.762000000000004,
"layouts": 5,
"layoutDurationMs": 1.4020000000000004,
"taskDurationMs": 122.74900000000002,
"heapDeltaBytes": 5118352,
"heapUsedBytes": 50215412,
"domNodes": 156,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 28.098,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 442.71200000002864,
"styleRecalcs": 17,
"styleRecalcDurationMs": 23.959999999999997,
"layouts": 5,
"layoutDurationMs": 1.277,
"taskDurationMs": 117.20999999999998,
"heapDeltaBytes": 4978440,
"heapUsedBytes": 50571536,
"domNodes": 154,
"jsHeapTotalBytes": 524288,
"scriptDurationMs": 23.931999999999995,
"eventListeners": 71,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/platform/cloud/subscription/composables/useSubscription.ts`:
- Around line 226-243: The current implementation in
consumePendingSubscriptionSuccess emits telemetry before performing the POST
consume call, which can let two tabs both pass the local check and double-fire
trackMonthlySubscriptionSucceeded; change the flow to use the consume-as-lock
pattern: perform the POST to
buildApiUrl(`/customers/pending-subscription-success/${id}/consume`) first and
only call trackMonthlySubscriptionSucceeded after verifying the response is a
successful consume (response.ok), treat 404 as “already consumed” and do not
emit telemetry, and throw on other non-OK non-404 statuses; apply the same
change to the other pending-subscription consume helper in this file that
currently tracks before consuming so telemetry is only emitted after a confirmed
successful consume.
- Around line 218-220: Replace raw English Error throws in useSubscription.ts
(the pending-success fetch handlers around the throw at line 218 and the similar
throws around 239-241) with user-facing, localized messages using the
composition API vue-i18n instance (e.g., call
t('main.pendingSubscriptionFetchFailed', { status: response.status }) or
similar) and throw an Error containing that localized string; also add the new
key(s) (e.g., "pendingSubscriptionFetchFailed") to src/locales/en/main.json with
a clear, actionable message that includes a placeholder for the status.
- Around line 395-397: fetchSubscriptionStatus currently awaits
syncPendingSubscriptionSuccess(headers) which, if it throws, causes the whole
fetch to reject even after subscriptionStatus.value was updated; wrap the call
to syncPendingSubscriptionSuccess(headers) in a try/catch inside the same block
so any error is caught and handled (log or telemetry via existing logger/emit
function) and do NOT rethrow, ensuring subscriptionStatus.value remains
considered a successful fetch; target the code around the
options?.syncPendingSuccess && statusData.is_active check and modify only the
call to syncPendingSubscriptionSuccess to swallow/record errors instead of
propagating them.
In `@src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts`:
- Around line 171-176: The method trackMonthlySubscriptionSucceeded currently
pushes { ecommerce: null } to window.dataLayer even when the provider is not
initialized; update this method to only mutate dataLayer after the provider's
established initialization check (use the same flag used elsewhere in this
class, e.g., this.initialized or this.isInitialized) and return early if not
initialized so the dataLayer push is skipped when dispatching is disabled;
ensure the guard wraps the window.dataLayer?.push call inside
trackMonthlySubscriptionSucceeded.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 46a3728e-c872-4aaf-a4c2-63adf5a7db96
📒 Files selected for processing (9)
src/platform/cloud/subscription/components/SubscriptionRequiredDialogContent.vuesrc/platform/cloud/subscription/composables/useSubscription.test.tssrc/platform/cloud/subscription/composables/useSubscription.tssrc/platform/telemetry/TelemetryRegistry.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.tssrc/platform/telemetry/providers/cloud/GtmTelemetryProvider.tssrc/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.tssrc/platform/telemetry/providers/cloud/PostHogTelemetryProvider.tssrc/platform/telemetry/types.ts
| throw new Error( | ||
| `Failed to fetch pending subscription success: ${response.status}` | ||
| ) |
There was a problem hiding this comment.
Use actionable, localized errors for new pending-success API failures
These new thrown Error messages are raw English status strings and may bubble through shared error handling with poor UX context.
💡 Proposed direction
- throw new Error(
- `Failed to fetch pending subscription success: ${response.status}`
- )
+ throw new AuthStoreError(
+ t('toastMessages.failedToSyncSubscriptionSuccess', {
+ error: `HTTP ${response.status}`
+ })
+ )
@@
- throw new Error(
- `Failed to consume pending subscription success: ${response.status}`
- )
+ throw new AuthStoreError(
+ t('toastMessages.failedToSyncSubscriptionSuccess', {
+ error: `HTTP ${response.status}`
+ })
+ )Also add the new translation key in src/locales/en/main.json.
As per coding guidelines, “Provide user-friendly and actionable error messages” and “Use vue-i18n in composition API for all string literals.”
Also applies to: 239-241
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/cloud/subscription/composables/useSubscription.ts` around lines
218 - 220, Replace raw English Error throws in useSubscription.ts (the
pending-success fetch handlers around the throw at line 218 and the similar
throws around 239-241) with user-facing, localized messages using the
composition API vue-i18n instance (e.g., call
t('main.pendingSubscriptionFetchFailed', { status: response.status }) or
similar) and throw an Error containing that localized string; also add the new
key(s) (e.g., "pendingSubscriptionFetchFailed") to src/locales/en/main.json with
a clear, actionable message that includes a placeholder for the status.
| const consumePendingSubscriptionSuccess = async ( | ||
| headers: Record<string, string>, | ||
| id: string | ||
| ): Promise<void> => { | ||
| const response = await fetch( | ||
| buildApiUrl(`/customers/pending-subscription-success/${id}/consume`), | ||
| { | ||
| method: 'POST', | ||
| headers | ||
| } | ||
| ) | ||
|
|
||
| if (!response.ok && response.status !== 404) { | ||
| throw new Error( | ||
| `Failed to consume pending subscription success: ${response.status}` | ||
| ) | ||
| } | ||
| } |
There was a problem hiding this comment.
Cross-tab dedupe is race-prone and can still double-fire telemetry
Current flow tracks before consume. Two tabs can both pass the local delivered check and both emit trackMonthlySubscriptionSucceeded before one consume returns 404.
💡 Proposed fix (consume-as-lock)
- const consumePendingSubscriptionSuccess = async (
+ const consumePendingSubscriptionSuccess = async (
headers: Record<string, string>,
id: string
- ): Promise<void> => {
+ ): Promise<boolean> => {
const response = await fetch(
buildApiUrl(`/customers/pending-subscription-success/${id}/consume`),
{
method: 'POST',
headers
}
)
- if (!response.ok && response.status !== 404) {
+ if (response.status === 404) {
+ return false
+ }
+
+ if (!response.ok) {
throw new Error(
`Failed to consume pending subscription success: ${response.status}`
)
}
+
+ return true
}
@@
- telemetry?.trackMonthlySubscriptionSucceeded({
+ const consumed = await consumePendingSubscriptionSuccess(
+ headers,
+ pendingSuccess.id
+ )
+ if (!consumed) {
+ return
+ }
+
+ telemetry?.trackMonthlySubscriptionSucceeded({
...(authStore.userId ? { user_id: authStore.userId } : {}),
@@
})
markSubscriptionSuccessAsDelivered(pendingSuccess.transaction_id)
- await consumePendingSubscriptionSuccess(headers, pendingSuccess.id)
}Also applies to: 253-287
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/cloud/subscription/composables/useSubscription.ts` around lines
226 - 243, The current implementation in consumePendingSubscriptionSuccess emits
telemetry before performing the POST consume call, which can let two tabs both
pass the local check and double-fire trackMonthlySubscriptionSucceeded; change
the flow to use the consume-as-lock pattern: perform the POST to
buildApiUrl(`/customers/pending-subscription-success/${id}/consume`) first and
only call trackMonthlySubscriptionSucceeded after verifying the response is a
successful consume (response.ok), treat 404 as “already consumed” and do not
emit telemetry, and throw on other non-OK non-404 statuses; apply the same
change to the other pending-subscription consume helper in this file that
currently tracks before consuming so telemetry is only emitted after a confirmed
successful consume.
| if (options?.syncPendingSuccess && statusData.is_active) { | ||
| await syncPendingSubscriptionSuccess(headers) | ||
| } |
There was a problem hiding this comment.
Don’t let pending-success sync failure fail subscription status fetch
If syncPendingSubscriptionSuccess throws, fetchSubscriptionStatus rejects even after subscriptionStatus.value is updated. This can incorrectly surface a “status fetch failed” path for a telemetry-side failure.
💡 Proposed fix
- if (options?.syncPendingSuccess && statusData.is_active) {
- await syncPendingSubscriptionSuccess(headers)
- }
+ if (options?.syncPendingSuccess && statusData.is_active) {
+ try {
+ await syncPendingSubscriptionSuccess(headers)
+ } catch (error) {
+ console.error(
+ '[Subscription] Failed to sync pending subscription success:',
+ error
+ )
+ }
+ }As per coding guidelines, “Implement proper error handling” and “Implement proper error propagation.”
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (options?.syncPendingSuccess && statusData.is_active) { | |
| await syncPendingSubscriptionSuccess(headers) | |
| } | |
| if (options?.syncPendingSuccess && statusData.is_active) { | |
| try { | |
| await syncPendingSubscriptionSuccess(headers) | |
| } catch (error) { | |
| // Log for debugging but don't fail the status fetch | |
| console.error('[Subscription] Pending sync error:', error) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/cloud/subscription/composables/useSubscription.ts` around lines
395 - 397, fetchSubscriptionStatus currently awaits
syncPendingSubscriptionSuccess(headers) which, if it throws, causes the whole
fetch to reject even after subscriptionStatus.value was updated; wrap the call
to syncPendingSubscriptionSuccess(headers) in a try/catch inside the same block
so any error is caught and handled (log or telemetry via existing logger/emit
function) and do NOT rethrow, ensuring subscriptionStatus.value remains
considered a successful fetch; target the code around the
options?.syncPendingSuccess && statusData.is_active check and modify only the
call to syncPendingSubscriptionSuccess to swallow/record errors instead of
propagating them.
| trackMonthlySubscriptionSucceeded( | ||
| metadata?: SubscriptionSuccessMetadata | ||
| ): void { | ||
| if (metadata?.ecommerce) { | ||
| window.dataLayer?.push({ ecommerce: null }) | ||
| } |
There was a problem hiding this comment.
Guard ecommerce: null reset behind provider initialization.
At Line 175, window.dataLayer?.push({ ecommerce: null }) bypasses the provider’s normal initialized gate, so this method can mutate dataLayer even when event dispatch is disabled.
Suggested fix
trackMonthlySubscriptionSucceeded(
metadata?: SubscriptionSuccessMetadata
): void {
+ if (!this.initialized) return
+
if (metadata?.ecommerce) {
window.dataLayer?.push({ ecommerce: null })
}
this.pushEvent(
'subscription_success',
metadata ? { ...metadata } : undefined
)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| trackMonthlySubscriptionSucceeded( | |
| metadata?: SubscriptionSuccessMetadata | |
| ): void { | |
| if (metadata?.ecommerce) { | |
| window.dataLayer?.push({ ecommerce: null }) | |
| } | |
| trackMonthlySubscriptionSucceeded( | |
| metadata?: SubscriptionSuccessMetadata | |
| ): void { | |
| if (!this.initialized) return | |
| if (metadata?.ecommerce) { | |
| window.dataLayer?.push({ ecommerce: null }) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/platform/telemetry/providers/cloud/GtmTelemetryProvider.ts` around lines
171 - 176, The method trackMonthlySubscriptionSucceeded currently pushes {
ecommerce: null } to window.dataLayer even when the provider is not initialized;
update this method to only mutate dataLayer after the provider's established
initialization check (use the same flag used elsewhere in this class, e.g.,
this.initialized or this.isInitialized) and return early if not initialized so
the dataLayer push is skipped when dispatching is disabled; ensure the guard
wraps the window.dataLayer?.push call inside trackMonthlySubscriptionSucceeded.
Codecov Report❌ Patch coverage is @@ Coverage Diff @@
## main #11285 +/- ##
===========================================
- Coverage 63.95% 44.23% -19.72%
===========================================
Files 1459 1350 -109
Lines 83590 69325 -14265
Branches 23083 19237 -3846
===========================================
- Hits 53457 30666 -22791
- Misses 29176 38059 +8883
+ Partials 957 600 -357
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 1010 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Draft Review — Architectural FeedbackVerdict: 🟢 Direction looks good What's looking good
Architectural concerns
Suggested directionThe approach is solid — keep going. The main suggestion is to consider extracting the pending-success delivery logic into its own composable to keep useSubscription's complexity manageable, and tighten the telemetry method signature now that it always receives metadata. Otherwise this is ready to move toward final review. This is an early-stage review focused on direction — detailed line-by-line feedback will come when the PR is marked ready for review. |
Summary
subscription_successthrough GTMdataLayerwith server-sourced ecommerce metadataWhy
The previous flow relied on a short focus-triggered polling loop in the billing dialog, which was brittle for Stripe's multi-tab return path. This change moves event authorship to the backend while keeping GTM delivery in the browser where
dataLayer.push(...)must happen.Validation
pnpm exec vitest run src/platform/cloud/subscription/composables/useSubscription.test.ts src/platform/telemetry/providers/cloud/GtmTelemetryProvider.test.ts src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.test.tspnpm typecheck┆Issue is synchronized with this Notion page by Unito