<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Naufal Hafizh</title>
    <description>The latest articles on DEV Community by Naufal Hafizh (@naufalfx805source).</description>
    <link>https://dev.to/naufalfx805source</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4074441%2Fd587ff5d-7648-4008-8ad8-a0aa20c195f2.png</url>
      <title>DEV Community: Naufal Hafizh</title>
      <link>https://dev.to/naufalfx805source</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/naufalfx805source"/>
    <language>en</language>
    <item>
      <title>TestSprite caught three real bugs before I could merge: building AgentPulse for TestSprite Hackathon S4</title>
      <dc:creator>Naufal Hafizh</dc:creator>
      <pubDate>Thu, 22 Oct 2026 22:39:02 +0000</pubDate>
      <link>https://dev.to/naufalfx805source/testsprite-caught-three-real-bugs-before-i-could-merge-building-agentpulse-for-testsprite-3gdg</link>
      <guid>https://dev.to/naufalfx805source/testsprite-caught-three-real-bugs-before-i-could-merge-building-agentpulse-for-testsprite-3gdg</guid>
      <description>&lt;p&gt;TestSprite Hackathon Season 4 had a simple theme: &lt;strong&gt;One Week. One Loop. Every PR.&lt;/strong&gt; Wire TestSprite into your repo so every pull request gets tested on its own, let your coding agent fix what fails, and let the check rerun by itself.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;AgentPulse&lt;/strong&gt;, a mission-control dashboard for teams that ship with AI coding agents: fleet reliability telemetry, a pipeline DAG studio, a test-scenario repository, a chaos and webhook simulator, and an inspector for the write → test → fix → test-again loop.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://agentpulse-pi.vercel.app" rel="noopener noreferrer"&gt;https://agentpulse-pi.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/naufalfx805-source/agentpulse" rel="noopener noreferrer"&gt;https://github.com/naufalfx805-source/agentpulse&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The PR this post is about: &lt;a href="https://github.com/naufalfx805-source/agentpulse/pull/5" rel="noopener noreferrer"&gt;https://github.com/naufalfx805-source/agentpulse/pull/5&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A suite generated from the live app.&lt;/strong&gt; In the TestSprite 3.0 web portal, the project &lt;code&gt;agentpulse-pi&lt;/code&gt; crawled the deployed site and produced 33 end-to-end tests across five use cases: Fleet Overview, Pipeline Studio, Test Scenarios, the Self-Healing Verification Loop, and Chaos Testing &amp;amp; Webhook Simulation. I later added four input-validation cases, for 37 in total.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A preview for every PR.&lt;/strong&gt; The Vercel project is connected to GitHub, so each pull request gets its own preview deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A TestSprite trigger on that preview.&lt;/strong&gt; In the portal's GitHub Action tab, the trigger fires on the &lt;em&gt;Preview deployment&lt;/em&gt; event and targets &lt;code&gt;https://agentpulse-git-{branch-slug}-&amp;lt;team&amp;gt;.vercel.app&lt;/code&gt;, so the suite runs against the PR's own code, not production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A merge gate.&lt;/strong&gt; TestSprite reports a &lt;code&gt;TestSprite V3&lt;/code&gt; status, and branch protection on &lt;code&gt;main&lt;/code&gt; requires it, admins included. A red check really does block the merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Gotchas worth knowing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vercel Deployment Protection.&lt;/strong&gt; On Hobby, previews sit behind Vercel login by default, so TestSprite would happily "test" the login page. My app is a public demo with no auth, so I turned protection off for previews. Don't reach for a bypass secret in the target URL: TestSprite prints that URL in a public PR comment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;63-character hostnames.&lt;/strong&gt; Vercel truncates long preview subdomains, which breaks a &lt;code&gt;{branch-slug}&lt;/code&gt; pattern. Short branch names (mine was &lt;code&gt;verify-gate&lt;/code&gt;) keep the URL predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A trigger only sees new events.&lt;/strong&gt; I created the trigger after the PR's first preview, so I redeployed that preview to fire the first run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep your checks honest.&lt;/strong&gt; An earlier CI job in this repo was named "TestSprite 3.0 Verification Gate" but only ran &lt;code&gt;npm test&lt;/code&gt; plus an archived action with no credentials. I renamed it to &lt;code&gt;Build &amp;amp; Unit Tests&lt;/code&gt; and let the real TestSprite App check carry its own name.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The loop on PR #5
&lt;/h2&gt;

&lt;p&gt;The PR started as docs plus four new validation cases for the "new test scenario" form. Here is what TestSprite did with it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;33/37, merge blocked.&lt;/strong&gt; The four new cases failed against the preview. The form accepted an expected HTTP status of &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;999&lt;/code&gt; or blank (storing &lt;code&gt;200&lt;/code&gt; while the assertion kept the raw value), and it rejected a target path with surrounding spaces because it checked the leading slash before trimming. Each failure came with screenshots and a suggested fix prompt for the coding agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix 1 → 35/37.&lt;/strong&gt; My coding agent (Claude Code) moved validation into a small framework-free module with unit tests: status must be a whole number from 100 to 599, and title and path are trimmed first. TestSprite reran on its own. The &lt;code&gt;0&lt;/code&gt;, &lt;code&gt;999&lt;/code&gt; and trimming cases passed, but two cases were still red. The blank one turned out to be a subtler bug. The test cleared the field with a scripted fill, which changes the DOM without firing React's &lt;code&gt;onChange&lt;/code&gt;, so the submit handler validated a stale &lt;code&gt;"200"&lt;/code&gt; from state. Autofill can do the same thing to real users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix 2 → 36/37.&lt;/strong&gt; The form now reads its values with &lt;code&gt;FormData&lt;/code&gt; on submit, so it validates what is actually in the fields. That left the other red case: Fleet Overview showed monitored pipelines but gave no way to open a specific one. "View Pipelines" always opened the first pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix 3 → 37/37.&lt;/strong&gt; Each pipeline card got a "View Details" button that opens that pipeline in Pipeline Studio. The check went green and the merge gate opened.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I took away
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End-to-end tests on a real preview catch what unit tests can't.&lt;/strong&gt; The stale-state bug lived between the DOM and React state. My unit tests passed the whole time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The merge gate is what makes it a loop.&lt;/strong&gt; Without branch protection, a red TestSprite comment is just advice. With it, the PR can't move until the suite is green.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The failure reports are agent-ready.&lt;/strong&gt; Every failing case came with what was observed and a fix prompt, which is exactly the input a coding agent needs for the next commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to the TestSprite team for running Season 4. If you're wiring TestSprite into your own repo, start with the preview trigger and the merge gate, and keep your branch names short.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>ai</category>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Sentry pointed at the crash. The bug was one frame upstream.</title>
      <dc:creator>Naufal Hafizh</dc:creator>
      <pubDate>Sun, 16 Aug 2026 09:50:49 +0000</pubDate>
      <link>https://dev.to/naufalfx805source/sentry-pointed-at-the-crash-the-bug-was-one-frame-1gnk</link>
      <guid>https://dev.to/naufalfx805source/sentry-pointed-at-the-crash-the-bug-was-one-frame-1gnk</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/formbricks/formbricks" rel="noopener noreferrer"&gt;Formbricks&lt;/a&gt; is an open-source survey and experience&lt;br&gt;
