Skip to content

fix(web): make bulk Loom CSV import resilient to batch failures - #2079

Open
richiemcilroy wants to merge 1 commit into
mainfrom
fix/loom-csv-import-resilience
Open

fix(web): make bulk Loom CSV import resilient to batch failures#2079
richiemcilroy wants to merge 1 commit into
mainfrom
fix/loom-csv-import-resilience

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Problem

The bulk Loom CSV importer sends rows to importFromLoomCsv in batches of 10, but the entire multi-batch loop ran inside a single try/catch. One failed batch call (server error, timeout, network blip, rate-limit rejection) aborted the loop: every remaining batch was never attempted and never appeared in the results table, not even as failed. On large CSVs this silently dropped most rows - the UI reported only the imports from the batches that ran, with no record of the rest anywhere.

Bulk imports also share a per-user rate limit, which large CSVs can exhaust mid-run, making this failure mode common rather than exotic.

Fix

All changes are client-side in ImportLoomPage.tsx; the server action, rate-limit code, and single-video import path are untouched.

  • Per-batch error isolation. Each batch call now has its own try/catch. A failed batch is recorded as failed rows (carrying the error message) and the loop continues with the remaining batches.
  • Every row is always accounted for. Rows that were never attempted (blocked run, unexpected interruption) are appended to the results as "Not attempted." instead of vanishing. The final results always cover the full CSV.
  • Rate-limit backoff. When a batch failure looks like rate limiting (per-row "Too many..." errors, or a thrown rate-limit-shaped error), the client waits 30s and retries once - only the rate-limited rows - before recording the failure and moving on. The server rejects rate-limited rows before creating anything, and dedupes already-imported Loom IDs, so the retry cannot create duplicates.
  • Downloadable results CSV. A "Download Results" button on the results card exports the full per-row outcome (row number, Loom URL, email, space, status, error) as a client-generated blob. The headers match the import template, so failed/not-attempted rows can be filtered and re-uploaded directly to retry.

Regression analysis

What could have regressed, and why it can't:

  • Successful runs. The happy path is unchanged: same batch size (10), same 1.5s cadence, same server calls in the same order, same per-batch progress and results rendering, same toasts and router.refresh(). The not-attempted fill is a no-op when every row has a result (early return before any sort), and the retry block never executes when no row failed with a rate-limit error. The only visible addition on success is the Download Results button.
  • Duplicate imports from retries. Retries resend only rows whose result was a rate-limit failure. Those rows are rejected server-side before any DB write, so nothing was created on the first attempt. Independently, the server's importedVideos dedupe rejects any Loom ID already imported for the org. Successful rows in a partially rate-limited batch are never resent.
  • Server behavior. actions/loom.ts is untouched; server-action tests (loom-import.test.ts, 12 tests) pass unchanged.
  • Other consumers. Every new helper is module-private to ImportLoomPage.tsx, and the component's only consumer is its route page. The single-video import handler, CSV parsing/mapping/preview, and shared @cap/ui components are behaviorally untouched (one layout class, flex-wrap, added to the results-card header so the new button wraps on narrow screens).
  • Blocked runs (permission/plan/limit errors). Previously the loop broke and only a toast showed; the toast logic is unchanged, but the blocked batch's rows now appear as failed and the rest as not attempted - strictly more information, no removed behavior.
  • Worst-case duration. The backoff adds at most one 30s wait + one retry call per batch, only while failures look like rate limiting; the run still terminates and the results table updates live throughout.

Verification

  • pnpm typecheck (next typegen + tsc -b): pass
  • pnpm exec biome check on the changed file: clean
  • vitest run __tests__/unit/loom-import.test.ts: 12/12 pass (server action unchanged)
  • No existing tests cover the client page component itself

Greptile Summary

The PR makes bulk Loom CSV imports continue after individual batch failures, retries rate-limited rows once, accounts for unattempted rows, and adds a downloadable result report.

  • Isolates failures and maintains progress per batch.
  • Retries only rows identified as rate-limited after a fixed backoff.
  • Produces complete result accounting and a client-generated CSV download.

Confidence Score: 4/5

The PR appears safe to merge, with a non-blocking CSV-formula hardening issue in the new results download.

Batch and retry result accounting preserves submitted row numbers and failed outcomes, while the remaining concern is that imported email and space-name values can be emitted as active spreadsheet formulas.

Files Needing Attention: apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx

Security Review

The downloadable results CSV does not neutralize formula-prefixed email or space-name values before spreadsheet consumption.

Important Files Changed

Filename Overview
apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx The batch-failure and retry accounting is consistent with the server action contract, but the new results exporter should neutralize spreadsheet formulas in imported fields.
Prompt To Fix All With AI
### Issue 1
apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx:173-178
**Neutralize formulas in CSV cells**

Formula-prefixed email and space-name values pass the current validation and are written directly to the downloadable CSV. Opening that file in formula-evaluating spreadsheet software interprets those values as formulas rather than literal data, so the exporter should neutralize spreadsheet control prefixes while preserving the intended retry workflow.

**How this was verified:** The imported fields flow through `buildResultsCsv` to `escapeCsvValue`, which escapes CSV delimiters but does not neutralize leading formula characters.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(web): make bulk Loom CSV import resi..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

A failed importFromLoomCsv batch call previously aborted the whole
run: every remaining batch was never attempted and never appeared in
the results table, so large CSVs could silently drop most rows.

- Isolate failures per batch: a failed batch is recorded as failed
  rows (with the error message) and the loop continues.
- Account for every row: rows never attempted are appended to the
  results as "Not attempted." instead of vanishing, including when
  the run stops early on a blocked response or unexpected error.
- Back off once per batch when a failure looks like rate limiting
  (30s wait, single retry of only the rate-limited rows) before
  recording it and moving on. Rate-limited rows are rejected server
  side before any import starts, so retrying them cannot duplicate.
- Add a Download Results button that exports the full per-row
  results as a CSV (re-uploadable headers) so failed rows can be
  filtered and retried safely.
@cursor

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Comment on lines +173 to +178
function escapeCsvValue(value: string) {
if (/[",\n\r]/.test(value)) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 security Neutralize formulas in CSV cells

Formula-prefixed email and space-name values pass the current validation and are written directly to the downloadable CSV. Opening that file in formula-evaluating spreadsheet software interprets those values as formulas rather than literal data, so the exporter should neutralize spreadsheet control prefixes while preserving the intended retry workflow.

How this was verified: The imported fields flow through buildResultsCsv to escapeCsvValue, which escapes CSV delimiters but does not neutralize leading formula characters.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/app/(org)/dashboard/import/loom/ImportLoomPage.tsx
Line: 173-178

Comment:
**Neutralize formulas in CSV cells**

Formula-prefixed email and space-name values pass the current validation and are written directly to the downloadable CSV. Opening that file in formula-evaluating spreadsheet software interprets those values as formulas rather than literal data, so the exporter should neutralize spreadsheet control prefixes while preserving the intended retry workflow.

**How this was verified:** The imported fields flow through `buildResultsCsv` to `escapeCsvValue`, which escapes CSV delimiters but does not neutralize leading formula characters.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant