<?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>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/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;1&lt;span class="hidden s:inline"&gt;&amp;nbsp;reaction&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;
              

              &lt;span class="hidden s:inline"&gt;Add&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>