management platform — the self-hostable alternative to Qualtrics. It is also one of the repos&lt;br&gt;
this challenge lists as already running Sentry.&lt;/p&gt;

&lt;p&gt;The part I worked in is its JavaScript SDK: &lt;code&gt;packages/js-core&lt;/code&gt;, which bootstraps the SDK and&lt;br&gt;
keeps its config in &lt;code&gt;localStorage&lt;/code&gt;, and &lt;code&gt;packages/surveys&lt;/code&gt;, which talks to the API and renders&lt;br&gt;
the survey widget. That code does not run on formbricks.com. It runs embedded in &lt;em&gt;other&lt;br&gt;
people's&lt;/em&gt; websites, which changes what a crash costs — nobody on the Formbricks team is&lt;br&gt;
watching the page it breaks.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The issue
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/formbricks/formbricks/issues/6581" rel="noopener noreferrer"&gt;formbricks#6581&lt;/a&gt; has been open since&lt;br&gt;
22 September 2025 and still is, with nobody assigned. The body cites Sentry issue&lt;br&gt;
&lt;code&gt;FORMBRICKS-CLOUD-3VE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeError: Cannot read properties of null (reading 'data')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That error is a nuisance to act on, because it names the property that got read, not the&lt;br&gt;
thing that was null, and not the code that made it null. You get a frame in whatever&lt;br&gt;
component happened to touch the value — and the actual defect is somewhere upstream, in&lt;br&gt;
whatever handed it over.&lt;/p&gt;

&lt;p&gt;Formbricks' JS SDK is embedded in other people's sites. A crash there isn't a broken page&lt;br&gt;
in the Formbricks app; it's a survey widget that silently dies inside a customer's product.&lt;br&gt;
That's what made the issue worth taking.&lt;/p&gt;
&lt;h3&gt;
  
  
  Finding where the null was born
&lt;/h3&gt;

&lt;p&gt;The reported frame reads &lt;code&gt;.data&lt;/code&gt; off something, so the question isn't "what crashed" — it's&lt;br&gt;
"who was allowed to hand that function a null in the first place?" Working backwards through&lt;br&gt;
the callers ends at &lt;code&gt;makeRequest&lt;/code&gt; in &lt;code&gt;packages/surveys/src/lib/utils.ts&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its signature is a promise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;makeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ApiErrorResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; is the codebase's discriminated union — &lt;code&gt;{ ok: true, data: T }&lt;/code&gt; or&lt;br&gt;
&lt;code&gt;{ ok: false, error: E }&lt;/code&gt;. Every caller branches on &lt;code&gt;.ok&lt;/code&gt; and then trusts &lt;code&gt;.data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But the API can answer &lt;code&gt;200 OK&lt;/code&gt; with &lt;code&gt;{"data": null}&lt;/code&gt;, and &lt;code&gt;makeRequest&lt;/code&gt; passed that&lt;br&gt;
straight through as &lt;code&gt;ok(null)&lt;/code&gt;. A success carrying nothing. The type says &lt;code&gt;T&lt;/code&gt;; the value is&lt;br&gt;
&lt;code&gt;null&lt;/code&gt;; TypeScript never sees it because the response was cast on the way in.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;response-queue.ts&lt;/code&gt; does what every caller does — and note that it does it &lt;em&gt;correctly&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;surveyState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateResponseId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ← the frame Sentry reports&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That caller isn't sloppy. It checks &lt;code&gt;.ok&lt;/code&gt;. It propagates the error. It earns the right to&lt;br&gt;
read &lt;code&gt;.data&lt;/code&gt; on the next line, because the type it was handed says that's safe. Then it&lt;br&gt;
crashes anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The crash is one frame away from the bug.&lt;/strong&gt; That distance is why the issue is still open&lt;br&gt;
nearly eleven months later. Nothing in the reported stack points at &lt;code&gt;makeRequest&lt;/code&gt;, and&lt;br&gt;
nothing in the crashing function is wrong.&lt;/p&gt;

&lt;p&gt;Two more places had the same shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ApiClient.uploadFile()&lt;/code&gt; destructured &lt;code&gt;data&lt;/code&gt; from the storage API's signing response with
no null check.&lt;/li&gt;
&lt;li&gt;Two call sites in &lt;code&gt;packages/js-core&lt;/code&gt; called &lt;code&gt;JSON.parse(localStorage.getItem(...))&lt;/code&gt;
unguarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one turned out to be the more interesting half.&lt;/p&gt;
&lt;h3&gt;
  
  
  The &lt;code&gt;localStorage&lt;/code&gt; half: a contract violated from the inside
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Config.loadFromLocalStorage()&lt;/code&gt; in &lt;code&gt;packages/js-core&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;loadFromLocalStorage&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TConfig&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JS_LOCAL_STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// TODO: validate config&lt;/span&gt;
      &lt;span class="c1"&gt;// This is a hack to get around the fact that we don't have a proper&lt;/span&gt;
      &lt;span class="c1"&gt;// way to validate the config yet.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;TConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// ← throws&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsedConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;No or invalid config in local storage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there were already telling on the bug before I touched it.&lt;/p&gt;

&lt;p&gt;There's a &lt;code&gt;TODO&lt;/code&gt; saying the config isn't validated — so the gap was known. And the error&lt;br&gt;
path's message already says &lt;strong&gt;"No or invalid config"&lt;/strong&gt;, which means someone wrote a branch&lt;br&gt;
for exactly this case.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;JSON.parse&lt;/code&gt; throws, so a corrupt value never reaches that &lt;code&gt;err&lt;/code&gt; four lines below. It&lt;br&gt;
leaves through the exception instead, past the function's own contract. The handling was&lt;br&gt;
written; the input just never arrived at it.&lt;/p&gt;

