feat(assets): classify jobs cursor rejection via structured errorCode#13781
Conversation
Parse the machine-readable code from the /jobs JSON error body onto JobsApiError.errorCode and narrow the store's cursor-recovery trigger to errorCode === 'INVALID_CURSOR' instead of any 400. Defense-in-depth for when the endpoint gains other 400 cases; a non-cursor 400 or a non-JSON proxy error no longer resets the history walk to offset 0.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
🎭 Playwright: ✅ 1691 passed, 0 failed · 1 flaky📊 Browser Reports
🎨 Storybook: ✅ Built — View Storybook📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-07-18T03:55:04.440Z",
"gitSha": "7f74d57aae5b1e4d168db9eb258681b8b053a097",
"branch": "matt/be-3422-invalid-cursor-errorcode",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2053.0270000000146,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.650000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 320.721,
"heapDeltaBytes": 13198912,
"heapUsedBytes": 65380984,
"domNodes": -267,
"jsHeapTotalBytes": 16117760,
"scriptDurationMs": 13.222999999999999,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2008.0490000000282,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.777000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 335.552,
"heapDeltaBytes": -1008260,
"heapUsedBytes": 51644064,
"domNodes": -289,
"jsHeapTotalBytes": 16642048,
"scriptDurationMs": 12.129000000000001,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1706.8780000000174,
"styleRecalcs": 72,
"styleRecalcDurationMs": 25.193,
"layouts": 12,
"layoutDurationMs": 2.313,
"taskDurationMs": 606.383,
"heapDeltaBytes": -12757496,
"heapUsedBytes": 56615216,
"domNodes": -240,
"jsHeapTotalBytes": 20344832,
"scriptDurationMs": 79.458,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1720.5670000000168,
"styleRecalcs": 75,
"styleRecalcDurationMs": 24.539,
"layouts": 12,
"layoutDurationMs": 2.1060000000000003,
"taskDurationMs": 564.8879999999999,
"heapDeltaBytes": -1737080,
"heapUsedBytes": 50454316,
"domNodes": -259,
"jsHeapTotalBytes": 16904192,
"scriptDurationMs": 77.016,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1709.0319999999792,
"styleRecalcs": 31,
"styleRecalcDurationMs": 11.883999999999999,
"layouts": 6,
"layoutDurationMs": 0.436,
"taskDurationMs": 284.112,
"heapDeltaBytes": -3725748,
"heapUsedBytes": 65704900,
"domNodes": -222,
"jsHeapTotalBytes": 13529088,
"scriptDurationMs": 13.958000000000002,
"eventListeners": -182,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1744.0879999999765,
"styleRecalcs": 31,
"styleRecalcDurationMs": 12.575000000000003,
"layouts": 6,
"layoutDurationMs": 0.51,
"taskDurationMs": 280.04400000000004,
"heapDeltaBytes": -3550560,
"heapUsedBytes": 65856952,
"domNodes": -222,
"jsHeapTotalBytes": 13266944,
"scriptDurationMs": 13.918999999999999,
"eventListeners": -182,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "dom-widget-clipping",
"durationMs": 500.583000000006,
"styleRecalcs": 12,
"styleRecalcDurationMs": 5.639,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 298.016,
"heapDeltaBytes": 5616112,
"heapUsedBytes": 57829804,
"domNodes": -289,
"jsHeapTotalBytes": 10350592,
"scriptDurationMs": 38.57,
"eventListeners": -203,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "dom-widget-clipping",
"durationMs": 413.68599999998423,
"styleRecalcs": 12,
"styleRecalcDurationMs": 5.433999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 270.12199999999996,
"heapDeltaBytes": 8931964,
"heapUsedBytes": 61188216,
"domNodes": -291,
"jsHeapTotalBytes": 7204864,
"scriptDurationMs": 37.414,
"eventListeners": -201,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2028.0400000000327,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.232000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 438.0729999999999,
"heapDeltaBytes": -24360820,
"heapUsedBytes": 59578232,
"domNodes": -259,
"jsHeapTotalBytes": 2707456,
"scriptDurationMs": 59.643,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-idle",
"durationMs": 2026.7839999999637,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.693999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 421.934,
"heapDeltaBytes": -25155636,
"heapUsedBytes": 59138328,
"domNodes": -261,
"jsHeapTotalBytes": 2183168,
"scriptDurationMs": 56.188999999999986,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2049.88400000002,
"styleRecalcs": 68,
"styleRecalcDurationMs": 11.573000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 773.182,
"heapDeltaBytes": 12847436,
"heapUsedBytes": 80824712,
"domNodes": -271,
"jsHeapTotalBytes": -1105920,
"scriptDurationMs": 262.434,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-pan",
"durationMs": 2046.7800000000125,
"styleRecalcs": 68,
"styleRecalcDurationMs": 10.555,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 729.165,
"heapDeltaBytes": 27236416,
"heapUsedBytes": 80592068,
"domNodes": -273,
"jsHeapTotalBytes": 466944,
"scriptDurationMs": 258.355,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3037.360999999976,
"styleRecalcs": 65,
"styleRecalcDurationMs": 13.541,
"layouts": 60,
"layoutDurationMs": 6.082,
"taskDurationMs": 912.4590000000001,
"heapDeltaBytes": 11744756,
"heapUsedBytes": 66588024,
"domNodes": 12,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 316.394,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-zoom",
"durationMs": 3041.058000000021,
"styleRecalcs": 65,
"styleRecalcDurationMs": 11.988999999999999,
"layouts": 60,
"layoutDurationMs": 5.091,
"taskDurationMs": 862.833,
"heapDeltaBytes": 11731416,
"heapUsedBytes": 66046576,
"domNodes": 14,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 295.39200000000005,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2043.7539999999785,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.099000000000002,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 409.95300000000003,
"heapDeltaBytes": -2312996,
"heapUsedBytes": 59144028,
"domNodes": -271,
"jsHeapTotalBytes": -557056,
"scriptDurationMs": 55.62200000000001,
"eventListeners": -199,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "minimap-idle",
"durationMs": 1991.9840000000022,
"styleRecalcs": 7,
"styleRecalcDurationMs": 3.6720000000000015,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 371.48099999999994,
"heapDeltaBytes": 7100644,
"heapUsedBytes": 61708032,
"domNodes": 14,
"jsHeapTotalBytes": 0,
"scriptDurationMs": 53.385000000000005,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 438.4110000000305,
"styleRecalcs": 49,
"styleRecalcDurationMs": 7.952000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 261.718,
"heapDeltaBytes": 8202984,
"heapUsedBytes": 67025820,
"domNodes": 24,
"jsHeapTotalBytes": 18350080,
"scriptDurationMs": 82.228,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999727
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 441.55600000004824,
"styleRecalcs": 47,
"styleRecalcDurationMs": 7.414000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 293.60600000000005,
"heapDeltaBytes": -2757724,
"heapUsedBytes": 49132596,
"domNodes": -271,
"jsHeapTotalBytes": 1175552,
"scriptDurationMs": 81.16,
"eventListeners": -193,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.663333333333338,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "subgraph-idle",
"durationMs": 2033.2109999999943,
"styleRecalcs": 10,
"styleRecalcDurationMs": 5.8,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 330.214,
"heapDeltaBytes": -8112664,
"heapUsedBytes": 61235668,
"domNodes": -266,
"jsHeapTotalBytes": 19820544,
"scriptDurationMs": 11.671,
"eventListeners": -195,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-idle",
"durationMs": 2003.3900000000244,
"styleRecalcs": 11,
"styleRecalcDurationMs": 6.103999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 290.31600000000003,
"heapDeltaBytes": -1825576,
"heapUsedBytes": 56997820,
"domNodes": 22,
"jsHeapTotalBytes": 24903680,
"scriptDurationMs": 11.437000000000001,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1706.4279999999599,
"styleRecalcs": 77,
"styleRecalcDurationMs": 27.051000000000002,
"layouts": 16,
"layoutDurationMs": 2.702,
"taskDurationMs": 522.2130000000001,
"heapDeltaBytes": -17139864,
"heapUsedBytes": 52312216,
"domNodes": -231,
"jsHeapTotalBytes": 19820544,
"scriptDurationMs": 57.826,
"eventListeners": -197,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1691.1670000000072,
"styleRecalcs": 76,
"styleRecalcDurationMs": 23.009,
"layouts": 16,
"layoutDurationMs": 2.3649999999999998,
"taskDurationMs": 481.774,
"heapDeltaBytes": -10507240,
"heapUsedBytes": 48357108,
"domNodes": 64,
"jsHeapTotalBytes": 25427968,
"scriptDurationMs": 57.145999999999994,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-transition-enter",
"durationMs": 848.479999999995,
"styleRecalcs": 16,
"styleRecalcDurationMs": 21.379,
"layouts": 6,
"layoutDurationMs": 8.581999999999999,
"taskDurationMs": 575.8149999999999,
"heapDeltaBytes": 4396480,
"heapUsedBytes": 80720348,
"domNodes": 13833,
"jsHeapTotalBytes": 16515072,
"scriptDurationMs": 19.062,
"eventListeners": 2531,
"totalBlockingTimeMs": 122,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "viewport-pan-sweep",
"durationMs": 8135.240000000011,
"styleRecalcs": 251,
"styleRecalcDurationMs": 41.098000000000006,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2659.645,
"heapDeltaBytes": -15829304,
"heapUsedBytes": 68016200,
"domNodes": -268,
"jsHeapTotalBytes": 6320128,
"scriptDurationMs": 804.307,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 8099.950000000035,
"styleRecalcs": 250,
"styleRecalcDurationMs": 33.999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 2743.195,
"heapDeltaBytes": 7286584,
"heapUsedBytes": 74363380,
"domNodes": -260,
"jsHeapTotalBytes": 5447680,
"scriptDurationMs": 879.74,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 7922.122000000001,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 7883.895,
"heapDeltaBytes": -21641372,
"heapUsedBytes": 173893324,
"domNodes": -3331,
"jsHeapTotalBytes": 23826432,
"scriptDurationMs": 356.28900000000004,
"eventListeners": -16378,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-idle",
"durationMs": 7470.566000000019,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 7410.847000000001,
"heapDeltaBytes": -33201608,
"heapUsedBytes": 170179116,
"domNodes": -3291,
"jsHeapTotalBytes": 19398656,
"scriptDurationMs": 346.764,
"eventListeners": -16370,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 9416.074999999979,
"styleRecalcs": 64,
"styleRecalcDurationMs": 17.469999999999985,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 9393.162999999999,
"heapDeltaBytes": -24201276,
"heapUsedBytes": 169028952,
"domNodes": -3291,
"jsHeapTotalBytes": 20971520,
"scriptDurationMs": 667.397,
"eventListeners": -9972,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-pan",
"durationMs": 9068.934000000012,
"styleRecalcs": 64,
"styleRecalcDurationMs": 15.755999999999993,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 9032.37,
"heapDeltaBytes": -21734400,
"heapUsedBytes": 168721096,
"domNodes": -3292,
"jsHeapTotalBytes": 20709376,
"scriptDurationMs": 624.0740000000001,
"eventListeners": -13810,
"totalBlockingTimeMs": 0,
"frameDurationMs": 17.223333333333297,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 429.44599999998445,
"styleRecalcs": 18,
"styleRecalcDurationMs": 18.186,
"layouts": 5,
"layoutDurationMs": 1.2799999999999998,
"taskDurationMs": 136.781,
"heapDeltaBytes": -6737912,
"heapUsedBytes": 46494300,
"domNodes": -276,
"jsHeapTotalBytes": 1175552,
"scriptDurationMs": 13.583,
"eventListeners": -175,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
},
{
"name": "workflow-execution",
"durationMs": 434.2670000000908,
"styleRecalcs": 18,
"styleRecalcDurationMs": 15.457,
"layouts": 4,
"layoutDurationMs": 0.849,
"taskDurationMs": 78.92099999999999,
"heapDeltaBytes": 5181896,
"heapUsedBytes": 65111160,
"domNodes": 159,
"jsHeapTotalBytes": 3145728,
"scriptDurationMs": 12.757000000000001,
"eventListeners": 69,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000273
}
]
} |
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 3 finding(s).
| Severity | Count |
|---|---|
| 🟡 Medium | 1 |
| 🟢 Low | 2 |
Panel: 6/8 reviewers contributed findings.
Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## matt/fe-962-fe-adopt-cursor-pagination-in-the-generated-tab-jobs-sourced #13781 +/- ##
============================================================================================================
- Coverage 76.26% 75.80% -0.47%
============================================================================================================
Files 1687 1637 -50
Lines 116777 110484 -6293
Branches 37481 36525 -956
============================================================================================================
- Hits 89063 83751 -5312
+ Misses 26969 25852 -1117
- Partials 745 881 +136
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 552 files with indirect coverage changes 🚀 New features to boost your workflow:
|
Address Cursor review findings on the errorCode narrowing: - Bound the error body before JSON.parse so an oversized (multi-MB) error payload can't block the UI thread synchronously. - Require status === 400 alongside errorCode === 'INVALID_CURSOR', since a rejected cursor is only ever a 400; the same code on another status is a real server error and must not reset the walk to the offset fallback. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ELI-5
When you scroll the Generated tab, the app pages through your job history with a
"cursor". If that cursor goes stale (e.g. after a server restart) the server
rejects the page with a 400 and the app restarts the walk from the beginning so
you don't get skipped or duplicated rows. Today the app treats any 400 on that
request as "stale cursor". That's fine right now because a rejected cursor is the
only 400 the endpoint can return — but if the endpoint ever starts returning 400
for other reasons, an unrelated error would wrongly nuke your loaded history back
to the top. This teaches the app to read the server's actual machine-readable
error code and only reset when it really is a rejected cursor.
Summary
Classify a rejected jobs pagination cursor by the server's structured error code
instead of the bare HTTP 400 status, so future non-cursor 400s don't trigger the
history offset fallback.
Changes
JobsApiErrornow parses the machine-readablecodefrom the JSON errorbody and exposes it as
errorCode(undefined when the body isn't thestructured error shape, e.g. a proxy error page).
assetsStore.isRejectedCursorErrornarrows fromstatus === 400toerrorCode === 'INVALID_CURSOR'.for a non-JSON body (no
errorCode) and a non-cursor 400 (cursor preserved).Review Focus
/jobslist endpoint returns itserrors as a structured JSON body carrying a machine-readable
codefield, andemits
INVALID_CURSORfor a rejected pagination cursor. That endpoint's only400 today is the rejected cursor, so old (
status === 400) and new(
errorCode === 'INVALID_CURSOR') conditions cannot disagree for currenttraffic — no behavior change now. The narrowing is defense-in-depth for when
the endpoint gains other 400 cases, and is strictly more correct: an unrelated
400 or a non-JSON proxy 400 no longer resets the walk to offset 0.
fetchJobsRaw. I parse it in theJobsApiErrorconstructor instead — the bodyalready flows in there, so the single throw site is unchanged and the parsing
lives with the field it populates. Same observable result.
JobsApiError/isRejectedCursorErroronly exist there; GitHub retargets tomainonce that merges.Verification
vitest runonfetchJobs.test.ts+assetsStore.test.ts: 124 passed.stashed vs applied, against the base branch).