{"slug": "i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors", "title": "I Used Sentry to Expose a Silent Data-Loss Bug with Zero Errors", "summary": "Developer Abbas Mirza built Aether Canvas during OpenAI Build Week and discovered a silent data-loss bug in its workspace index persistence. The bug allowed two concurrent operations to overwrite each other's index updates, leaving workspace files unreachable without any error. Mirza fixed it by moving the read-modify-write transaction into the write queue and added deterministic before/after test harnesses.", "body_md": "*This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.*\n\nThe front-page lesson:A green “success” proves that an operation finished. It does not prove that its result is still there.\n\n**THE FOUR-MINUTE FIELD REPORT**\n\n**IN THIS EDITION**\n\nThe project ·\nThe incident ·\nThe patch ·\nThe safeguards ·\nThe signal\n\nSECTION I · THE PROJECT DESK\n\nLast month, I built **Aether Canvas** during OpenAI Build Week.\n\nIt is a local-first Electron application where spatially grouping ordinary files creates the mini-app you need. Put a flight confirmation, hotel booking, budget, packing list, and city guide together, for example, and Aether turns the cluster into a living trip workspace with routes, spending, tasks, and places—all traceable to the source files.\n\nThe idea is simple:\n\n**Space is the prompt.**\n\nBut the build was anything but simple. It was a one-week hackathon, and those seven days had to cover validating the idea, designing the architecture, building the interface, connecting the AI workflow, testing the main experience, and preparing the final demo and presentation.\n\nWe shipped a working product. We did not have enough time for deep concurrency and persistence stress testing.\n\nWHAT WE SHIPPEDA real local-first Electron product: spatial files, AI-generated workspaces, autosave, persistence, and a polished demo path. |\nWHAT THE CLOCK HIDThe main experience worked, but the one-week schedule left little room for simultaneous-operation and shutdown stress tests. |\n\nI also chose not to modify the original submitted repository before the winner announcement. I wanted the artifact being judged to remain exactly as submitted. Instead, I created a [separate Bug Smash repository](https://gitlab.com/abbasmir12/aether-canva-bugsmash), with the submission preserved at [commit 3163641a](https://gitlab.com/abbasmir12/aether-canva-bugsmash/-/commit/3163641a4cfbf13f551f4799cde37079ec6a5bb0) and tagged\n\n`openai-hackathon-submission`\n\nThat gave me something unusually valuable for debugging: an untouched before state.\n\nSECTION II · INCIDENT REPORT\n\nAether stores each workspace in its own JSON file and keeps a separate index containing the workspaces visible in the sidebar.\n\nThe original implementation already used atomic temporary-file replacement and a write queue. At first glance, that looked safe. After tracing the workspace IPC calls, autosave path, rename path, and persistence service, I found the boundary was in the wrong place.\n\n✓ WHAT WAS SAFEEach individual JSON replacement was atomic. A partial write would not leave behind a half-written index. |\n✕ WHAT WAS NOT SAFEThe read and modification happened before the queued write, so two complete operations could still overwrite one another. |\n\nThe queue protected an individual JSON write:\n\n``` js\nconst operation = writeQueue.then(async () => {\n  await fs.writeFile(temporaryPath, contents, 'utf8');\n  await fs.rename(temporaryPath, targetPath);\n});\n```\n\nBut updating the workspace index is not one write. It is a complete **read → modify → write** transaction:\n\n```\nCreate A: read index [] ── add A ── write [A]\nCreate B: read index [] ── add B ── write [B]\n                                      ▲\n                     both writes succeed; A disappears from the index\n```\n\nTwo operations could read the same old index before either write entered the queue. Both would make a perfectly valid update. Both writes would complete successfully. The last valid-but-stale snapshot would win.\n\nNo malformed JSON. No rejected promise. No crash for error monitoring to catch.\n\nJust a workspace file that still existed on disk but was no longer reachable from the application.\n\nI did not want to hammer the UI with clicks until I got lucky. I built deterministic before/after harnesses that run the same workloads against two real implementations:\n\n🔴 BEFORELoads the authentic workspace service directly from submitted commit `3163641a` with `git show` —not from a hand-written broken copy.\n|\n🟢 AFTERLoads the repaired service from the Bug Smash branch and subjects it to the exact same operations and assertions. |\n\nThe workload creates **40 workspaces simultaneously**, then runs **20 autosave-versus-rename races**. Each stage uses an isolated temporary Electron profile, opens the real desktop UI with the resulting data, and deletes that data after the window closes. My actual Aether spaces are never touched.\n\n```\nnpm run bugsmash:before -- --sentry\nnpm run bugsmash:after -- --sentry\n```\n\nThe harness also refuses to present an inconclusive result: the before stage must reproduce the legacy failure, and the after stage must preserve every mutation.\n\n| Identical workload | Original submission | Repaired version |\n|---|---|---|\n| Workspace files written | 40 / 40 | 40 / 40 |\n| Workspaces reachable in the index | 1 / 40 |\n40 / 40 |\n| Orphaned workspace files | 39 |\n0 |\n| Autosave/rename trials that lost a mutation | 20 / 20 |\n0 / 20 |\n\nSECTION III · THE PATCH DESK\n\n| CASE FILE | EVIDENCE |\n|---|---|\nMerge request |\n|\n\n`57fc12a`\n\n— transaction-safe persistenceThe heart of the repair is intentionally small. A failure-safe exclusive queue now surrounds the **complete public operation**:\n\n``` js\nconst runExclusive = async <T>(operation: () => Promise<T>): Promise<T> => {\n  pendingOperations += 1;\n  const queued = operationQueue.catch(() => undefined).then(operation);\n  operationQueue = queued.then(() => undefined, () => undefined);\n\n  try {\n    return await queued;\n  } finally {\n    pendingOperations -= 1;\n  }\n};\n```\n\nEvery workspace mutation now crosses that boundary as one unit:\n\n``` js\nreturn {\n  list: () => runExclusive(readIndex),\n  create: (name) => runExclusive(() => create(name)),\n  load: (id) => runExclusive(() => readJson(filePath(id))),\n  save: (workspace) => runExclusive(() => save(workspace)),\n  rename: (id, name) => runExclusive(async () => {\n    const workspace = await readJson(filePath(id));\n    await save({ ...workspace, name: name.trim() || 'Untitled Space' });\n  }),\n};\n```\n\nNow a create or rename finishes reading, modifying, and persisting its state before the next operation begins. A rejected operation is absorbed only for queue continuity and is still returned to its original caller, so one disk failure cannot permanently poison later saves.\n\n[Explore the complete before/after repository](https://gitlab.com/abbasmir12/aether-canva-bugsmash)\n\nSECTION IV · ENGINEERING FOLLOW-THROUGH\n\nFixing the index race exposed two nearby assumptions that also needed attention. I treated this as one persistence-integrity repair rather than stopping at the first passing test.\n\n01 · TRANSACTION BOUNDARYOne long-lived store and one exclusive queue protect every complete persistence operation. |\n02 · SAVE REVISIONSA newer edit cannot be cleared by the completion of an older in-flight save. |\n03 · CLOSE HANDSHAKEElectron waits for the renderer's latest snapshot before destroying the window. |\n04 · REGRESSION PROOFSix deterministic tests protect the failure modes instead of merely mirroring the implementation. |\n\nThe Electron main process now keeps one long-lived workspace store across IPC handlers. Creating a new store per request would create multiple queues and quietly defeat serialization.\n\nThe renderer previously used a boolean dirty flag. That is not enough when a new edit arrives while an older save is still running: completion of the older save can clear the flag belonging to the newer edit.\n\nThe new saver tracks revisions and drains until the saved revision catches the latest one:\n\n``` js\nwhile (this.latest && this.savedRevision < this.revision) {\n  const snapshot = this.latest;\n  const revision = this.revision;\n  await save(snapshot);\n  this.savedRevision = revision;\n}\n```\n\nIf revision 2 arrives while revision 1 is being written, revision 1 cannot declare revision 2 safe. The loop writes again.\n\nAn asynchronous `beforeunload`\n\ncallback cannot force Electron to wait before destroying its renderer. The main process now intercepts the close request, asks the renderer to drain its latest snapshot, and closes the native window only after the renderer acknowledges completion.\n\nThe six deterministic tests cover:\n\nStrict TypeScript checks, the renderer/main/preload production builds, and Linux AppImage packaging also pass with the repair.\n\nReproduce the controlled comparison locally\n\n```\ngit clone https://gitlab.com/abbasmir12/aether-canva-bugsmash.git\ncd aether-canva-bugsmash\ngit switch fix/workspace-transaction-race\nnpm install\nnpx vite build\n\n# Run each stage separately so the real Electron state remains visible.\nnpm run bugsmash:before\nnpm run bugsmash:after\n\n# Or print both results without opening two staged windows.\nnpm run bugsmash:demo\n```\n\nThe Sentry flag requires your own DSN. The reproduction itself does not.\n\nSECTION V · OBSERVABILITY DESK\n\nI am submitting this entry for **Best Use of Sentry**.\n\nSentry had a specific job here: make a silent logical invariant observable.\n\nIt would be inaccurate to say that Sentry magically discovered the source line or repaired the race. Code inspection and deterministic stress tests found the transaction-boundary bug. Sentry then gave me runtime evidence that the application could report successful operations while its persisted state was inconsistent—and confirmed that the repaired build remained consistent under the identical workload.\n\nWHAT SENTRY DIDRecorded queue pressure, operation duration, and the count-only workspace integrity invariant in the running Electron application. |\nWHAT SENTRY DID NOT DOIt did not invent the root cause or upload the user's workspace. Code inspection and tests located the faulty transaction boundary. |\n\nUsing the Sentry Electron SDK, every workspace IPC operation creates a custom transaction containing only:\n\n`create`\n\n, `save`\n\n, or `rename`\n\n;This made serialization pressure visible. For example, an authenticated `workspace.create`\n\ntrace showed a queue depth of 10 and a root duration of approximately 175 ms. A successful span status alone, however, could not reveal the hidden loss. For that I needed a product-level invariant.\n\nThe opt-in `workspace.integrity-audit`\n\ncompares the index with the workspace directory locally and reports only aggregate counts:\n\n```\nattributes: {\n  'aether.workspace.files_count': audit.workspaceFiles,\n  'aether.workspace.indexed_count': audit.indexedWorkspaces,\n  'aether.workspace.orphaned_count': audit.orphanedFiles,\n  'aether.workspace.missing_file_count': audit.missingFiles,\n  'aether.workspace.integrity_consistent': audit.consistent,\n}\n\nspan.setStatus(audit.consistent\n  ? { code: 1, message: 'ok' }\n  : { code: 2, message: 'data_loss' });\n```\n\nThe original run produced:\n\n```\nfiles_count          40\nindexed_count         1\norphaned_count       39\nintegrity_consistent false\nstatus               data_loss\n```\n\nThe repaired run produced:\n\n```\nfiles_count          40\nindexed_count        40\norphaned_count        0\nintegrity_consistent true\nstatus               ok\n```\n\n🔴 BEFORESILENT DATA LOSS 40 files · 1 indexed39 orphaned · data_loss\n|\n│ │ │ │ │ │ │ │ │ |\n🟢 AFTERINTEGRITY RESTORED 40 files · 40 indexed0 orphaned · ok\n|\n\n**THE TRACE'S STRANGEST HEADLINE****Issues: 0 · Orphaned workspaces: 39**\n\nTraditional exception monitoring was telling the truth—nothing threw. The integrity transaction supplied the missing definition of correctness.\n\nAether is local-first, so the instrumentation had to respect that promise. The custom telemetry never includes workspace names, IDs, paths, file contents, canvas content, AI prompts, or AI responses. Transaction hooks remove user, request, extra, and breadcrumb payloads; UI-click breadcrumbs are disabled; raw IP storage is disabled in the Sentry project; and an advanced scrubbing rule removes `user.geo`\n\n.\n\nThis was also a useful lesson: observability is not only about collecting more information. Sometimes it is about identifying the **smallest safe signal** that proves the system is healthy.\n\nFINAL EDITION · THE QUIETEST FIXES CAN PROTECT THE MOST IMPORTANT DATA", "url": "https://wpnews.pro/news/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors", "canonical_source": "https://dev.to/mirshah12/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors-hmi", "published_at": "2026-08-15 15:08:13+00:00", "updated_at": "2026-08-15 15:42:13.414306+00:00", "lang": "en", "topics": ["developer-tools", "ai-products"], "entities": ["Aether Canvas", "OpenAI", "Sentry", "Abbas Mirza"], "alternates": {"html": "https://wpnews.pro/news/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors", "markdown": "https://wpnews.pro/news/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors.md", "text": "https://wpnews.pro/news/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors.txt", "jsonld": "https://wpnews.pro/news/i-used-sentry-to-expose-a-silent-data-loss-bug-with-zero-errors.jsonld"}}