&lt;p&gt;And the constructor calls it like the contract is real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;savedConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loadFromLocalStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// no try/catch&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;savedConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;savedConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no &lt;code&gt;try/catch&lt;/code&gt; because there shouldn't need to be one — the function returns a&lt;br&gt;
&lt;code&gt;Result&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So where does the throw actually land? Not on the host page, it turns out.&lt;br&gt;
&lt;code&gt;CommandQueue.run()&lt;/code&gt; wraps every command in &lt;code&gt;wrapThrowsAsync&lt;/code&gt; and, on failure, does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;🧱 Formbricks - Global error: &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup never completes. &lt;code&gt;isSetup&lt;/code&gt; is never set. And every SDK command after it hits the&lt;br&gt;
guard at the top of the queue loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;setupResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`🧱 Formbricks - Setup not complete.`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole failure. One corrupt &lt;code&gt;localStorage&lt;/code&gt; entry, and the SDK spends the rest of&lt;br&gt;
the page lifetime politely declining to do anything. The host site works fine. Nothing is&lt;br&gt;
visibly broken. The survey just never appears, and the only trace is two console lines&lt;br&gt;
nobody is reading on a customer's production site.&lt;/p&gt;

&lt;p&gt;A crash that takes the page down gets reported in an hour. This one can run for months.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localStorage&lt;/code&gt; is not a place where you get to assume well-formed input. You can get a&lt;br&gt;
truncated write from a tab that closed mid-&lt;code&gt;setItem&lt;/code&gt;, a quota failure, a hand-edit in&lt;br&gt;
devtools, or a payload written by a previous version of the SDK. The same unguarded parse&lt;br&gt;
also sat in &lt;code&gt;migrateLocalStorage()&lt;/code&gt;, which runs &lt;em&gt;first&lt;/em&gt; during setup — so a corrupt entry&lt;br&gt;
aborted setup before anything else could run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;All three commits are on &lt;strong&gt;&lt;a href="https://github.com/naufalfx805-source/formbricks" rel="noopener noreferrer"&gt;my fork's &lt;code&gt;main&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;,&lt;br&gt;
rebased onto upstream &lt;code&gt;ace2c9b&lt;/code&gt; — 289 insertions across 9 files:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;commit&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/naufalfx805-source/formbricks/commit/de95090" rel="noopener noreferrer"&gt;&lt;code&gt;de95090&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fix(surveys)&lt;/code&gt; — null guards in &lt;code&gt;makeRequest&lt;/code&gt; / &lt;code&gt;uploadFile&lt;/code&gt;, plus &lt;code&gt;safeJsonParse&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/naufalfx805-source/formbricks/commit/2171a15" rel="noopener noreferrer"&gt;&lt;code&gt;2171a15&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fix(js-core)&lt;/code&gt; — route both &lt;code&gt;localStorage&lt;/code&gt; parse sites through it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/naufalfx805-source/formbricks/commit/a730481" rel="noopener noreferrer"&gt;&lt;code&gt;a730481&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;test(surveys)&lt;/code&gt; — cover the &lt;code&gt;uploadFile&lt;/code&gt; guard&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Files touched: &lt;code&gt;js-core/src/lib/common/{config,setup,utils}.ts&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;surveys/src/lib/{utils,api-client}.ts&lt;/code&gt;, plus their four test files.&lt;/p&gt;

&lt;p&gt;The before/after Sentry reproduction is a separate repo:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/naufalfx805-source/bugsmash-sentry-repro" rel="noopener noreferrer"&gt;bugsmash-sentry-repro&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why there's no upstream PR
&lt;/h3&gt;

&lt;p&gt;Formbricks' &lt;code&gt;CONTRIBUTING.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;we don't have the capacity to properly facilitate community contributions … only in rare&lt;br&gt;
exceptions&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The challenge asks entrants to read &lt;code&gt;CONTRIBUTING.md&lt;/code&gt; and not add to maintainer burnout, and&lt;br&gt;
its FAQ allows merging on a fork when upstream can't take the PR. So it is merged on the fork.&lt;/p&gt;

&lt;p&gt;Opening a PR would have felt more like a submission. It would also have put a 289-line review&lt;br&gt;
across 9 files on a team that has said in writing they can't take it.&lt;/p&gt;
&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;A single small helper, and routing every unguarded parse through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;safeJsonParse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;makeRequest&lt;/code&gt; returns &lt;code&gt;err()&lt;/code&gt; instead of &lt;code&gt;ok(null)&lt;/code&gt; when the payload is empty.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;uploadFile&lt;/code&gt; throws a &lt;em&gt;named&lt;/em&gt; error before the destructure.&lt;/li&gt;
&lt;li&gt;Both &lt;code&gt;js-core&lt;/code&gt; parse sites fall back to the &lt;code&gt;err&lt;/code&gt; path each function already declared.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;js-core&lt;/code&gt; has no workspace dependencies, so it can't import the copy in&lt;br&gt;
&lt;code&gt;@formbricks/surveys&lt;/code&gt; — it got its own. Duplication I'd normally push back on, but a&lt;br&gt;
package with zero deps is a deliberate property here, not an oversight.&lt;/p&gt;
&lt;h3&gt;
  
  
  Auditing my own fix
&lt;/h3&gt;

&lt;p&gt;The fix was written, tested, green. Then I went back and read it as if someone else had&lt;br&gt;
sent it to me. Two things were wrong, and neither one shows up as a failing test.&lt;/p&gt;
&lt;h4&gt;
  
  
  1. The helper was dead code
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;safeJsonParse&lt;/code&gt; was exported. It was unit-tested — nine tests, all passing. It was cited&lt;br&gt;
in the commit message as the fix for a &lt;code&gt;JSON.parse&lt;/code&gt; crash class.&lt;/p&gt;

&lt;p&gt;Nothing called it.&lt;/p&gt;

&lt;p&gt;The tests passed because they tested the helper directly. Coverage went up. The diff looked&lt;br&gt;
right. And the actual crash sites were untouched, because I'd fixed the &lt;em&gt;category&lt;/em&gt; of bug&lt;br&gt;
and then not connected it to the two places that had it.&lt;/p&gt;

&lt;p&gt;That's the failure mode I'd flag hardest in someone else's PR, and it went straight past me&lt;br&gt;
in my own.&lt;/p&gt;
&lt;h4&gt;
  
  
  2. The guard nobody tested
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;uploadFile&lt;/code&gt; got its null check. &lt;code&gt;api-client.test.ts&lt;/code&gt; had eight existing &lt;code&gt;uploadFile&lt;/code&gt; tests&lt;br&gt;
and they all passed.&lt;/p&gt;

&lt;p&gt;They also all passed against the &lt;strong&gt;pre-fix&lt;/strong&gt; source. I checked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tests  17 passed (17)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seventeen green tests, and not one of them would have noticed the guard being deleted,&lt;br&gt;
inverted, or never written. The fix was real and the coverage number said nothing about it.&lt;/p&gt;

&lt;p&gt;Both follow-ups came from auditing the fix, not the codebase. The check that would have&lt;br&gt;
caught either one takes about a minute:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Grep your own fix for call sites and test coverage before you claim it.&lt;/strong&gt;&lt;br&gt;
If the new helper appears only in its own definition and its own test file, it's dead.&lt;br&gt;
If the guarded line still behaves identically with the guard removed, it's untested.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Proving the tests are real
&lt;/h3&gt;

&lt;p&gt;A regression test that has never failed is a guess. So for each one I reverted the fix and&lt;br&gt;
re-ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;× loadFromStorage() returns err instead of throwing on malformed JSON
  → SyntaxError: Expected property name or '}' in JSON at position 1
× loadFromStorage() returns err instead of throwing on a truncated write
  → SyntaxError: Unterminated string in JSON at position 26
× throws a named error when the signing response carries a null data payload
  → TypeError: Cannot destructure property 'signedUrl' of 'data' as it is null.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same error class as the Sentry issue. Restored the fix, all green:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;js-core&lt;/code&gt; — &lt;strong&gt;304 passing&lt;/strong&gt;, 20/20 files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;surveys&lt;/code&gt; — &lt;strong&gt;626 passing&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of the control tests correctly pass &lt;em&gt;both&lt;/em&gt; ways, which is the point of having them.&lt;/p&gt;

&lt;h4&gt;
  
  
  A detour worth mentioning
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;pnpm install&lt;/code&gt; is unusable on this repo from WSL — on &lt;code&gt;/mnt/c&lt;/code&gt; it hangs with zero output&lt;br&gt;
for ten minutes and never finishes. I couldn't run the suite the normal way at all.&lt;/p&gt;

&lt;p&gt;What worked: copy the package source to the Linux filesystem, write a minimal&lt;br&gt;
&lt;code&gt;vitest.config.ts&lt;/code&gt;, install with plain &lt;code&gt;npm&lt;/code&gt;. &lt;code&gt;js-core&lt;/code&gt; has no workspace deps so it's&lt;br&gt;
clean; &lt;code&gt;surveys&lt;/code&gt; needs &lt;code&gt;types&lt;/code&gt;, &lt;code&gt;survey-ui&lt;/code&gt;, and &lt;code&gt;i18n-utils&lt;/code&gt; copied alongside it in the&lt;br&gt;
same &lt;code&gt;packages/&lt;/code&gt; layout, because several tests import &lt;code&gt;../../../types/x&lt;/code&gt; relatively.&lt;/p&gt;

&lt;p&gt;One trap cost me a while: &lt;strong&gt;pin the same vitest the repo pins.&lt;/strong&gt; My harness was on vitest 3&lt;br&gt;
against a repo pinning 4.1.6, and four tests in an untouched file failed with&lt;br&gt;
&lt;code&gt;No script element for surveys.umd.cjs was appended to document.head&lt;/code&gt;. They failed&lt;br&gt;
identically against pre-fix source — nothing to do with my change — and vanished on 4.1.6.&lt;br&gt;
I nearly reported a passing suite as "304 passing except four known failures" on the&lt;br&gt;
strength of a version mismatch in my own tooling.&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;Since the issue came from Sentry, "fixed" should be demonstrable in Sentry — not just in a&lt;br&gt;
diff. So I built a reproduction that runs the &lt;strong&gt;real package source at both commits&lt;/strong&gt;&lt;br&gt;
against a live DSN:&lt;br&gt;
&lt;a href="https://github.com/naufalfx805-source/bugsmash-sentry-repro" rel="noopener noreferrer"&gt;&lt;code&gt;bugsmash-sentry-repro&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;src-before/&lt;/code&gt; is &lt;code&gt;git archive&lt;/code&gt; of upstream &lt;code&gt;ace2c9b&lt;/code&gt;. &lt;code&gt;src-after/&lt;/code&gt; is the fork with the&lt;br&gt;
fix. No hand-edited source — the repo's &lt;code&gt;NOTICE.md&lt;/code&gt; gives the commands to reproduce both&lt;br&gt;
trees yourself, so you don't have to take my word for what's in them.&lt;/p&gt;

&lt;p&gt;Each scenario runs in its own process and the exception is left &lt;strong&gt;uncaught&lt;/strong&gt; on purpose.&lt;br&gt;
The entire reason this reached Sentry is that it escaped every handler in its path — so&lt;br&gt;
catching it and calling &lt;code&gt;captureException&lt;/code&gt; by hand would produce an event with a different&lt;br&gt;
shape than the reported one.&lt;/p&gt;

&lt;p&gt;Every event is tagged &lt;code&gt;variant:before&lt;/code&gt; / &lt;code&gt;variant:after&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scenario&lt;/th&gt;
&lt;th&gt;before&lt;/th&gt;
&lt;th&gt;after&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config-truncated&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;crashed&lt;/strong&gt; — &lt;code&gt;SyntaxError: Unterminated string in JSON&lt;/code&gt;, uncaught, out of the constructor&lt;/td&gt;
&lt;td&gt;handled — &lt;code&gt;err("No or invalid config in local storage")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;config-hand-edited&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;crashed&lt;/strong&gt; — &lt;code&gt;SyntaxError: Expected property name or '}'&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;handled — &lt;code&gt;err("No or invalid config in local storage")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;make-request-null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;crashed&lt;/strong&gt; — &lt;code&gt;ok(null)&lt;/code&gt; propagates → &lt;code&gt;TypeError: Cannot read properties of null&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;handled — &lt;code&gt;err("API returned an empty data payload")&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;upload-null-data&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;crashed&lt;/strong&gt; — &lt;code&gt;TypeError: Cannot destructure property 'signedUrl' of 'data' as it is null.&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;crashed&lt;/strong&gt; — &lt;code&gt;Error: Upload API returned an empty data payload&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Totals: 5 crashed, 3 handled.&lt;/p&gt;

&lt;p&gt;That last row is the one I'd point a Sentry engineer at. &lt;strong&gt;It still throws.&lt;/strong&gt; An empty&lt;br&gt;
signing response genuinely is a failure and &lt;code&gt;uploadFile&lt;/code&gt; should still fail. The crash rate&lt;br&gt;
doesn't move.&lt;/p&gt;

&lt;p&gt;What moves is what lands in the dashboard. Before, Sentry gets an error naming a local&lt;br&gt;
variable in a destructuring pattern — you have to go read the source to learn which network&lt;br&gt;
call failed. After, it gets an error that says the upload API returned an empty payload.&lt;/p&gt;

&lt;p&gt;Same event count. One of them you can triage from the issue list.&lt;/p&gt;

&lt;p&gt;In the dashboard those five crashes arrive as &lt;strong&gt;three&lt;/strong&gt; issues, not four, and the reason is&lt;br&gt;
worth a moment. Sentry groups by stack trace, not by message — and both &lt;code&gt;localStorage&lt;/code&gt;&lt;br&gt;
scenarios throw from the same &lt;code&gt;JSON.parse&lt;/code&gt; on the same line of &lt;code&gt;loadFromLocalStorage&lt;/code&gt;. A&lt;br&gt;
truncated write and a hand-edited value are different inputs with different error text, and&lt;br&gt;
they land in one issue together.&lt;/p&gt;

&lt;p&gt;Which is exactly what makes the production version hard to read. You don't see "corrupt&lt;br&gt;
localStorage, four flavours." You see one issue with a rising event count and whichever&lt;br&gt;
message happened to arrive last — and the fix has to cover causes the issue title never&lt;br&gt;
mentions.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;variant:after&lt;/code&gt; side inverts that, and this is the result I did not expect going in.&lt;br&gt;
Four rows, but only &lt;strong&gt;one&lt;/strong&gt; carries Sentry's red &lt;code&gt;Unhandled&lt;/code&gt; badge — the &lt;code&gt;uploadFile&lt;/code&gt; error&lt;br&gt;
that should still fail. The other three are the handled paths reporting themselves, each as&lt;br&gt;
its own entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Upload API returned an empty data payload          Unhandled
[make-request-null]  null API payload handled without an unnamed crash
[config-truncated]   corrupt localStorage handled without throwing
[config-hand-edited] corrupt localStorage handled without throwing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two &lt;code&gt;localStorage&lt;/code&gt; causes that were fused into a single issue before are now separate&lt;br&gt;
and individually named. Grouping by stack trace collapsed them precisely because they&lt;br&gt;
crashed at the same line; once neither one crashes, each is visible on its own terms.&lt;/p&gt;

&lt;p&gt;So the fix does two things to the telemetry, not one. It renames the crash that remains, and&lt;br&gt;
it un-merges the two failures that were hiding behind one row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnu49p2zqiw4p2sz0ddeh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnu49p2zqiw4p2sz0ddeh.png" alt="Sentry issue list filtered to variant:before, showing three issues: TypeError reading 'id', TypeError destructuring signedUrl, and SyntaxError from JSON.parse — all marked Unhandled"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;variant:before&lt;/code&gt; — three issues, every one of them &lt;code&gt;Unhandled&lt;/code&gt;. The &lt;code&gt;SyntaxError&lt;/code&gt; row carries&lt;br&gt;
four events because both &lt;code&gt;localStorage&lt;/code&gt; scenarios group into it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8a2s9gjifi1w7ng261z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd8a2s9gjifi1w7ng261z.png" alt="Sentry issue list filtered to variant:after, showing four rows of which only the Upload API error is marked Unhandled; the other three are handled-path reports"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;code&gt;variant:after&lt;/code&gt; — four rows, one &lt;code&gt;Unhandled&lt;/code&gt;. The two &lt;code&gt;localStorage&lt;/code&gt; causes that shared an&lt;br&gt;
issue before are now separate and individually named.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59g6c65r684cw3hp1fc1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F59g6c65r684cw3hp1fc1.png" alt="Sentry issue detail for TypeError: Cannot destructure property 'signedUrl' of 'data' as it is null, thrown from ApiClient.uploadFile"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov8y09kgjn056v1rbw0o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov8y09kgjn056v1rbw0o.png" alt="Sentry issue detail for Error: Upload API returned an empty data payload, thrown from the same line of ApiClient.uploadFile"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pair that makes the point. Same function, same line, both still &lt;code&gt;Unhandled&lt;/code&gt;. One names a&lt;br&gt;
variable in a destructuring pattern; the other names the failure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One thing that bit me while building this, and is worth knowing if you write a harness like&lt;br&gt;
it: &lt;strong&gt;you cannot read the outcome from the exit status once a DSN is set.&lt;/strong&gt; With Sentry&lt;br&gt;
disabled the process dies through Node's default handler and exits non-zero. With a DSN, the&lt;br&gt;
SDK installs its own &lt;code&gt;uncaughtException&lt;/code&gt; handler, flushes the event, and exits &lt;code&gt;0&lt;/code&gt; — so a&lt;br&gt;
crashed run and a clean run become indistinguishable by status, and my first pass cheerfully&lt;br&gt;
labelled all eight runs "handled" while printing their stack traces directly above the label.&lt;br&gt;
The harness reads a sentinel the script prints on its success path instead.&lt;/p&gt;
&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I ran the whole investigation past Gemini afterwards, in one conversation, in the order I'd&lt;br&gt;
actually hit things. The useful result wasn't a win or a failure — it was that the model's&lt;br&gt;
accuracy tracked the &lt;em&gt;shape of the question&lt;/em&gt; far more than the difficulty of the problem.&lt;/p&gt;
&lt;h3&gt;
  
  
  Cold triage: a good list, in the wrong order
&lt;/h3&gt;

&lt;p&gt;Given only the Sentry error and the &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; signature — no mention of &lt;code&gt;makeRequest&lt;/code&gt; — it&lt;br&gt;
produced four candidate causes. The right one was &lt;strong&gt;fourth&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A backend API might return &lt;code&gt;{"ok": true, "data": null}&lt;/code&gt; … the type definition is out of&lt;br&gt;
sync with backend reality."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Above it sat three plausible-sounding wrong trees: a 204 empty body, ad-blockers&lt;br&gt;
monkey-patching &lt;code&gt;fetch&lt;/code&gt; on the host page, and prototype pollution. All real phenomena for an&lt;br&gt;
embedded SDK. None of them this bug.&lt;/p&gt;

&lt;p&gt;Its first recommended fix is the interesting part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Safe to access result.data.id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That patches the crash frame — every call site — rather than the helper that manufactured the&lt;br&gt;
bad &lt;code&gt;Result&lt;/code&gt;. It's the exact trap this post is about, and the model walked into it. Its&lt;br&gt;
&lt;em&gt;second&lt;/em&gt; suggestion (validate inside the helper before returning &lt;code&gt;ok&lt;/code&gt;) was the right layer, so&lt;br&gt;
it offered both without ranking them.&lt;/p&gt;

&lt;p&gt;Fair summary: it generated a candidate list containing the answer. Picking the answer out of&lt;br&gt;
the list still needed someone who knew the codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;code&gt;localStorage&lt;/code&gt; method: strong, then confidently wrong
&lt;/h3&gt;

&lt;p&gt;Shown &lt;code&gt;loadFromLocalStorage&lt;/code&gt; and its constructor, it got everything local: the uncaught&lt;br&gt;
&lt;code&gt;SyntaxError&lt;/code&gt;, the unguarded constructor, the unvalidated cast. It named the real-world&lt;br&gt;
causes unprompted — &lt;em&gt;"interrupted write, browser crash, or user tampering"&lt;/em&gt; — which are the&lt;br&gt;
scenarios in my repro. It even flagged &lt;code&gt;JSON.parse("null")&lt;/code&gt;, one of the three regression tests.&lt;/p&gt;

&lt;p&gt;Its proposed fix is essentially the shipped one.&lt;/p&gt;

&lt;p&gt;Then it asserted this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"it can break the host website's JavaScript execution, preventing their own application&lt;br&gt;
logic, checkout flows, or analytics from running. Host developers will quickly remove your&lt;br&gt;
SDK to protect their site."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which is false, and I'd already checked why: &lt;code&gt;CommandQueue.run()&lt;/code&gt; catches it and logs. The&lt;br&gt;
host page is fine.&lt;/p&gt;

&lt;p&gt;It had two functions and no queue, and extrapolated a blast radius the surrounding code&lt;br&gt;
contradicts. Note the direction of the error — it reached for the &lt;em&gt;dramatic&lt;/em&gt; failure. The&lt;br&gt;
real one is quieter and worse: no broken site, just a survey that never appears, for months.&lt;/p&gt;

&lt;h3&gt;
  
  
  The review that mattered
&lt;/h3&gt;

&lt;p&gt;Then I showed it the full first commit and asked one open question: &lt;strong&gt;"Is the fix complete?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It said no — correct verdict. Its reason: &lt;code&gt;uploadFile&lt;/code&gt; throws a raw &lt;code&gt;Error&lt;/code&gt; instead of&lt;br&gt;
returning a &lt;code&gt;Result&lt;/code&gt;, a &lt;em&gt;"discrepancy in error handling strategy."&lt;/em&gt; It suggested returning&lt;br&gt;
&lt;code&gt;err({...})&lt;/code&gt; instead.&lt;/p&gt;

&lt;p&gt;Except &lt;code&gt;uploadFile&lt;/code&gt; is declared &lt;code&gt;): Promise&amp;lt;string&amp;gt;&lt;/code&gt;. It never returned a &lt;code&gt;Result&lt;/code&gt;, and it&lt;br&gt;
already throws in &lt;strong&gt;nine&lt;/strong&gt; other places. Throwing is its convention; the new guard is the most&lt;br&gt;
consistent thing that could have gone there. The suggested fix wouldn't compile.&lt;/p&gt;

&lt;p&gt;And it named neither actual defect.&lt;/p&gt;

&lt;p&gt;So I asked two closed questions instead:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is &lt;code&gt;safeJsonParse&lt;/code&gt; actually called anywhere in the shipped production code path by this&lt;br&gt;
commit? Is there a test that would fail if the &lt;code&gt;if (!data)&lt;/code&gt; guard were deleted?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt; &lt;code&gt;safeJsonParse&lt;/code&gt; … is not called anywhere else … zero production modules invoke it.&lt;br&gt;
(It appears to have been written preemptively … but was never actually wired into&lt;br&gt;
&lt;code&gt;loadFromLocalStorage&lt;/code&gt; or other parsing logic in this PR.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No.&lt;/strong&gt; … deleting its &lt;code&gt;if (!data)&lt;/code&gt; guard would allow all tests to pass successfully,&lt;br&gt;
leaving that part of the code entirely unprotected and untested.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both correct, immediately, no hedging. It even named &lt;code&gt;loadFromLocalStorage&lt;/code&gt; — the exact call&lt;br&gt;
site the follow-up commit wired up — and described the falsification check I'd run by hand.&lt;/p&gt;

&lt;p&gt;Same diff. Same conversation. Same model. The only thing that changed was the question.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd actually take from it
&lt;/h3&gt;

&lt;p&gt;It was never missing information. It was answering what I asked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Is this complete?" is an open question&lt;/strong&gt;, and an open question gets answered from the most&lt;br&gt;
salient thing in view — in a diff, that's the code that changed. So it reviewed the patch, and&lt;br&gt;
when the patch looked fine, it reached for a style objection to satisfy the question.&lt;br&gt;
&lt;strong&gt;"Is this called?" and "is this covered?" are closed&lt;/strong&gt;, and both were answered instantly from&lt;br&gt;
identical input, because they point at what the patch left out.&lt;/p&gt;

&lt;p&gt;The rule I'm keeping: &lt;strong&gt;ask a review model closed questions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And the limit, stated honestly: the follow-up worked because I already knew what to ask.&lt;br&gt;
Nothing here found the dead code for me. That still came from reading my own diff and&lt;br&gt;
wondering who called the thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away from this
&lt;/h2&gt;

&lt;p&gt;The bug itself is small: four unguarded reads across two packages. What kept it alive for&lt;br&gt;
eleven months is distance. The crash lands one frame from the defect, the SDK's own error&lt;br&gt;
handler turns the fallout into two console lines nobody reads, and Sentry groups two distinct&lt;br&gt;
causes into one row. Every layer between the defect and the person who could fix it removes a&lt;br&gt;
little more information.&lt;/p&gt;

&lt;p&gt;The fix closes that distance in both directions — the code fails where it's wrong, and the&lt;br&gt;
report says what went wrong.&lt;/p&gt;

&lt;p&gt;The part I'll carry forward is smaller and less flattering. Three separate systems told me&lt;br&gt;
this fix was done: a green test suite, a clean diff, and a review model. All three were&lt;br&gt;
answering a narrower question than the one I thought I'd asked. The suite proved the helper&lt;br&gt;
worked, not that anything used it. The diff showed correct code, not reachable code. The&lt;br&gt;
model reviewed the patch, because that's what I gave it.&lt;/p&gt;

&lt;p&gt;None of them were wrong. They were just each answering &lt;em&gt;"is this code good?"&lt;/em&gt; when the&lt;br&gt;
question I needed was &lt;em&gt;"is this code doing anything?"&lt;/em&gt; — and that one has to be asked out&lt;br&gt;
loud, by name, of every piece.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fork: &lt;a href="https://github.com/naufalfx805-source/formbricks" rel="noopener noreferrer"&gt;naufalfx805-source/formbricks&lt;/a&gt; ·&lt;br&gt;
Repro: &lt;a href="https://github.com/naufalfx805-source/bugsmash-sentry-repro" rel="noopener noreferrer"&gt;bugsmash-sentry-repro&lt;/a&gt; ·&lt;br&gt;
Issue: &lt;a href="https://github.com/formbricks/formbricks/issues/6581" rel="noopener noreferrer"&gt;formbricks#6581&lt;/a&gt; ·&lt;br&gt;
Sentry: &lt;code&gt;FORMBRICKS-CLOUD-3VE&lt;/code&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;My Smash Stories entry — &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg"&gt;a race condition in a CLI's credential store&lt;/a&gt; — lands on the same lesson from the other side.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>debugging</category>
      <category>javascript</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>nice</title>
      <dc:creator>Naufal Hafizh</dc:creator>
      <pubDate>Sun, 16 Aug 2026 01:48:27 +0000</pubDate>
      <link>https://dev.to/naufalfx805source/nice-684</link>
      <guid>https://dev.to/naufalfx805source/nice-684</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg" class="crayons-story__hidden-navigation-link"&gt;I shipped a lock. Then I had to prove it could fail.&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Summer Bug Smash: Smash Stories 🐛🛹&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/naufalfx805source" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4074441%2Fd587ff5d-7648-4008-8ad8-a0aa20c195f2.png" alt="naufalfx805source profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/naufalfx805source" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Naufal Hafizh
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Naufal Hafizh
                
                
              
              &lt;div id="story-author-preview-content-4403141" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/naufalfx805source" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4074441%2Fd587ff5d-7648-4008-8ad8-a0aa20c195f2.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Naufal Hafizh&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 15&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg" id="article-link-4403141"&gt;
          I shipped a lock. Then I had to prove it could fail.
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/bugsmash"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;bugsmash&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/testing"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;testing&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/node"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;node&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;5&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              1&lt;span class="hidden s:inline"&gt;&amp;nbsp;comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>I shipped a lock. Then I had to prove it could fail.</title>
      <dc:creator>Naufal Hafizh</dc:creator>
      <pubDate>Sat, 15 Aug 2026 10:56:08 +0000</pubDate>
      <link>https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg</link>
      <guid>https://dev.to/naufalfx805source/i-shipped-a-lock-then-i-had-to-prove-it-could-fail-1nfg</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;## The bug that doesn't report itself&lt;/p&gt;

&lt;p&gt;&lt;code&gt;testsprite-cli&lt;/code&gt; stores API keys in a credentials file, one entry per profile. Writing a&lt;br&gt;
  profile looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;mutateCredentialsFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the whole file, mutate in memory, write the whole thing back via &lt;code&gt;renameSync&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The rename is atomic, and that's what makes this interesting — it's not a corruption bug.&lt;br&gt;
  The file is &lt;em&gt;never&lt;/em&gt; left half-written. Someone thought about atomicity and got it right.&lt;/p&gt;

&lt;p&gt;What the rename doesn't decide is &lt;strong&gt;whose snapshot wins&lt;/strong&gt;. Two writers, both read the same&lt;br&gt;
  starting state, both add their own profile, second &lt;code&gt;renameSync&lt;/code&gt; lands last. The first&lt;br&gt;
  profile is gone.&lt;/p&gt;

&lt;p&gt;Not corrupted. Not errored. Gone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;  $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;testsprite setup &lt;span class="nt"&gt;--profile&lt;/span&gt; staging &amp;amp;
&lt;span class="gp"&gt;  $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;testsprite setup &lt;span class="nt"&gt;--profile&lt;/span&gt; prod &amp;amp;
&lt;span class="gp"&gt;  $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# both exit 0&lt;/span&gt;
&lt;span class="gp"&gt;  $&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# one of them didn't happen&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing raises. Nothing exits non-zero. You find out later, when a command that needs&lt;br&gt;
  &lt;code&gt;staging&lt;/code&gt; reports &lt;code&gt;AUTH_REQUIRED&lt;/code&gt;, and you assume you typo'd something and set it up again.&lt;/p&gt;

&lt;p&gt;That's the part that stuck with me. The failure mode isn't "it broke," it's "it quietly&lt;br&gt;
  didn't happen, and the error surfaces somewhere else, later, attached to a different&lt;br&gt;
  command." A user hitting this has no path back to the cause.&lt;/p&gt;

&lt;p&gt;And it isn't exotic usage. Two shells. A CI job overlapping a local setup. An agent running&lt;br&gt;
  commands in parallel — which, for a CLI whose whole job is being driven by an agent, is&lt;br&gt;
  approximately the normal case.&lt;/p&gt;

&lt;p&gt;## The fix, which was the easy part&lt;/p&gt;

&lt;p&gt;An advisory lock file held across the entire read-modify-write cycle, not just the write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lockPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;wx&lt;/code&gt; is &lt;code&gt;O_EXCL&lt;/code&gt; — it succeeds for exactly one process and throws &lt;code&gt;EEXIST&lt;/code&gt; for everyone&lt;br&gt;
  else. Losers spin until a 5-second timeout.&lt;/p&gt;

&lt;p&gt;Two details that aren't optional:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale lock reclaim.&lt;/strong&gt; A process killed mid-write leaves the lock file behind and every&lt;br&gt;
  future write hangs. So a lock past a threshold gets reclaimed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-checking ownership after the wait.&lt;/strong&gt; Reclaim introduces its own race: while you were&lt;br&gt;
  waiting, someone may have decided &lt;em&gt;your&lt;/em&gt; lock was stale and taken it. So immediately before&lt;br&gt;
  the write, &lt;code&gt;assertHeld()&lt;/code&gt; re-reads the lock file and confirms the token is still yours.&lt;/p&gt;

&lt;p&gt;That merged as &lt;a href="https://github.com/TestSprite/testsprite-cli/pull/272" rel="noopener noreferrer"&gt;#272&lt;/a&gt;. Tests passed.&lt;br&gt;
  Done.&lt;/p&gt;

&lt;p&gt;## Except the tests couldn't have failed&lt;/p&gt;

&lt;p&gt;Here's what I noticed afterward, and it's the actual story.&lt;/p&gt;

&lt;p&gt;The suite had tests for the credentials module. They passed. They also would have passed&lt;br&gt;
  with the entire lock deleted — because &lt;strong&gt;every branch of that lock is cross-process by&lt;br&gt;
  construction&lt;/strong&gt;, and the tests all ran in one process.&lt;/p&gt;

&lt;p&gt;Think about what you'd need to reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;EEXIST&lt;/code&gt; path needs a &lt;em&gt;second process&lt;/em&gt; already holding the lock.&lt;/li&gt;
&lt;li&gt;The wait loop needs contention that lasts longer than one process's critical section.&lt;/li&gt;
&lt;li&gt;Stale reclaim needs a lock file left by a process that died.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assertHeld()&lt;/code&gt; failing needs a third party to steal ownership mid-flight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not one of those is reachable from a single process. An in-process mutex wouldn't even have&lt;br&gt;
  fixed the original bug, and an in-process test can't reach the fix that did.&lt;/p&gt;

&lt;p&gt;So the lock was real code protecting a real race, sitting behind a green suite that was&lt;br&gt;
  structurally incapable of noticing if it broke. Delete the lock, tests still pass, bug&lt;br&gt;
  comes back silently.&lt;/p&gt;

&lt;p&gt;That's a worse position than having no test, because the green check reads as a claim.&lt;/p&gt;

&lt;p&gt;## Making a race lose on purpose&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/TestSprite/testsprite-cli/pull/280" rel="noopener noreferrer"&gt;#280&lt;/a&gt; is tests only — no &lt;code&gt;src/&lt;/code&gt;&lt;br&gt;
  changes, nothing ships to users.&lt;/p&gt;

&lt;p&gt;It spawns &lt;strong&gt;eight real child processes&lt;/strong&gt; — &lt;code&gt;spawn(process.execPath, [CHILD_PATH])&lt;/code&gt;, not&lt;br&gt;
  worker threads, not a mocked &lt;code&gt;fs&lt;/code&gt; — each writing a distinct profile to one shared&lt;br&gt;
  credentials file.&lt;/p&gt;

&lt;p&gt;And spawning them is not enough, which is the trap.&lt;/p&gt;

&lt;p&gt;Eight Node processes come up staggered by tens of milliseconds. The critical section is&lt;br&gt;
  shorter than that. Left alone they queue politely, one after another, never contend, and&lt;br&gt;
  the test passes &lt;em&gt;without ever touching the lock&lt;/em&gt; — the same failure it was written to rule&lt;br&gt;
  out, one level up.&lt;/p&gt;

&lt;p&gt;The fix is a starting gate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Each child writes a &lt;code&gt;&amp;lt;profile&amp;gt;.ready&lt;/code&gt; marker, then spin-waits for a shared &lt;code&gt;start&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;The parent blocks until all eight ready markers exist.&lt;/li&gt;
&lt;li&gt;The parent writes &lt;code&gt;start&lt;/code&gt;, releasing all eight into the critical section together.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The children sleep between polls with &lt;code&gt;Atomics.wait&lt;/code&gt; on a throwaway &lt;code&gt;SharedArrayBuffer&lt;/code&gt;&lt;br&gt;
  rather than &lt;code&gt;setTimeout&lt;/code&gt; — a genuine blocking sleep, with no event-loop scheduling sitting&lt;br&gt;
  between the release and the write.&lt;/p&gt;

&lt;p&gt;Now they collide on purpose. All eight profiles have to be present at the end, and no lock&lt;br&gt;
  file may remain.&lt;/p&gt;

&lt;p&gt;## The other thing a concurrency test has to prove&lt;/p&gt;

&lt;p&gt;A concurrency test that quietly stops testing anything is worse than no test — which is the&lt;br&gt;
  same hazard as the staggered-start one, wearing a different hat. Eight children exiting&lt;br&gt;
  early for an unrelated reason produces a perfectly green run that asserts nothing.&lt;/p&gt;

&lt;p&gt;So the child's contract is pinned by exit code:&lt;/p&gt;

&lt;p&gt;| Case | Exit | Asserted |&lt;br&gt;
  |---|---|---|&lt;br&gt;
  | Missing required env vars | &lt;code&gt;1&lt;/code&gt; | names all four required vars |&lt;br&gt;
  | Start marker never appears | &lt;code&gt;2&lt;/code&gt; | &lt;code&gt;Timed out waiting for start marker: &amp;lt;path&amp;gt;&lt;/code&gt; |&lt;br&gt;
  | &lt;code&gt;writeProfile&lt;/code&gt; rejects (&lt;code&gt;bad]&lt;/code&gt; profile name) | &lt;code&gt;3&lt;/code&gt; | stderr contains &lt;code&gt;Invalid request.&lt;/code&gt; |&lt;/p&gt;

&lt;p&gt;Without these, one typo'd env var name makes all eight children bail before they write&lt;br&gt;
  anything, and the test reports success.&lt;/p&gt;

&lt;p&gt;Every case builds its own &lt;code&gt;mkdtempSync&lt;/code&gt; root and removes it in &lt;code&gt;finally&lt;/code&gt;; nothing touches&lt;br&gt;
  the real credentials path. Timeouts are bounded — 10s per child, 20s for the concurrent&lt;br&gt;
  case, 15s for the timeout case — because an unbounded concurrency test in CI is its own&lt;br&gt;
  kind of outage.&lt;/p&gt;

&lt;p&gt;## What I took from it&lt;/p&gt;

&lt;p&gt;The two PRs are the same length of work and only one of them was hard. Writing the lock was&lt;br&gt;
  mechanical once the race was clear. Building a situation where the lock &lt;em&gt;visibly&lt;/em&gt; matters&lt;br&gt;
  took a starting gate, a blocking sleep primitive, and a set of exit codes whose only job is&lt;br&gt;
  to stop the test from lying.&lt;/p&gt;

&lt;p&gt;The pattern I keep running into, in this repo and elsewhere:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A fix isn't verified until you've watched the test fail without it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a race, that's not a figure of speech — it's the entire engineering problem, because&lt;br&gt;
  the default outcome of a concurrency test is that it passes for the wrong reason.&lt;/p&gt;

&lt;p&gt;Both PRs are merged: &lt;a href="https://github.com/TestSprite/testsprite-cli/pull/272" rel="noopener noreferrer"&gt;#272&lt;/a&gt; (the&lt;br&gt;
  lock, from issue &lt;a href="https://github.com/TestSprite/testsprite-cli/issues/77" rel="noopener noreferrer"&gt;#77&lt;/a&gt;) and&lt;br&gt;
  &lt;a href="https://github.com/TestSprite/testsprite-cli/pull/280" rel="noopener noreferrer"&gt;#280&lt;/a&gt; (the cross-process test).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I also wrote up &lt;a href="https://dev.to/naufalfx805source/sentry-pointed-at-the-crash-the-bug-was-one-frame-1gnk"&gt;a null-propagation crash class in Formbricks&lt;/a&gt;&lt;br&gt;
  for Clear the Lineup, which ended in the same place from the other direction: a fix that&lt;br&gt;
  looked complete, and a green suite that wasn't checking the thing I'd claimed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bugsmash</category>
      <category>testing</category>
      <category>node</category>
      <category>devchallenge</category>
    </item>
  </channel>
</rss>
