<?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: Prince Panchani</title>
    <description>The latest articles on DEV Community by Prince Panchani (@prince_panchani_f971a20ec).</description>
    <link>https://dev.to/prince_panchani_f971a20ec</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%2F4041663%2Fc03386cb-bda7-4fe6-8f82-498f4bdb8913.png</url>
      <title>DEV Community: Prince Panchani</title>
      <link>https://dev.to/prince_panchani_f971a20ec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prince_panchani_f971a20ec"/>
    <language>en</language>
    <item>
      <title>Migrating a 5-year-old React admin app from CRA + Webpack to Vite + SWC — 166 files, 70 days</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Sun, 23 Aug 2026 11:12:08 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/migrating-a-5-year-old-react-admin-app-from-cra-webpack-to-vite-swc-166-files-70-days-4g6l</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/migrating-a-5-year-old-react-admin-app-from-cra-webpack-to-vite-swc-166-files-70-days-4g6l</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 build was fine. Everything else was on fire.&lt;/p&gt;

&lt;p&gt;Every codebase has a number that nobody says out loud.&lt;/p&gt;

&lt;p&gt;Ours was the time between hitting Ctrl+S and seeing the change in the browser. Long enough to check Slack. Long enough to forget what you were testing. On a cold start, long enough to make coffee.&lt;/p&gt;

&lt;p&gt;DreamNet is the internal admin platform for ZURU's housing division — user management, RBAC, asset catalogues, project publishing, release builds, an order-pricing engine, event management, dashboards with charts and maps. Roughly 130 source files across &lt;code&gt;src/scene&lt;/code&gt; alone. It was scaffolded with Create React App and had been running on &lt;code&gt;react-scripts@3.0.1&lt;/code&gt; — a release from &lt;strong&gt;2019&lt;/strong&gt; — held together with rewire, &lt;code&gt;env-cmd&lt;/code&gt;, and &lt;code&gt;node-sass@4.14.1&lt;/code&gt;, which needed a specific Node version to even compile.&lt;/p&gt;

&lt;p&gt;The proposal was simple: replace the build tool. Vite + SWC. Faster cold start, near-instant HMR, better production builds.&lt;/p&gt;

&lt;p&gt;The reality was that the build tool was the only part that went smoothly.&lt;/p&gt;

&lt;p&gt;This is the story of MR — 166 changed files, 28+ diff revisions, 19 review comments, and 70 days between "let's swap the bundler" and green on production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why a bundler swap is never a bundler swap
&lt;/h2&gt;

&lt;p&gt;Here's the thing nobody tells you about CRA: &lt;strong&gt;CRA is not a bundler. ** CRA is an API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over five years, a codebase doesn't just &lt;em&gt;use&lt;/em&gt; webpack. It absorbs webpack's semantics into its source. &lt;code&gt;process.env&lt;/code&gt;, &lt;code&gt;%PUBLIC_URL%&lt;/code&gt;, &lt;code&gt;import { ReactComponent as Icon }&lt;/code&gt;, JSX inside &lt;code&gt;.js&lt;/code&gt; files, implicit Node globals in browser code, automatic Babel transpilation of every CommonJS dependency you ever installed.&lt;/p&gt;

&lt;p&gt;None of those is React features. All of them are load-bearing.&lt;/p&gt;

&lt;p&gt;So the first thing I did was &lt;em&gt;not&lt;/em&gt; write &lt;code&gt;vite.config.mjs&lt;/code&gt;. I ran a survey: every place the source assumed that only webpack could satisfy. That list became the actual scope of work — and it was about ten times larger than the config file.&lt;/p&gt;

&lt;p&gt;The config file, for the record, is 23 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;react&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@vitejs/plugin-react-swc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;svgr&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vite-plugin-svgr&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;react&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;svgr&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;exportAsDefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="na"&gt;envDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;environments&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;outDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;build&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;&lt;code&gt;build.outDir: 'build'&lt;/code&gt; and &lt;code&gt;port: 3000&lt;/code&gt; are deliberate. Vite defaults to &lt;code&gt;dist&lt;/code&gt; and &lt;code&gt;5173&lt;/code&gt;. Our GitLab CI jobs, Docker image, and deploy scripts all expected &lt;code&gt;build/&lt;/code&gt; on &lt;code&gt;:3000&lt;/code&gt;. Changing the bundler &lt;em&gt;and&lt;/em&gt; the deployment contract in the same MR is how you end up with a broken pipeline you can't attribute to anything. &lt;strong&gt;Absorb the churn in config, not in infrastructure.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Constraint one: React 16 stays
&lt;/h2&gt;

&lt;p&gt;The tempting move here is to bundle the migration with a React 18 upgrade. &lt;code&gt;ReactDOM.render&lt;/code&gt; is deprecated, &lt;code&gt;createRoot&lt;/code&gt; is right there, and you're already in the file.&lt;/p&gt;

&lt;p&gt;I didn't. We stayed on &lt;code&gt;react@16.14.0&lt;/code&gt;, &lt;code&gt;ReactDOM.render&lt;/code&gt;, and &lt;code&gt;react-router-dom@4.3.1&lt;/code&gt;. All of it.&lt;/p&gt;

&lt;p&gt;The reasoning: a build migration has &lt;em&gt;no user-visible intent&lt;/em&gt;. If nothing changes for the user, then &lt;strong&gt;every&lt;/strong&gt; visual or behavioural difference is a regression, full stop. That is an incredibly powerful invariant to review against — the reviewer can just diff the two environments side by side, and any delta is a bug. Fold a React 18 upgrade in, and you lose it: now some differences are expected, some aren't, and every discussion becomes an argument about which is which.&lt;/p&gt;

&lt;p&gt;The MR is labelled a breaking change for &lt;em&gt;tooling&lt;/em&gt;. It should be a no-op for &lt;em&gt;behaviour&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 1: &lt;code&gt;type: module&lt;/code&gt; and the file-extension domino
&lt;/h2&gt;

&lt;p&gt;Adding &lt;code&gt;"type": "module"&lt;/code&gt; to &lt;code&gt;package.json&lt;/code&gt; is one line. It cost a day.&lt;/p&gt;

&lt;p&gt;Node now treats every &lt;code&gt;.js&lt;/code&gt; file in the project as ESM. Which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;.eslintrc.js&lt;/code&gt; used &lt;code&gt;module.exports&lt;/code&gt; → &lt;strong&gt;crashes&lt;/strong&gt;. Renamed to &lt;code&gt;.eslintrc.cjs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The Vite config had to be &lt;code&gt;.mjs&lt;/code&gt; to be unambiguous.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.lintstagedrc.js&lt;/code&gt; had to be written as &lt;code&gt;export default { ... }&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then esbuild's rule hit: &lt;strong&gt;esbuild will not parse JSX inside a &lt;code&gt;.js&lt;/code&gt; file.&lt;/strong&gt; Babel happily did. esbuild refuses, by design, because the &lt;code&gt;.js&lt;/code&gt; extension makes no promise about JSX and guessing costs parse time.&lt;/p&gt;

&lt;p&gt;That forced a wave of renames — &lt;code&gt;src/app.js&lt;/code&gt; → &lt;code&gt;app.jsx&lt;/code&gt;, &lt;code&gt;src/index.js&lt;/code&gt; → &lt;code&gt;index.jsx&lt;/code&gt;, &lt;code&gt;src/service/apollo_wrapper.js&lt;/code&gt; → &lt;code&gt;.jsx&lt;/code&gt; — and one genuine refactor. &lt;code&gt;src/constants.js&lt;/code&gt; was a data module that had quietly grown JSX in it: nav configs, ingredient lists, session-OS maps, all carrying &lt;code&gt;icon: &amp;lt;AacIngredientIcon /&amp;gt;&lt;/code&gt;. I split it into a new &lt;code&gt;src/constant.jsx&lt;/code&gt; holding everything JSX-bearing, leaving &lt;code&gt;constants.js&lt;/code&gt; as pure data.&lt;/p&gt;

&lt;p&gt;That split turned out to be worth doing on its own merits — it stopped a 200-line render-bearing module from being imported by things that only wanted string constants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; extension discipline feels like bureaucracy under Babel. Under esbuild it's a type system. It's a better default.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 2: &lt;code&gt;process.env&lt;/code&gt; doesn't exist in the browser
&lt;/h2&gt;

&lt;p&gt;CRA injected &lt;code&gt;process.env&lt;/code&gt; into browser code. Vite doesn't — it exposes &lt;code&gt;import.meta.env&lt;/code&gt;, and only for variables prefixed &lt;code&gt;VITE_&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;REACT_APP_*&lt;/code&gt; reference had to move. &lt;code&gt;src/config.js&lt;/code&gt; alone was a solid block of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- const cloudFunctionsEnvironment = process.env.REACT_APP_API_ENV;
- export const serverProxyURL   = process.env.REACT_APP_PROXY_URL;
- export const redirectURL      = process.env.REACT_APP_REDIRECT_URL;
&lt;/span&gt;&lt;span class="gi"&gt;+ const cloudFunctionsEnvironment = import.meta.env.VITE_API_ENV;
+ export const serverProxyURL   = import.meta.env.VITE_PROXY_URL;
+ export const redirectURL      = import.meta.env.VITE_REDIRECT_URL;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…plus the entire cookie-name map, which derives ten keys off &lt;code&gt;VITE_COOKIE_PREFIX&lt;/code&gt;, and the OAuth token-refresh config in &lt;code&gt;helper.jsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The subtle part isn't the rename. It's that &lt;code&gt;process.env.FOO&lt;/code&gt; on an unset variable silently yields &lt;code&gt;undefined&lt;/code&gt;, and you get a broken URL at runtime. There is no build-time error. A single missed rename ships a &lt;code&gt;undefinedaccess_token&lt;/code&gt; cookie to production, and you find out from a support ticket.&lt;/p&gt;

&lt;p&gt;Two things de-risked this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A grep-and-verify pass, not a find-and-replace pass.&lt;/strong&gt; Every hit reviewed individually, because some &lt;code&gt;process.env&lt;/code&gt; references were in Node-side config that should &lt;em&gt;stay&lt;/em&gt; &lt;code&gt;process.env&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consolidating the env files.&lt;/strong&gt; CRA's &lt;code&gt;env-cmd&lt;/code&gt; setup had &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.env.local&lt;/code&gt;, &lt;code&gt;.env.localV2&lt;/code&gt;, &lt;code&gt;.env.development&lt;/code&gt;, &lt;code&gt;.env.staging&lt;/code&gt; scattered at the repo root, selected by npm script. I moved them into &lt;code&gt;environments/&lt;/code&gt; and let Vite's native mode flag drive selection:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"local"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"vite --mode localhost"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build:dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="s2"&gt;"vite build --mode development"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build:stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build --mode staging"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;envDir: 'environments'&lt;/code&gt; in the config. &lt;code&gt;env-cmd&lt;/code&gt; deleted. One mechanism instead of two, and the mode name is now visible in the command you actually type.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;index.html&lt;/code&gt; also moved from &lt;code&gt;public/&lt;/code&gt; to the project root — Vite treats it as the build entry, not a template — and &lt;code&gt;%PUBLIC_URL%&lt;/code&gt; placeholders became plain absolute paths.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 3: the white screen with no stack trace
&lt;/h2&gt;

&lt;p&gt;First successful build. Open the app. White screen.&lt;/p&gt;

&lt;p&gt;Console: &lt;code&gt;Uncaught ReferenceError: global is not defined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is the migration bug that eats an afternoon, because the error points at bundled vendor code and tells you nothing about &lt;em&gt;why&lt;/em&gt; it's there.&lt;/p&gt;

&lt;p&gt;The cause: &lt;code&gt;global&lt;/code&gt; is a &lt;strong&gt;Node&lt;/strong&gt; identifier. It does not exist in browsers. Webpack, being a Node-first bundler, silently shimmed it for every CommonJS dependency that reached for it. Vite — an ESM-first, browser-first bundler — does not.&lt;/p&gt;

&lt;p&gt;Our dependency tree was old enough to be full of candidates. &lt;code&gt;socket.io-client@2.3.0&lt;/code&gt; and &lt;code&gt;draft-js@0.10.5&lt;/code&gt; are both from an era when "just assume &lt;code&gt;global&lt;/code&gt;" was normal library code. Because the reference is &lt;em&gt;inside a dependency&lt;/em&gt;, you can't fix it in your own source.&lt;/p&gt;

&lt;p&gt;The fix I shipped is deliberately blunt — a shim in &lt;code&gt;index.html&lt;/code&gt;, before the module entry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"./src/index.jsx"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this and not &lt;code&gt;define: { global: 'window' }&lt;/code&gt; in the Vite config?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;define&lt;/code&gt; does a raw textual substitution across every module at build time. That's a shotgun: it rewrites the identifier &lt;code&gt;global&lt;/code&gt; everywhere, including inside strings and comments in dependency code, and it behaves differently in dev vs build. I hit a real inconsistency between &lt;code&gt;vite dev&lt;/code&gt; and &lt;code&gt;vite build&lt;/code&gt; output while testing it.&lt;/p&gt;

&lt;p&gt;The HTML shim is honest about what it is: one global, defined once, in the document, visible to anyone who opens &lt;code&gt;index.html&lt;/code&gt;. It's not elegant. It's &lt;em&gt;legible&lt;/em&gt;, and for a shim whose entire purpose is to be deleted the day those two dependencies get upgraded, legibility beats elegance.&lt;/p&gt;

&lt;p&gt;That's the trade-off I'd defend in review, and did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 4: five dependencies that couldn't make the jump
&lt;/h2&gt;

&lt;p&gt;This is where the migration stopped being about the build and started being about the product.&lt;/p&gt;

&lt;p&gt;Some packages simply don't survive contact with ESM + esbuild — they ship CommonJS-only, depend on Node builtins, or were abandoned before ESM mattered. Five had to go:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;Replaced with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reactjs-localstorage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;a 15-line &lt;code&gt;storageWrapper&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rc-time-picker-date-fns&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;native &lt;code&gt;&amp;lt;input type="time"&amp;gt;&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;react-sortable-hoc&lt;/code&gt; + &lt;code&gt;array-move&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;plain function-returned JSX&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;react-numeric-input&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;native numeric input&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node-sass@4.14.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sass@1.69.7&lt;/code&gt; (Dart Sass)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;node-sass&lt;/code&gt; deserves a footnote: it's a native binding compiled against a specific Node ABI. It's the reason the project was pinned to an old Node in the first place. Deleting it is what let CI move from &lt;code&gt;node:14.16.1&lt;/code&gt; to &lt;code&gt;node:16.20.2&lt;/code&gt; — which Vite 4 requires anyway. One dependency was holding the entire toolchain hostage.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;reactjs-localstorage&lt;/code&gt; was the easy one. The library's whole surface is &lt;code&gt;get&lt;/code&gt;/&lt;code&gt;set&lt;/code&gt;/&lt;code&gt;getObject&lt;/code&gt;/&lt;code&gt;setObject&lt;/code&gt;, and &lt;code&gt;helper.jsx&lt;/code&gt; had grown &lt;em&gt;nine&lt;/em&gt; thin wrappers around it (&lt;code&gt;StoreCookie&lt;/code&gt;, &lt;code&gt;GetDcIdToken&lt;/code&gt;, &lt;code&gt;StoreLastLoggedInUser&lt;/code&gt;, &lt;code&gt;DeleteIdTokenCookie&lt;/code&gt;…) most of which nothing called any more. I replaced the package and the nine wrappers with one object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&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;storageWrapper&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&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;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&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="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;defaultValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;clear&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;Net: &lt;strong&gt;one dependency and ~40 lines of dead abstraction deleted&lt;/strong&gt;, and the call sites got clearer — &lt;code&gt;GetStorePermission()&lt;/code&gt; now visibly does &lt;code&gt;JSON.parse(storageWrapper.get('DC_PERMISSION', []))&lt;/code&gt; instead of hiding the parse inside a library method named &lt;code&gt;getObject&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;react-sortable-hoc&lt;/code&gt; was the interesting one. The reviewer's call was not to find a replacement drag-and-drop library at all — the sortable behaviour on the jobs dashboard wasn't actually used — and instead unwrap &lt;code&gt;SortableContainer&lt;/code&gt; / &lt;code&gt;SortableItem&lt;/code&gt; back into plain functions returning JSX, preserving the code structure so the diff stayed reviewable. Removing a feature is a legitimate answer to "this dependency won't build." It's just the one nobody suggests.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 5: &lt;code&gt;ReactComponent&lt;/code&gt; was never a real thing
&lt;/h2&gt;

&lt;p&gt;CRA let you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ReactComponent&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;ClockIcon&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./clock.svg&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;That named export is a &lt;strong&gt;webpack loader convention&lt;/strong&gt;. It is not part of any standard. Vite has no idea what you mean.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vite-plugin-svgr&lt;/code&gt; covers the common case, and I added it. But the reviewer caught a genuine inconsistency: I was using svgr in some files and &lt;code&gt;&amp;lt;img src={...} /&amp;gt;&lt;/code&gt; in others, with no rule for which. That's the kind of thing that looks like a nit and is actually a maintenance tax — the next person has to read the imports to know what an SVG &lt;em&gt;is&lt;/em&gt; in this codebase.&lt;/p&gt;

&lt;p&gt;The rule we landed on, and the reason for it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static, decorative SVGs&lt;/strong&gt; → &lt;code&gt;&amp;lt;img src={Icon} /&amp;gt;&lt;/code&gt;. Cheap, cacheable, no parse cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SVGs that need to respond to the theme&lt;/strong&gt; → hand-authored inline JSX components in &lt;code&gt;src/images/svg_helper.jsx&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That second category is the interesting one. DreamNet has a dark mode driven by CSS custom properties. An &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; is an opaque replaced element — &lt;strong&gt;it cannot inherit &lt;code&gt;--aurora-colors-text&lt;/code&gt; from the page.&lt;/strong&gt; So the dropzone's upload icons, which need to be visible in both themes, had to become real DOM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&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;MobileIcon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;viewBox&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"0 0 27 43"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;rect&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"var(--aurora-colors-text)"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"var(--aurora-colors-text)"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;path&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"var(--aurora-colors-primary-300)"&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&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;The two &lt;code&gt;.svg&lt;/code&gt; files they replaced were deleted outright. A pile of the review churn in this MR — &lt;code&gt;fix: download icon dark mode issue&lt;/code&gt;, &lt;code&gt;fix: calendar icon colour in dark mode&lt;/code&gt;, &lt;code&gt;fix: Edit-icon in collection page in dark mode&lt;/code&gt;, &lt;code&gt;fix: dark mode empty table text issue&lt;/code&gt; — is this exact class of bug, found one screen at a time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 6: Fast Refresh has opinions about your exports
&lt;/h2&gt;

&lt;p&gt;Two commits in this MR read &lt;code&gt;resolve: resolve warning of fast refresh component&lt;/code&gt; and &lt;code&gt;fix: remove warning of fastRefresh&lt;/code&gt;, and I want to explain them because the fix looks like a no-op.&lt;/p&gt;

&lt;p&gt;React Fast Refresh can only hot-reload a module if it can statically prove the module's exports are all components. An anonymous expression export defeats that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fast Refresh can't track this&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;withToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Dropzone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// It can track this&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DropZoneComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;withToast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Dropzone&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;DropZoneComponent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identical at runtime. Completely different for HMR: the first form silently forces a full page reload on every edit, which quietly deletes most of the developer-experience benefit you migrated for.&lt;/p&gt;

&lt;p&gt;We wired &lt;code&gt;eslint-plugin-react-refresh&lt;/code&gt; into the config so this is enforced rather than remembered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-refresh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="nx"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-refresh/only-export-components&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&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="na"&gt;allowConstantExport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;&lt;code&gt;allowConstantExport: true&lt;/code&gt; matters — Vite supports mixing constant exports with component exports, and without it the rule fires constantly on legitimate files.&lt;/p&gt;

&lt;p&gt;The same file also turned off &lt;code&gt;react/react-in-jsx-scope&lt;/code&gt; and &lt;code&gt;react/jsx-uses-react&lt;/code&gt;, because SWC's automatic JSX runtime means &lt;code&gt;import React from 'react'&lt;/code&gt; is no longer required. That's why dozens of files in this diff lose their React import — it's not cosmetic, it's the transform contract changing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 7: the chart.js version that went backwards
&lt;/h2&gt;

&lt;p&gt;One line in &lt;code&gt;package.json&lt;/code&gt; got a direct challenge in review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Reviewer:&lt;/strong&gt; Any specific reason to go backwards? Is it a compatibility issue with Node?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;chart.js&lt;/code&gt; went &lt;strong&gt;3.9.1 → 3.8.0&lt;/strong&gt;. Downgrading a dependency during a modernisation MR looks like giving up, and it deserved the question.&lt;/p&gt;

&lt;p&gt;The answer: at medium and small viewports, the Doughnut chart's legend swatches stopped picking up the light/dark theme colour. I bisected across chart.js patch versions, confirmed 3.8.0 was clean, and 3.9.1 was not, and found the matching upstream report — &lt;a href="https://github.com/chartjs/Chart.js/issues/10372" rel="noopener noreferrer"&gt;chartjs/Chart.js#10372&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before&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%2Fzwi3rhwfse0zxcpfnmjk.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%2Fzwi3rhwfse0zxcpfnmjk.png" alt=" " width="799" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After&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%2Fzot8kl3qrny8sj06zz9g.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%2Fzot8kl3qrny8sj06zz9g.png" alt=" " width="800" height="443"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pinning back to 3.8.0 with a link to the upstream issue is the correct call here, and I'd make it again. The alternative was writing a custom legend renderer to work around a known upstream regression, in an MR that already had 166 changed files and a hard invariant of "nothing user-visible changes." &lt;strong&gt;Scope discipline is a technical decision, not a project-management one.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Chapter 8: the reviewer who wanted zero
&lt;/h2&gt;

&lt;p&gt;The single most valuable thing that happened to this MR was this comment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;After I ran &lt;code&gt;npm run eslint-fix&lt;/code&gt;, I still see 90 warnings!!!! I think I have asked you to resolve it. Here is the patch I did in one of the files to resolve two of these. You can apply the patch with &lt;code&gt;git apply mypatch.patch&lt;/code&gt; […] I want 0 errors/warnings here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Attached: an actual &lt;code&gt;.patch&lt;/code&gt; file demonstrating the fix pattern.&lt;/p&gt;

&lt;p&gt;Ninety warnings is a normal number for a five-year-old codebase, and the reflexive response is "those are pre-existing, out of scope." That would have been wrong, and here's the concrete reason why.&lt;/p&gt;

&lt;p&gt;While clearing them, ESLint surfaced this in &lt;code&gt;event_details.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;LocationOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prototype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// ← prototype&lt;/span&gt;
  &lt;span class="na"&gt;locations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PropTypes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayOf&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;&lt;code&gt;prototype&lt;/code&gt;, not &lt;code&gt;propTypes&lt;/code&gt;. A typo. Which means &lt;strong&gt;that component had never validated its props — the runtime type checks had been silently disabled since the day the line was written.&lt;/strong&gt; No error, no warning, no symptom. Just a safety net that was never actually attached.&lt;/p&gt;

&lt;p&gt;It's now &lt;code&gt;propTypes&lt;/code&gt;, with a matching &lt;code&gt;defaultProps&lt;/code&gt;. That's a real bug, in production, found by a lint pass that a lazier version of me would have argued was out of scope.&lt;/p&gt;

&lt;p&gt;The lint gate now runs on every commit rather than on trust — husky's &lt;code&gt;package.json&lt;/code&gt; &lt;code&gt;hooks&lt;/code&gt; block replaced with a &lt;code&gt;.husky/pre-commit&lt;/code&gt; script driving &lt;code&gt;lint-staged&lt;/code&gt;, so only touched files get checked and the hook stays fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// .lintstagedrc.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*.{js,jsx,ts,tsx}&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;eslint --fix&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;h2&gt;
  
  
  How this was actually validated (the honest version)
&lt;/h2&gt;

&lt;p&gt;DreamNet has no automated test suite. &lt;code&gt;react-scripts test&lt;/code&gt; existed in &lt;code&gt;package.json&lt;/code&gt; and ran nothing. I'm not going to dress that up.&lt;/p&gt;

&lt;p&gt;So validation was three things, and I'd argue the first one is underrated:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The invariant did the work.&lt;/strong&gt; Because we deliberately changed &lt;em&gt;no&lt;/em&gt; behaviour, "correct" was defined as "byte-for-byte identical to &lt;code&gt;dev&lt;/code&gt;, on every screen." That turns verification from a judgement call into a comparison. The reviewer could — and repeatedly did — post side-by-side screenshots of &lt;code&gt;dev&lt;/code&gt; vs the branch and say "this spacing is different," and there was no argument to have. Six weeks of review comments are almost entirely of that form.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The migration was kept alive against a moving target.&lt;/strong&gt; &lt;code&gt;dev&lt;/code&gt; did not freeze for 70 days. The MR absorbed merges of 31, 23, 15, 18, 35, 57 and 17 commits from &lt;code&gt;dev&lt;/code&gt; over its lifetime. That was a deliberate choice over rebasing at the end: every merge is a small, attributable conflict resolution. One giant rebase at the end is an unreviewable mess where a subtle behaviour change hides indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The deploy chain proved itself.&lt;/strong&gt; &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; moved to &lt;code&gt;node:16.20.2&lt;/code&gt; across the lint, dev, stage and prod jobs, and the build was then exercised through the real pipeline — dev, then stage, then production — rather than trusted from a local &lt;code&gt;npm run build&lt;/code&gt;. Vite's dev server and its Rollup production build are &lt;em&gt;different code paths&lt;/em&gt;; a dev-only smoke test proves less than it feels like it does. &lt;code&gt;react-numeric-input&lt;/code&gt; in particular only failed at &lt;strong&gt;build&lt;/strong&gt; time, not in dev — commit &lt;code&gt;fix: remove react-numeric-input package and resolve breaking changes in build&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Merged 4 January 2024. Still the build system today, now serving dev, stage, production and a Hong Kong deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What it bought
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;TODO before publishing:&lt;/strong&gt; fill these in from your own machine. Run &lt;code&gt;time npm run build&lt;/code&gt; on the merge commit &lt;code&gt;f275733&lt;/code&gt; and on its parent &lt;code&gt;d46df1c&lt;/code&gt;, and time a cold &lt;code&gt;vite&lt;/code&gt;/&lt;code&gt;react-scripts start&lt;/code&gt; for both. Don't publish estimates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;CRA + Webpack&lt;/th&gt;
&lt;th&gt;Vite + SWC&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold dev server start&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HMR / file change reflected&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production build (CI)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node version required&lt;/td&gt;
&lt;td&gt;14.16.1&lt;/td&gt;
&lt;td&gt;16.20.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct dependencies&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;td&gt;26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build-tool devDependencies&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dependency numbers are the ones I care about most, honestly. &lt;code&gt;react-scripts@3.0.1&lt;/code&gt; was a single line in &lt;code&gt;package.json&lt;/code&gt; that pulled in Babel, webpack, PostCSS, ESLint, Jest and their entire transitive universe, all pinned to 2019, all unupgradeable independently. Replacing it with &lt;code&gt;vite&lt;/code&gt; + &lt;code&gt;@vitejs/plugin-react-swc&lt;/code&gt; + &lt;code&gt;vite-plugin-svgr&lt;/code&gt; means every one of those concerns is now a package we can move on its own schedule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Migrate the build tool, not the app.&lt;/strong&gt; Every change that isn't strictly required by the new bundler is a change that makes "is this a regression?" unanswerable. Staying on React 16 wasn't conservatism — it was what made the MR reviewable at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The dependencies are the migration.&lt;/strong&gt; The config was 23 lines and one afternoon. Five incompatible packages were ten weeks. Audit &lt;code&gt;package.json&lt;/code&gt; against ESM compatibility &lt;em&gt;before&lt;/em&gt; you estimate, not after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Removing a library is a domain-modelling exercise in disguise.&lt;/strong&gt; &lt;code&gt;rc-time-picker-date-fns&lt;/code&gt; didn't just render a widget — it imposed a data model where time and date were fused into one &lt;code&gt;Date&lt;/code&gt;. Replacing it forced us to name the operation we'd always been doing badly (&lt;code&gt;compareTimes&lt;/code&gt;), which is how the "can't schedule a future event in the afternoon" bug finally surfaced.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;global is not defined&lt;/code&gt; means a dependency is older than ESM.&lt;/strong&gt; Shim it visibly in &lt;code&gt;index.html&lt;/code&gt; rather than invisibly via &lt;code&gt;define&lt;/code&gt;. Leave a shim you can find and delete later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Take the lint warnings seriously, especially the pre-existing ones.&lt;/strong&gt; &lt;code&gt;prototype&lt;/code&gt; instead of &lt;code&gt;propTypes&lt;/code&gt; had silently disabled runtime prop validation on a production component. Ninety warnings hid exactly one real bug. That ratio is worth it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Merge from main constantly on a long-lived branch.&lt;/strong&gt; Seven merges from &lt;code&gt;dev&lt;/code&gt; over 70 days, each a small resolvable conflict. The alternative is one enormous rebase where a behavioural change can hide with nobody noticing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. No test suite isn't an excuse — it's a constraint you design around.&lt;/strong&gt; Ours was replaced by a hard invariant ("nothing user-visible changes") plus a reviewer with screenshots. Not as good as tests. Considerably better than vibes.&lt;/p&gt;




&lt;p&gt;Thanks also to the open-source projects that made the destination worth the trip: &lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt;, &lt;a href="https://swc.rs/" rel="noopener noreferrer"&gt;SWC&lt;/a&gt;, &lt;a href="https://github.com/vitejs/vite-plugin-react-swc" rel="noopener noreferrer"&gt;&lt;code&gt;@vitejs/plugin-react-swc&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/pd4d10/vite-plugin-svgr" rel="noopener noreferrer"&gt;&lt;code&gt;vite-plugin-svgr&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://sass-lang.com/dart-sass/" rel="noopener noreferrer"&gt;Dart Sass&lt;/a&gt;, and &lt;a href="https://www.chartjs.org/" rel="noopener noreferrer"&gt;Chart.js&lt;/a&gt; — whose maintainers had already documented &lt;a href="https://github.com/chartjs/Chart.js/issues/10372" rel="noopener noreferrer"&gt;#10372&lt;/a&gt; by the time I went looking, which saved me a day.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you've done a CRA → Vite migration on a codebase older than three years, I'd genuinely like to hear which dependency was your &lt;code&gt;rc-time-picker&lt;/code&gt;. Everyone has one.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>The 2021 Sentry bug that made every Element Desktop crash unreadable</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Sat, 22 Aug 2026 17:34:17 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/smashing-bugs-strengthening-systems-accessibility-observability-in-element-web-1985</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/smashing-bugs-strengthening-systems-accessibility-observability-in-element-web-1985</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;blockquote&gt;
&lt;p&gt;On 22 November 2021, &lt;a href="https://github.com/novocaine" rel="noopener noreferrer"&gt;@novocaine&lt;/a&gt; opened &lt;a href="https://github.com/element-hq/element-web/issues/32242" rel="noopener noreferrer"&gt;issue #32242&lt;/a&gt; on Element Web. He diagnosed it correctly, named the exact Sentry integration that would fix it, and left one open question he couldn't answer.&lt;/p&gt;

&lt;p&gt;It sat there for &lt;strong&gt;four years and nine months&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On 12 August 2026 I shipped the fix — and answered his question.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;&lt;a href="https://github.com/element-hq/element-web" rel="noopener noreferrer"&gt;Element Web&lt;/a&gt; is the flagship &lt;a href="https://matrix.org/" rel="noopener noreferrer"&gt;Matrix&lt;/a&gt; client — secure, decentralised, end-to-end-encrypted collaboration. 13.4k stars, 2.7k forks, ~3,600 open issues, AGPL-3.0. It ships as a web app and, packaged with Electron, as &lt;strong&gt;Element Desktop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The user base matters here. Element is deployed by governments, hospitals, universities and armed forces — organisations that choose Matrix precisely because they can't hand their communications to a US SaaS vendor. Many of those deployments are &lt;strong&gt;desktop-first&lt;/strong&gt;. Which makes it a genuine problem that, for nearly five years, Element Desktop was the one platform whose crash reports the maintainers couldn't actually read.&lt;/p&gt;

&lt;p&gt;I contributed two merged fixes. This post is mostly about the Sentry one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The main event: Element Desktop's crash reports were minified garbage
&lt;/h3&gt;

&lt;p&gt;Element Desktop doesn't serve the app over &lt;code&gt;https://&lt;/code&gt;. Electron serves it from a custom protocol, &lt;code&gt;vector://vector/webapp&lt;/code&gt;. So every stack frame Sentry captured on desktop looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;vector&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//vector/webapp/bundles/abc123/bundle.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sentry's source-map resolver has no idea what &lt;code&gt;vector://&lt;/code&gt; is. It can't match that path against the uploaded artifacts for the release. Two things followed, and the second is the expensive one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Desktop crashes arrived unsymbolicated.&lt;/strong&gt; Minified function names, no original file, no real line numbers. &lt;code&gt;t.default.a is not a function&lt;/code&gt; at &lt;code&gt;bundle.js:1:284915&lt;/code&gt;. Technically a crash report; practically a shrug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Desktop and web crashes stopped grouping.&lt;/strong&gt; Sentry groups issues partly by stack frame filenames. Web reported &lt;code&gt;https://app.element.io/bundles/abc123/bundle.js&lt;/code&gt;; desktop reported &lt;code&gt;vector://vector/webapp/bundles/abc123/bundle.js&lt;/code&gt;. Same crash, same release, same line of source — &lt;strong&gt;two separate issues in Sentry.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That second one quietly corrupts your prioritisation. An issue hitting 600 users looks like two issues hitting 400 and 200. One of them is legible and one isn't, so the legible one gets triaged and the illegible one gets ignored — and the illegible one is the platform your enterprise customers are running.&lt;/p&gt;

&lt;p&gt;novocaine spotted all of this in 2021. From the original report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sentry errors emitted by Element Desktop aren't being source-mapped, which means they aren't being grouped with the same exceptions on Element Web, and are harder to debug.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He even linked Sentry's RewriteFrames docs and said that's what should be used. Then he added the part that I think is why nothing happened for five years:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The bundle hash is also in the pathname, and it doesn't match web on the same release (&lt;strong&gt;not sure why this is&lt;/strong&gt;) … I'm interested in why 2 is happening at all, it may lead to issues later on&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An unanswered question inside a bug report is a very effective blocker. The issue got labelled &lt;code&gt;S-Minor&lt;/code&gt; and &lt;code&gt;O-Uncommon&lt;/code&gt; and went to sleep.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other one: the high-contrast theme had no contrast
&lt;/h3&gt;

&lt;p&gt;Separately, &lt;a href="https://github.com/element-hq/element-web/issues/34213" rel="noopener noreferrer"&gt;#34213&lt;/a&gt;: in Element's Light High Contrast theme, hovering a Spotlight search result rendered light grey text on a light grey background. Measured contrast ratio &lt;strong&gt;1.38:1&lt;/strong&gt;; WCAG AA requires &lt;strong&gt;4.5:1&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;em&gt;high-contrast theme&lt;/em&gt; was the illegible one. For users who enable high contrast because they need it, "Explore Public Rooms" was a blank row. More on that further down.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PR&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/element-hq/element-web/pull/34667" rel="noopener noreferrer"&gt;element-hq/element-web#34667&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;Merged&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sentry &lt;code&gt;rewriteFramesIntegration&lt;/code&gt; for Element Desktop — closes a 2021 issue&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/element-hq/element-web/pull/34465" rel="noopener noreferrer"&gt;element-hq/element-web#34465&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;✅ Merged&lt;/td&gt;
&lt;td&gt;High-contrast Spotlight legibility + cross-theme axe regression suite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/element-hq/element-web/pull/34468" rel="noopener noreferrer"&gt;element-hq/element-web#34468&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;❌ Closed by maintainers&lt;/td&gt;
&lt;td&gt;Submenu hover-out dismissal — included because being rejected is instructive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(Taking this section out of template order deliberately — it's the substance of the submission.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sentry tools used
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error Monitoring&lt;/strong&gt; — the integration pipeline itself (&lt;code&gt;Sentry.init&lt;/code&gt;, integration list, &lt;code&gt;processEvent&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Maps / Releases &amp;amp; Artifacts&lt;/strong&gt; — artifact path matching, which is what was actually broken&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue Grouping&lt;/strong&gt; — restoring cross-platform grouping between desktop and web&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A live Sentry project&lt;/strong&gt; — used as the verification harness, firing an identical fabricated crash through both configs and diffing the resulting issues&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not claiming Session Replay, Distributed Tracing or Seer. I didn't use them. What I did was repair the layer everything else in Sentry sits on: if your frames don't resolve, nothing downstream of them is trustworthy either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Root cause: an opt-out that silently freezes in time
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;apps/web/src/sentry.ts&lt;/code&gt; initialises Sentry with &lt;code&gt;defaultIntegrations: false&lt;/code&gt; and a hand-picked list:&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;integrations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inboundFiltersIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;functionToStringIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;breadcrumbsIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;httpContextIntegration&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dedupeIntegration&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;&lt;strong&gt;&lt;code&gt;defaultIntegrations: false&lt;/code&gt; is the actual root cause&lt;/strong&gt;, and it's worth dwelling on because it's a pattern lots of apps have.&lt;/p&gt;

&lt;p&gt;It's chosen for good reasons — bundle size, no surprise behaviour, explicit is better than implicit. But it converts "you get sensible defaults" into "you get exactly this list, forever." When the SDK ships a new integration, or when your app grows a deployment target that would have needed one, nothing tells you. There is no warning, no deprecation, no failing test. The list just quietly stops describing what you need.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rewriteFramesIntegration&lt;/code&gt; was never on that list. So frames went to Sentry exactly as the Electron renderer produced them, custom protocol and all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;Eight lines:&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="c1"&gt;// Element Desktop serves the app from the custom `vector://vector/webapp` origin rather&lt;/span&gt;
&lt;span class="c1"&gt;// than a normal web origin, so its stack frames don't match Element Web's and end up&lt;/span&gt;
&lt;span class="c1"&gt;// ungrouped and unsymbolicated. Normalize both to the same relative form so desktop&lt;/span&gt;
&lt;span class="c1"&gt;// crashes group with, and source-map against, the same release as the web app.&lt;/span&gt;
&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewriteFramesIntegration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vector://vector/webapp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app://&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;&lt;code&gt;app://&lt;/code&gt; is &lt;a href="https://docs.sentry.io/platforms/javascript/configuration/integrations/rewriteframes/" rel="noopener noreferrer"&gt;Sentry's documented convention&lt;/a&gt; for custom-origin apps — the pattern Sentry's own Electron guidance uses. It's a &lt;strong&gt;no-op for Element Web&lt;/strong&gt;, whose frames never start with that prefix, which is what makes this safe to ship globally rather than gating it behind a platform check.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two-character detail that would have shipped a fake fix
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;prefix: "app://"&lt;/code&gt;. Two slashes. Not three.&lt;/p&gt;

&lt;p&gt;This looks like bikeshedding. It isn't, and it's the part of this PR I'd defend hardest.&lt;/p&gt;

&lt;p&gt;Here's what &lt;code&gt;rewriteFrames&lt;/code&gt; actually does to a filename. It strips &lt;code&gt;root&lt;/code&gt; off the front:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vector://vector/webapp/bundles/abc123/bundle.js
─────── root ────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;leaving &lt;code&gt;/bundles/abc123/bundle.js&lt;/code&gt; — &lt;strong&gt;with the leading slash still attached&lt;/strong&gt;. Then it prepends &lt;code&gt;prefix&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;prefix: "app://"&lt;/code&gt; gives you the canonical &lt;code&gt;app:///bundles/abc123/bundle.js&lt;/code&gt;. But the string &lt;code&gt;app:///&lt;/code&gt; is what you &lt;em&gt;see&lt;/em&gt; in every Sentry doc and every Stack Overflow answer, so writing it into the config is the natural mistake. Do that and you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;////bundles/abc123/bundle.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four slashes. And here's why that's genuinely dangerous rather than merely wrong: &lt;strong&gt;it fails completely silently.&lt;/strong&gt; Sentry accepts the event. The issue appears in your dashboard. The stack trace renders. The paths even look plausible at a glance. The only symptom is that artifacts still don't match — so you'd have closed a four-year-old issue, told the maintainers it was fixed, and swapped one broken path for a differently broken path that nobody would notice for months.&lt;/p&gt;

&lt;p&gt;There is no error message anywhere in that failure mode. The only defence is asserting on the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  So I tested the output, not the config
&lt;/h3&gt;

&lt;p&gt;Most tests of a Sentry integration check that it's wired up. Mine does that — and then does the thing that actually matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: mocked SDK — is the integration wired in?&lt;/strong&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="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sentry/browser&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;inboundFiltersIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;InboundFilters&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;functionToStringIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FunctionToString&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;breadcrumbsIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Breadcrumbs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;httpContextIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HttpContext&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;dedupeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dedupe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="na"&gt;rewriteFramesIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;mockReturnValue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RewriteFrames&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="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normalizes Element Desktop's vector:// stack frames so they group with Element Web&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&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;await&lt;/span&gt; &lt;span class="nf"&gt;initSentry&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;dsn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://examplePublicKey@o0.ingest.sentry.io/0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;test&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rewriteFramesIntegration&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveBeenCalledWith&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vector://vector/webapp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app://&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;integrations&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;mock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&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="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;integrations&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayContaining&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RewriteFrames&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;This guards against the &lt;code&gt;defaultIntegrations: false&lt;/code&gt; failure mode repeating — if someone edits that array later and drops the integration, this fails. Given that the original bug &lt;em&gt;was&lt;/em&gt; "something isn't in the integrations list," a regression test on the list itself is the right shape.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: the real SDK — what string actually comes out?&lt;/strong&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="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rewriteFramesIntegration output&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Uses the real Sentry SDK integration (not the mock above) to verify the actual rewritten&lt;/span&gt;
    &lt;span class="c1"&gt;// path, since it's easy to get the exact prefix/slash count wrong (e.g. `app:///` here would&lt;/span&gt;
    &lt;span class="c1"&gt;// double up with the leading slash already present in the frame, producing `app:////...`).&lt;/span&gt;
    &lt;span class="c1"&gt;// Relies on this test file's jsdom environment providing a real `window`, which is what makes&lt;/span&gt;
    &lt;span class="c1"&gt;// the SDK's browser root/prefix substitution apply in the first place.&lt;/span&gt;
    &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rewrites a vector:// frame to a clean app:/// path&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;RealSentry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requireActual&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Sentry&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@sentry/browser&lt;/span&gt;&lt;span class="dl"&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;integration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;RealSentry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewriteFramesIntegration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vector://vector/webapp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app://&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="na"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;stacktrace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;frames&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="na"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vector://vector/webapp/bundles/abc123/bundle.js&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="p"&gt;}&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;processed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;integration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;processEvent&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;processed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;stacktrace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frames&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;app:///bundles/abc123/bundle.js&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;jest.requireActual&lt;/code&gt; pulls the genuine &lt;code&gt;@sentry/browser&lt;/code&gt; past the mock, so this drives Sentry's real &lt;code&gt;processEvent&lt;/code&gt; pipeline with a hand-built event and asserts the &lt;strong&gt;exact&lt;/strong&gt; resulting filename — every slash of it.&lt;/p&gt;

&lt;p&gt;A mock-only suite would pass forever with &lt;code&gt;app:///&lt;/code&gt; sitting in the config. That's the whole argument for layer 2: &lt;strong&gt;test the observable output of the SDK, not your belief about what you configured.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The comment about jsdom is load-bearing too. &lt;code&gt;rewriteFrames&lt;/code&gt;' root/prefix substitution branch only applies when it detects a browser environment via a real &lt;code&gt;window&lt;/code&gt;. Run this under a node test environment and the assertion silently stops testing what you think — so the reason it works is written down for whoever touches it next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verified against a real Sentry project
&lt;/h3&gt;

&lt;p&gt;Unit tests prove the transform. They don't prove Sentry's ingestion agrees with you. So before opening the PR I sent an &lt;strong&gt;identical fabricated crash through both configs&lt;/strong&gt; — pre-fix and post-fix — into a live Sentry project, and compared the resulting issues side by side. Before/after screenshots are in the PR description.&lt;/p&gt;

&lt;p&gt;That step is what turned "this should work" into "I watched it work," and it's what I'd insist on for any change to error-reporting plumbing. Observability code is uniquely bad at telling you when it's broken, because its failure mode is &lt;em&gt;producing something that looks like data&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Answering the question that had been open since 2021
&lt;/h3&gt;

&lt;p&gt;I originally scoped novocaine's second point out. The PR said so explicitly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This PR addresses the path-prefix normalization only; the bundle-hash discrepancy is a separate, unrelated build/packaging question this PR doesn't investigate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then &lt;a href="https://github.com/t3chguy" rel="noopener noreferrer"&gt;@t3chguy&lt;/a&gt; dropped a hint in review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Back in '21 the build system was probably significantly different, nowadays the desktop build process literally downloads the release asset in the &lt;code&gt;fetch&lt;/code&gt; script&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I went and read &lt;code&gt;apps/desktop/scripts/fetch-package.ts&lt;/code&gt;. The modern desktop build pulls &lt;code&gt;element-&amp;lt;version&amp;gt;.tar.gz&lt;/code&gt; &lt;strong&gt;directly from the GitHub release&lt;/strong&gt; and repacks that exact tarball into &lt;code&gt;webapp.asar&lt;/code&gt;. It doesn't build its own bundle at all.&lt;/p&gt;

&lt;p&gt;Which means the bundle hash &lt;em&gt;can't&lt;/em&gt; diverge any more — the hash mismatch novocaine saw was an artifact of the 2021 pipeline, and that pipeline is gone.&lt;/p&gt;

&lt;p&gt;And that materially changes what this PR does. If the hashes still differed, stripping the &lt;code&gt;vector://&lt;/code&gt; prefix would only have fixed symbolication. Because they don't, once the prefix is normalised the remaining path — hash included — is &lt;strong&gt;identical&lt;/strong&gt; to the web app's frames for the same release. So the fix closes both halves of the original report:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&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;Desktop stack traces&lt;/td&gt;
&lt;td&gt;Minified, unsymbolicated&lt;/td&gt;
&lt;td&gt;Source-mapped against the release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same crash on desktop + web&lt;/td&gt;
&lt;td&gt;Two unrelated Sentry issues&lt;/td&gt;
&lt;td&gt;One issue, correct event volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Issue #32242&lt;/td&gt;
&lt;td&gt;Open since Nov 2021&lt;/td&gt;
&lt;td&gt;Closed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty minutes of reading a build script. That's the difference between closing a symptom and closing an issue — and between "here's a patch" and "here's the answer to the thing you asked in 2021."&lt;/p&gt;

&lt;p&gt;Merged via the merge queue on 12 August 2026, 46 checks passing.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;(Template section — the Sentry deep dive is above; this covers the second merged fix.)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The accessibility bug: a token that means the opposite thing in a different theme
&lt;/h3&gt;

&lt;p&gt;My first instinct on #34213 was a missing hover colour. Wrong — the colour was there and it was deliberate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.mx_SpotlightDialog_option&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="err"&gt;&amp;amp;:hover,&lt;/span&gt;
    &lt;span class="err"&gt;&amp;amp;[aria-selected="true"]&lt;/span&gt; &lt;span class="err"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;quinary-content&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;background&lt;/span&gt; &lt;span class="cp"&gt;!important&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c"&gt;/* ← the bug */&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;$background&lt;/code&gt; is the theme's canvas colour; &lt;code&gt;$quinary-content&lt;/code&gt; is the hover fill. In a &lt;strong&gt;dark&lt;/strong&gt; theme this is correct — the hover pill is light, &lt;code&gt;$background&lt;/code&gt; is dark, you get dark text on a light pill. In &lt;strong&gt;light-high-contrast&lt;/strong&gt;, &lt;code&gt;$background&lt;/code&gt; is white-ish. So is &lt;code&gt;$quinary-content&lt;/code&gt;. Same rule, inverted theme, invisible text.&lt;/p&gt;

&lt;p&gt;This is a semantic-token failure, not a typo, which is why it survived review for so long. &lt;code&gt;color: $background&lt;/code&gt; &lt;em&gt;reads&lt;/em&gt; as intentional. It's only wrong once you know what the token resolves to in that specific theme. The correct token for "the theme's readable text colour" is &lt;code&gt;$primary-content&lt;/code&gt; — changed in &lt;strong&gt;12 places&lt;/strong&gt; across search results, recently-viewed rows, the filter chip, the keyboard-shortcut hint and the generic dropdown menu.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;!important&lt;/code&gt; on nearly every one of those lines is its own signal. Rules that needed &lt;code&gt;!important&lt;/code&gt; were fought into place, and rules that were fought into place don't get re-derived when someone adds a new theme.&lt;/p&gt;

&lt;h3&gt;
  
  
  Widening the test caught a second, unreported bug
&lt;/h3&gt;

&lt;p&gt;My first test computed the WCAG ratio by hand in Playwright — 1.38:1 before, ≥4.5:1 after. t3chguy pushed me to use &lt;code&gt;axe-core&lt;/code&gt; instead, and he was right for a reason I'd underweighted: my version only checked the four elements I thought to check. It was a spot check wearing the costume of a general one.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://github.com/Half-Shot" rel="noopener noreferrer"&gt;@Half-Shot&lt;/a&gt; pushed further — don't test only high contrast:&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;for &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;theme&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;light-high-contrast&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="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; theme legibility`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Regression tests for https://github.com/element-hq/element-web/issues/34213&lt;/span&gt;
        &lt;span class="c1"&gt;// Hovered/selected Spotlight results must not render light text on a light&lt;/span&gt;
        &lt;span class="c1"&gt;// background (or vice versa) in any theme, not just high contrast ones.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running axe against the &lt;strong&gt;ordinary&lt;/strong&gt; light and dark themes flagged that &lt;code&gt;publicRoomAlias&lt;/code&gt; and &lt;code&gt;publicRoomDescription&lt;/code&gt;, styled &lt;code&gt;$secondary-content&lt;/code&gt; / &lt;code&gt;$tertiary-content&lt;/code&gt;, also failed 4.5:1 against the hover background. Not in high contrast. In the themes essentially everyone uses. That became a second fix in the base stylesheet.&lt;/p&gt;

&lt;p&gt;The reported bug was one theme. The real bug was "hover states in this dialog were never contrast-checked against anything."&lt;/p&gt;

&lt;p&gt;I also scoped something &lt;strong&gt;out&lt;/strong&gt;: axe surfaced a pre-existing &lt;code&gt;nested-interactive&lt;/code&gt; violation (focusable end-adornments inside clickable rows). Real problem, unrelated to colour, would have meant restructuring markup in a PR about a colour token. So — &lt;code&gt;axe.disableRules("nested-interactive")&lt;/code&gt; with an &lt;code&gt;XXX&lt;/code&gt; comment naming exactly why. A named exclusion with a reason is a signpost; a silent one is a bug you hid.&lt;/p&gt;

&lt;p&gt;Getting it green took three rounds of CI. Invalid &lt;code&gt;//&lt;/code&gt; comments in a &lt;code&gt;.pcss&lt;/code&gt; file crashed the Oxfmt parser. Then a screenshot baseline failed — and failed again after I regenerated it locally, with a &lt;strong&gt;418px diff&lt;/strong&gt;, because the baseline is &lt;code&gt;filtered-one-result-linux.png&lt;/code&gt; and the &lt;code&gt;-linux&lt;/code&gt; is a contract, not a label. Font rendering differs between my machine and the CI runner. Committing the image CI itself produced was the fix.&lt;/p&gt;

&lt;p&gt;Final: 12 commits, +83 −12, 4 files, merged 13 August 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coda: the fix that was correct and still got rejected
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/element-hq/element-web/pull/34468" rel="noopener noreferrer"&gt;PR #34468&lt;/a&gt; is closed, and that's why I'm including it.&lt;/p&gt;

&lt;p&gt;Element's room-list submenu opens on hover but never closes on hover-out. Radix's dropdown primitives close a submenu on click-outside, Escape, item selection, or hovering a sibling trigger — pointer leaving and idling elsewhere isn't a recognised trigger. I tracked pointer position against both trigger and content and closed only the submenu after a 300ms grace period, tested in &lt;strong&gt;real Chromium&lt;/strong&gt; via &lt;code&gt;@vitest/browser-playwright&lt;/code&gt; because Radix's pointer machinery genuinely doesn't behave under jsdom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Half-Shot" rel="noopener noreferrer"&gt;@Half-Shot&lt;/a&gt;&lt;/strong&gt; flagged something I hadn't weighed: time-based auto-dismissal can be an accessibility problem, and his OS/browser doesn't do this for submenus at all. He took it to Element's design team, who judged it a step back for mouse users. Closed.&lt;/p&gt;

&lt;p&gt;Another user disagreed in the thread — the submenu &lt;em&gt;opens&lt;/em&gt; on hover, so persisting outside hover is inconsistent, and there's no way to dismiss just the submenu. I think he's right. I also think the call belongs to the maintainers. Correct code and correct product are different judgements, and in someone else's repo only the first one is mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;defaultIntegrations: false&lt;/code&gt; has a maintenance cost nobody budgets for.&lt;/strong&gt; You've opted out of every future SDK improvement, silently, forever. If you use it, put a calendar reminder on reviewing that list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability bugs are the worst kind, because broken instrumentation still produces output.&lt;/strong&gt; A four-slash path renders a perfectly convincing Sentry issue. Assert on the exact output string, using the real SDK — not on your config.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grouping failures are more expensive than symbolication failures.&lt;/strong&gt; Unsymbolicated crashes are annoying. &lt;em&gt;Split&lt;/em&gt; crashes distort which bugs you fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read the build script.&lt;/strong&gt; The unanswered question in a five-year-old issue was answerable in twenty minutes, and answering it turned a partial fix into a complete one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic tokens carry meaning, not values — and meaning flips across themes.&lt;/strong&gt; If your design system has themes, contrast belongs in CI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Widen the test before you narrow the fix.&lt;/strong&gt; Three themes instead of one found a bug nobody had reported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;To &lt;strong&gt;&lt;a href="https://github.com/novocaine" rel="noopener noreferrer"&gt;@novocaine&lt;/a&gt;&lt;/strong&gt;, who wrote a bug report in 2021 so precise that it named the fix — and was honest enough to flag the part he couldn't explain. Five years later, that report was still the best documentation of the problem.&lt;/p&gt;

&lt;p&gt;To the Element maintainers who reviewed these as community PRs: &lt;strong&gt;&lt;a href="https://github.com/t3chguy" rel="noopener noreferrer"&gt;@t3chguy&lt;/a&gt;&lt;/strong&gt;, whose one-line aside about the &lt;code&gt;fetch&lt;/code&gt; script is the reason this PR closes the whole issue instead of half of it, and who told me three separate times that CI was still red; &lt;strong&gt;&lt;a href="https://github.com/Half-Shot" rel="noopener noreferrer"&gt;@Half-Shot&lt;/a&gt;&lt;/strong&gt;, whose "test all the themes" note directly caused the second contrast bug to be found, and who escalated the submenu change to design rather than rubber-stamping it; and &lt;strong&gt;&lt;a href="https://github.com/americanrefugee" rel="noopener noreferrer"&gt;@americanrefugee&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/dbkr" rel="noopener noreferrer"&gt;@dbkr&lt;/a&gt;&lt;/strong&gt; for review and triage.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt; for &lt;code&gt;rewriteFramesIntegration&lt;/code&gt; and for &lt;a href="https://docs.sentry.io/platforms/javascript/configuration/integrations/rewriteframes/" rel="noopener noreferrer"&gt;documenting the custom-origin pattern&lt;/a&gt; clearly enough that a 2021 bug reporter could link to the right page from memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/element-hq/element-web" rel="noopener noreferrer"&gt;Element Web&lt;/a&gt; is AGPL-3.0 and takes community contributions seriously — the review I got on an eight-line config change was better than most people get on features. Their &lt;a href="https://github.com/element-hq/element-web/labels/Good%20First%20Issue" rel="noopener noreferrer"&gt;Good First Issue&lt;/a&gt; list is a genuinely good place to start.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
    </item>
    <item>
      <title>From a 15-Second Walk to a Digital Memory: Building a Canine Cognitive Twin</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Mon, 17 Aug 2026 01:05:55 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/from-a-15-second-walk-to-a-digital-memory-building-a-canine-cognitive-twin-476c</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/from-a-15-second-walk-to-a-digital-memory-building-a-canine-cognitive-twin-476c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-08-13"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Canine Cognitive Twin&lt;/strong&gt; — a living digital twin for a dog.&lt;/p&gt;

&lt;p&gt;A dog owner uploads a short morning-walk video directly from the browser. Gemini analyses the video and returns structured observations plus first-person narration. ElevenLabs turns that narration into the dog's voice. Snowflake Dynamic Tables transform individual observations into a longitudinal baseline, while Cortex identifies potential gait drift that may be difficult to notice manually. Solana records each chapter with a content hash and anomaly flag.&lt;/p&gt;

&lt;p&gt;The result is more than a video analyser:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One walk becomes one chapter in a digital memoir — with a longitudinal behavioural record and an ownership/proof layer.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Not a veterinary device. Not diagnostic. Consult a veterinarian.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🎥 Live Demo
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8JzkaPxSW3w"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  💻 Source Code
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/PrinceXDev/dog-day-edition" rel="noopener noreferrer"&gt;View the Canine Cognitive Twin repository on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Status
&lt;/h2&gt;

&lt;p&gt;The demo demonstrates the implemented end-to-end architecture while clearly distinguishing live processing from synthetic historical data used for anomaly detection.&lt;/p&gt;

&lt;p&gt;The browser upload, Gemini analysis, ElevenLabs narration, Snowflake pipeline, anomaly detection, dashboard, and Solana recording are implemented as separate services connected through the ingestion pipeline.&lt;/p&gt;

&lt;p&gt;Historical gait data used to establish the anomaly-detection baseline is intentionally synthetic and is explicitly labelled as &lt;strong&gt;&lt;code&gt;SYNTHETIC HISTORY&lt;/code&gt;&lt;/strong&gt; in the dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implemented Pipeline
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Browser video upload&lt;/td&gt;
&lt;td&gt;React + Vite&lt;/td&gt;
&lt;td&gt;Allows the owner to upload a dog-walk video directly from the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video analysis&lt;/td&gt;
&lt;td&gt;Gemini 3 Pro&lt;/td&gt;
&lt;td&gt;Extracts behaviour tags, gait asymmetry, emotion, and first-person narration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;Gemini &lt;code&gt;responseSchema&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Keeps model output predictable and machine-readable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Text-to-speech&lt;/td&gt;
&lt;td&gt;ElevenLabs Flash v2.5&lt;/td&gt;
&lt;td&gt;Converts the dog's narration into speech&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data processing&lt;/td&gt;
&lt;td&gt;Snowflake Dynamic Tables&lt;/td&gt;
&lt;td&gt;Transforms raw observations into cleaned events and longitudinal summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Anomaly detection&lt;/td&gt;
&lt;td&gt;Snowflake Cortex&lt;/td&gt;
&lt;td&gt;Detects potential gait drift against historical patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard&lt;/td&gt;
&lt;td&gt;Streamlit in Snowflake&lt;/td&gt;
&lt;td&gt;Presents the dog's memoir and health-radar style insights&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Immutable record&lt;/td&gt;
&lt;td&gt;Solana Devnet&lt;/td&gt;
&lt;td&gt;Records chapter index, content hash, and anomaly flag&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership layer&lt;/td&gt;
&lt;td&gt;Solana PDA&lt;/td&gt;
&lt;td&gt;Gives each dog a persistent on-chain identity&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Synthetic Data Disclosure
&lt;/h2&gt;

&lt;p&gt;Anomaly detection requires a historical baseline. A single video capture cannot establish meaningful longitudinal behaviour, so the repository includes generated history for demonstration and development.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Reason&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;90 days of gait/vitals history&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Synthetic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Required to establish a baseline for anomaly detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Injected gait drift around day 60&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Synthetic&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Demonstrates that the anomaly pipeline can identify a change from baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice Design voice&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pre-configured&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Designed once and reused as the dog's consistent voice persona&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sound effects&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Pre-rendered&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sniffing, panting, collar jingles, and environmental sounds are prepared ahead of time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini fallback response&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Development fallback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prevents a temporary API/rate-limit failure from stopping the demo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The synthetic history is generated by &lt;a href="https://github.com/PrinceXDev/dog-day-edition/blob/main/snowflake/generate_synthetic_history.py" rel="noopener noreferrer"&gt;&lt;code&gt;snowflake/generate_synthetic_history.py&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Every dashboard panel relying on generated historical data is explicitly marked with a &lt;strong&gt;&lt;code&gt;SYNTHETIC HISTORY&lt;/code&gt;&lt;/strong&gt; badge.&lt;/p&gt;

&lt;p&gt;There is no intentional blending of synthetic historical data with live observations without disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;p&gt;The end-to-end data flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;React Web App
      │
      │ Browser video upload
      ▼
Cloudflare Worker
      │
      ├──► Supabase Storage
      │       └── Media storage
      │
      ├──► SHA-256
      │
      └──► Gemini 3 Pro
              │
              ├── behavior_tags
              ├── gait_asymmetry_score
              ├── emotion
              └── narration
                    │
                    ▼
             ElevenLabs
                    │
                    ▼
              Web App Playback

Gemini Result
      │
      ▼
Snowflake RAW_EVENTS
      │
      ▼
Dynamic Tables
      │
      ├──► CLEANED_EVENTS
      │
      └──► DAILY_SUMMARY
                │
                ▼
      Snowflake ML Anomaly Detection
                │
                ▼
          GAIT_ANOMALIES
                │
                ▼
      Streamlit-in-Snowflake
         ├── Memoir
         └── Health Radar
                │
                ▼
          Oracle Relay
                │
                ▼
          Solana Devnet
         record_memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Why I Made These Architecture Decisions
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Browser Upload Instead of a Mobile App
&lt;/h3&gt;

&lt;p&gt;The final implementation uses a browser-based upload flow instead of requiring a separate mobile application.&lt;/p&gt;

&lt;p&gt;This makes the experience immediately accessible:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open the web app → upload a dog-walk video → watch the cognitive twin process it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The browser sends the video to the Cloudflare Worker, which becomes the entry point for the processing pipeline.&lt;/p&gt;
&lt;h3&gt;
  
  
  Oracle Relay Instead of Direct Snowflake Signing
&lt;/h3&gt;

&lt;p&gt;Snowflake Dynamic Tables cannot directly sign Solana transactions or freely call arbitrary external APIs.&lt;/p&gt;

&lt;p&gt;Instead, a lightweight &lt;code&gt;oracle-relay&lt;/code&gt; polls Snowflake's &lt;code&gt;V_ORACLE_PAYLOAD&lt;/code&gt;, signs the transaction, and submits it to Solana Devnet.&lt;/p&gt;

&lt;p&gt;This keeps blockchain operations outside the main Gemini + TTS request path, so a slow blockchain transaction does not block the user-facing processing pipeline.&lt;/p&gt;

&lt;p&gt;The Anchor program still verifies the oracle public key on-chain.&lt;/p&gt;
&lt;h3&gt;
  
  
  Separate Runtime Implementations
&lt;/h3&gt;

&lt;p&gt;The ingestion worker runs inside the Cloudflare Workers runtime, while the oracle relay runs in Node.js with &lt;code&gt;tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Although both components use keypair-based authentication, their runtime crypto APIs and execution environments differ. Keeping them separate avoids introducing an abstraction purely to unify two different runtime environments.&lt;/p&gt;
&lt;h3&gt;
  
  
  Fallback Instead of Silent Failure
&lt;/h3&gt;

&lt;p&gt;External AI APIs can fail because of rate limits or temporary availability issues.&lt;/p&gt;

&lt;p&gt;Instead of silently pretending the response was generated live, the worker can load a pre-recorded Gemini response when the live request fails.&lt;/p&gt;

&lt;p&gt;The response explicitly contains:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fallback"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This allows the dashboard and pipeline to communicate the state rather than hiding it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prize Technology
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Used For&lt;/th&gt;
&lt;th&gt;Why It Matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google AI / Gemini&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Multimodal video analysis, schema-constrained JSON, and contextual reasoning&lt;/td&gt;
&lt;td&gt;Provides the actual observation and interpretation layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Snowflake&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dynamic Tables, historical baselines, Cortex anomaly detection, and Streamlit&lt;/td&gt;
&lt;td&gt;Turns individual observations into longitudinal intelligence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Voice Design, Flash v2.5 narration, and sound design&lt;/td&gt;
&lt;td&gt;Gives the digital twin a consistent personality and voice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Solana&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dog identity and immutable memory records&lt;/td&gt;
&lt;td&gt;Adds an ownership and proof layer outside a centralised application database&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These technologies are not decorative integrations.&lt;/p&gt;

&lt;p&gt;Each one has a specific role in the product:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gemini → Observe &amp;amp; Understand&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snowflake → Remember &amp;amp; Detect Change&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs → Give the Twin a Voice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana → Own &amp;amp; Prove&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/PrinceXDev" rel="noopener noreferrer"&gt;
        PrinceXDev
      &lt;/a&gt; / &lt;a href="https://github.com/PrinceXDev/dog-day-edition" rel="noopener noreferrer"&gt;
        dog-day-edition
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Canine Cognitive Twin&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;A living digital twin for a dog. An owner uploads a clip of a morning walk from the
browser; Gemini reads the video and returns structured observations plus a first-person
narration; ElevenLabs speaks it; Snowflake Dynamic Tables fold it into a rolling
baseline; Cortex flags gait drift the eye would miss; Solana records the chapter under
the owner's own keys.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Not a veterinary device. Not diagnostic. Consult a veterinarian.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New here and just want to get something running? Jump to &lt;a href="https://github.com/PrinceXDev/dog-day-edition#quick-start" rel="noopener noreferrer"&gt;Quick start&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What is live vs. what is synthetic&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;This table is the first thing in the repo on purpose. Everything below the line is
generated, and it is labeled generated on screen too.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Live — clicked during the demo, nothing faked&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Tech&lt;/th&gt;
&lt;th&gt;Latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Owner uploads a video of a walk from the browser, straight to the Worker&lt;/td&gt;
&lt;td&gt;React + Vite web app&lt;/td&gt;
&lt;td&gt;~5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/PrinceXDev/dog-day-edition" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;canine-cognitive-twin/
├── snowflake/          Dynamic Tables, Cortex anomaly model, Streamlit app
├── gemini-pipeline/    Schema-constrained Gemini analysis
├── elevenlabs-service/ Voice Design, narration, SFX
├── ingestion-worker/   Cloudflare Worker: Supabase → Gemini → Snowflake → TTS
├── oracle-relay/       Snowflake → Solana transaction relay
├── solana-program/     Anchor program for dog identity and memories
├── web-app/            React + Vite browser application
└── demo-assets/        Fallback response and demo assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The current implementation establishes the core pipeline from &lt;strong&gt;video → observation → narration → longitudinal analysis → immutable memory&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The next milestones would be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Owner-facing export of the memoir and on-chain proof&lt;/li&gt;
&lt;li&gt;Multi-dog households with one Solana wallet and a PDA per dog&lt;/li&gt;
&lt;li&gt;Per-dog voice calibration&lt;/li&gt;
&lt;li&gt;Longer-term behavioural and mobility trends&lt;/li&gt;
&lt;li&gt;More advanced gait and movement visualisations&lt;/li&gt;
&lt;li&gt;Additional behavioural signals beyond gait&lt;/li&gt;
&lt;li&gt;Stronger owner-facing explanations around detected changes&lt;/li&gt;
&lt;li&gt;Continuous longitudinal learning from real owner-approved captures&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;I'm submitting this project for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best Use of Snowflake&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best Use of Solana&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Best Use of Google AI&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal wasn't simply to combine four technologies.&lt;/p&gt;

&lt;p&gt;Each technology solves a different part of the same problem:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Observe → Understand → Remember → Give a Voice → Prove&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Final Note
&lt;/h2&gt;

&lt;p&gt;I wanted the project to demonstrate something more meaningful than a simple AI video analyser.&lt;/p&gt;

&lt;p&gt;A single video can describe what a dog is doing &lt;strong&gt;right now&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A cognitive twin should be able to understand what is changing &lt;strong&gt;over time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's why the architecture combines multimodal AI, longitudinal data processing, anomaly detection, voice, and an ownership layer.&lt;/p&gt;

&lt;p&gt;The final experience is intentionally simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Upload a walk. Let the twin understand it. Turn it into a memory. Track what changes over time. Keep the record under the owner's control.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks for reading. 🐶&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>The 2 A.M. Fridge: A Landing Page With Exactly One Light Source</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Mon, 10 Aug 2026 10:30:17 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/the-2-am-fridge-a-landing-page-with-exactly-one-light-source-297c</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/the-2-am-fridge-a-landing-page-with-exactly-one-light-source-297c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🍕 What I Built
&lt;/h2&gt;

&lt;p&gt;Most "comfort food" landing pages are going to show you a hero shot of steam rising off a bowl of ramen, a warm colour palette, maybe a parallax scroll. I wanted to build the opposite of that: a page with almost no light in it, where the only thing you can see is whatever you choose to open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 2 A.M. Fridge&lt;/strong&gt; is a single page. It's pitch black. There's a fridge door in the middle of the screen, closed. You pull the tab, the door swings open, and a rectangle of cold light spills into the kitchen — and that's the entire lighting model for the rest of the experience. Nothing else on the page is lit except what the fridge illuminates.&lt;/p&gt;

&lt;p&gt;The concept came from a very specific, very unglamorous kind of comfort: not the food itself, but the ritual of standing in front of an open fridge at 2 a.m., not really hungry, just looking. So instead of a menu or a hero pitch, the page is structured as five "acts" — top shelf, middle shelf, the drawer, the freezer, and a hidden jar pushed to the back — and every item you find has a short piece of fiction attached to it. Someone else's mustard, three-day-old curry, a jar of stock made in a good week for a bad one. Nothing dramatic. Just the small, specific stories that live in everyone's fridge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The technical decision that everything else hangs off:&lt;/strong&gt; the whole scene is driven by one CSS custom property.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--light-intensity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;"&amp;lt;number&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;As you drag the pull-tab (or press Enter/Space on it — more on that below), &lt;code&gt;fridge-door.ts&lt;/code&gt; updates &lt;code&gt;--light-intensity&lt;/code&gt; from 0 to 1. That single number feeds the lightwell's radial-gradient glow, the door outline's &lt;code&gt;filter: brightness()&lt;/code&gt;, every shelf item's cast shadow, and the condensation overlay's opacity. One value, read in one place, drives the entire visual state of the page. No shadow maps, no lighting engine — just a number and a handful of CSS consumers that all agree on what it means.&lt;/p&gt;

&lt;p&gt;The interactions I'm most proud of aren't the obvious ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The fridge door has real spring physics&lt;/strong&gt;, not a CSS transition easing curve. There's a tiny critically-damped spring integrator (&lt;code&gt;spring.ts&lt;/code&gt;, no dependencies) that also powers a "nudge" effect — hover over a shelf item and its neighbours flinch slightly, like you bumped the shelf.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shelf items are a real 2D grid widget.&lt;/strong&gt; Arrow keys move focus row-to-column, Home/End jump to the ends, and only the focused item sits in the tab sequence (roving &lt;code&gt;tabindex&lt;/code&gt;). It's not a &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; soup pretending to be interactive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The jar pushed to the back of the freezer&lt;/strong&gt; is a two-step micro-puzzle — move the ice tray, move the bag of peas, then the jar becomes reachable. It holds the emotional peak of the whole piece (a broth someone's grandmother made "for a day she must have known she might not be there for"), and it's built from three ordinary buttons. No drag, no timing window, no pointer-only trap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The closing line is emergent, not fixed.&lt;/strong&gt; There are about ten hand-written closing sentences, and which one you see depends on what you actually did — did you find the jar, did you close the door and reopen it, did you never touch a mouse, did you linger longest at the freezer. It's a pure decision tree over signals tracked in memory for that visit only. Nothing is sent anywhere, nothing persists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this needed a framework to do the heavy lifting. It's Astro, TypeScript, and vanilla DOM/CSS — the LCP element (the fridge door) is server-rendered HTML/CSS with zero JS required to paint it, and everything animated only ever touches &lt;code&gt;transform&lt;/code&gt;/&lt;code&gt;opacity&lt;/code&gt;/&lt;code&gt;filter&lt;/code&gt;. &lt;code&gt;backdrop-filter&lt;/code&gt; never gets driven by a scroll or state variable on purpose — it's one of the most expensive properties you can animate, and this page didn't need to prove a point by using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://the-2-am-fridge.netlify.app/" rel="noopener noreferrer"&gt;https://the-2-am-fridge.netlify.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please actually open the fridge door instead of just scrolling past it — drag the pull-tab, or tab to it and hit Enter. Go through all five acts. Hover over a shelf item to see the neighbour-nudge. Find the jar at the back of the freezer (you'll need to move two things first). Try the "Turn on the kitchen light" toggle in the header, which flips the entire page to a fully lit, 7:1-contrast layout using the exact same design tokens, not a separate theme bolted on. Then close the door and see what the closing line says about your visit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/PrinceXDev/perfect-landing-comfort-food" rel="noopener noreferrer"&gt;https://github.com/PrinceXDev/perfect-landing-comfort-food&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to see how it's actually wired together, &lt;code&gt;src/scripts/fridge-door.ts&lt;/code&gt; and &lt;code&gt;src/scripts/spring.ts&lt;/code&gt; are the two files that do the most with the least code.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧑‍💻 Journey
&lt;/h2&gt;

&lt;p&gt;I didn't start with "let's build a dark UI." I started with the idea that the most honest comfort-food story isn't a recipe, it's the fridge itself — specifically what's still in it that nobody's dealt with. Once I had that, the constraint fell out naturally: if the fridge is the only story, the fridge should be the only light.&lt;/p&gt;

&lt;p&gt;That constraint turned out to be the best thing that happened to the build, because it forced every decision through one question — does this need light, or can it stay dark? The hero heading is visually hidden (there's nothing to read in pitch black, just a floating pull-tab and a timestamp). The condensation effect on the shelf glass runs through an SVG &lt;code&gt;feTurbulence&lt;/code&gt; filter rather than a canvas or WebGL trick, because it only needed to look right inside a small lit rectangle, not do anything clever at scale.&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the visuals — it was making sure the darkness didn't become an accessibility liability. It's easy to build a moody dark UI that's a nightmare for anyone using a screen reader, reduced motion, or keyboard-only navigation. I didn't want the "experience" to be a tax paid only by sighted mouse users, so I wrote a whole &lt;code&gt;/accessibility&lt;/code&gt; page documenting the actual decisions, not a boilerplate statement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The drag-to-open door has a non-dragging equivalent (WCAG 2.5.7) — Enter/Space fully opens or closes it.&lt;/li&gt;
&lt;li&gt;Motion respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt; and a persisted on/off toggle, implemented as a real CSS state (&lt;code&gt;html[data-motion]&lt;/code&gt;), not a one-time sweep of running animations — a sweep misses &lt;code&gt;requestAnimationFrame&lt;/code&gt; loops and SVG SMIL entirely. The condensation filter's SMIL &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; specifically needed &lt;code&gt;SVGSVGElement.pauseAnimations()&lt;/code&gt; because CSS media queries can't reach into an SVG filter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forced-colours: active&lt;/code&gt; (Windows Contrast Themes) is handled deliberately: since the OS overrides colours regardless of what I author, the decorative light/shadow/condensation layers are hidden outright instead of rendering unforced on top of a forced palette.&lt;/li&gt;
&lt;li&gt;Every interactive target clears the 24×24px minimum (WCAG 2.5.8), verified on an iPhone 13 viewport in Playwright.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Writing that page after the fact, rather than bolting accessibility on as an afterthought, changed how I thought about the "emergent epilogue" feature too. The keyboard-only branch of the closing line exists because a keyboard-only visitor does something measurably true — no &lt;code&gt;pointerdown&lt;/code&gt; ever fires, but real input clearly did — so the accessibility work became part of the story itself instead of a separate feature to demo.&lt;/p&gt;

&lt;p&gt;The other genuinely hard problem was performance discipline against my own worst instincts. It would have been very easy to reach for &lt;code&gt;backdrop-filter&lt;/code&gt; for the frosted-glass condensation look, or to run the shelf-nudge spring loop continuously "just in case." Instead, the spring loop starts on &lt;code&gt;requestAnimationFrame&lt;/code&gt; only when nudged and kills itself (&lt;code&gt;rafId = null&lt;/code&gt;) the instant every item has settled, and I kept the animated set strictly to properties the compositor can handle cheaply.&lt;/p&gt;

&lt;p&gt;I write the shelf and freezer stories almost like flash fiction — a few sentences, an interior monologue, no exposition. That took longer than any of the code. Getting a hidden jar of a grandmother's broth to land emotionally in two sentences, gated behind an actual puzzle instead of just appearing, was the part I rewrote the most.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I'd improve next:&lt;/strong&gt; the README is upfront about this — there's no art direction or photography pass yet (it's a deliberately text-only build), fonts are still the system stack rather than self-hosted and subset, and the mobile pass covers touch/viewport correctness but hasn't had the same interaction polish as desktop pointer events. If I kept going, next would be a proper mobile drag gesture for the door, and maybe extending the "one custom property drives everything" idea to a subtle day/night cycle based on the visitor's actual local time.&lt;/p&gt;

&lt;p&gt;What I learned, more than anything: constraints are a gift. "The fridge is the only light source" sounds like it limits what you can build. In practice, it made almost every design decision obvious, because there was always a clear test — does this need the light, or does it live in the dark.&lt;/p&gt;




&lt;p&gt;If you try it, I'd genuinely like to know which shelf item got to you, and whether you found the jar. Open the fridge, poke around, and if something breaks or the accessibility claims above don't hold up — file an issue, that page says as much itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#webdev&lt;/code&gt; &lt;code&gt;#javascript&lt;/code&gt; &lt;code&gt;#astro&lt;/code&gt; &lt;code&gt;#accessibility&lt;/code&gt; &lt;code&gt;#showdev&lt;/code&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>139,907 Calls Later: What It Actually Takes to Port a Numerical Library from JavaScript to Rust</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Mon, 10 Aug 2026 05:47:44 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/139907-calls-later-what-it-actually-takes-to-port-a-numerical-library-from-javascript-to-rust-5c07</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/139907-calls-later-what-it-actually-takes-to-port-a-numerical-library-from-javascript-to-rust-5c07</guid>
      <description>&lt;h1&gt;
  
  
  139,907 Calls Later: What It Actually Takes to Port a Numerical Library from JavaScript to Rust
&lt;/h1&gt;

&lt;p&gt;Porting a library between languages is mostly a typing exercise until the output has to be &lt;em&gt;exactly&lt;/em&gt; the same.&lt;/p&gt;

&lt;p&gt;For most code, "the same" has slack in it. A web handler that returns the right JSON with the fields in a different order is fine. A parser that produces an equivalent AST with different node names is fine. But when you port an arbitrary-precision arithmetic library, there is no slack. &lt;code&gt;"5e-8".toFixed(2)&lt;/code&gt; is either &lt;code&gt;"0.00"&lt;/code&gt; or it is wrong. There is no third answer, no "close enough," no reviewer who will accept &lt;code&gt;0.01&lt;/code&gt; because the algorithm is &lt;em&gt;morally&lt;/em&gt; correct.&lt;/p&gt;

&lt;p&gt;I spent the back end of the Code Resurrection window rebuilding &lt;a href="https://github.com/MikeMcl/bignumber.js" rel="noopener noreferrer"&gt;MikeMcl/bignumber.js&lt;/a&gt; — about 2,520 lines of JavaScript — in Rust. The interesting part of that work was not writing Rust. It was the fourteen separate occasions where I was confident the port was correct, and ~140,000 captured calls from the original library's own test suite disagreed with me.&lt;/p&gt;

&lt;p&gt;This is the write-up of what broke, how I found it, and the one decision I'd take back.&lt;/p&gt;




&lt;h2&gt;
  
  
  The project I chose
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://coderesurrection.com/2026/" rel="noopener noreferrer"&gt;Code Resurrection 2026&lt;/a&gt; frames porting as &lt;em&gt;digital archaeology&lt;/em&gt;: you don't just move code across a language boundary, you excavate what the original actually promised and carry those promises forward. The framing matters more than it sounds like it does, and I'll come back to why.&lt;/p&gt;

&lt;p&gt;I picked bignumber.js off the JavaScript → Go/Rust track for a reason that turned out to be the whole story: &lt;strong&gt;it is a library whose entire value proposition is that its output is exact.&lt;/strong&gt; Most candidate projects let you argue about fidelity. This one doesn't. Either my &lt;code&gt;dividedBy&lt;/code&gt; returns the same digits as the original's &lt;code&gt;dividedBy&lt;/code&gt; for every input, or my port is a different library that happens to have the same method names.&lt;/p&gt;

&lt;p&gt;That gave me something rare in a porting project: an unambiguous, mechanically checkable definition of "done."&lt;/p&gt;




&lt;h2&gt;
  
  
  What bignumber.js is for
&lt;/h2&gt;

&lt;p&gt;The canonical demo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
&lt;span class="c1"&gt;// 0.30000000000000004&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a JavaScript bug; it's IEEE 754. A binary floating-point &lt;code&gt;f64&lt;/code&gt; has 53 bits of mantissa and stores values as a binary fraction. &lt;code&gt;0.1&lt;/code&gt; and &lt;code&gt;0.2&lt;/code&gt; are not representable in binary any more than &lt;code&gt;1/3&lt;/code&gt; is representable in decimal — you get the nearest &lt;code&gt;f64&lt;/code&gt;, and the error surfaces when you add them.&lt;/p&gt;

&lt;p&gt;For graphics or physics, that's invisible. For money, tax, or anything that reconciles against a ledger, it's a defect. bignumber.js exists to give JavaScript a number type that stores decimal digits as decimal digits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// "0.3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The library covers arbitrary-precision decimal arithmetic, nine rounding modes, configurable precision and exponential-notation thresholds, non-decimal bases, and a formatting layer. It's ~2,520 lines, MIT-licensed, and has been load-bearing infrastructure in the JS ecosystem for over a decade.&lt;/p&gt;

&lt;p&gt;Which raises the obvious question for a port: &lt;strong&gt;what, exactly, is the specification?&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The archaeology: the source code was the smaller half of the artefact
&lt;/h2&gt;

&lt;p&gt;Here is the thing I did not expect, and it reframed the whole project.&lt;/p&gt;

&lt;p&gt;bignumber.js ships ~2,520 lines of implementation. Its test suite is &lt;strong&gt;47,713 lines containing 65,727 assertions across 34 files.&lt;/strong&gt; The tests are nineteen times larger than the thing they test.&lt;/p&gt;

&lt;p&gt;That ratio is not an accident, and it is not over-testing. It is the actual shape of the specification. The implementation tells you &lt;em&gt;how&lt;/em&gt; the library computes; the test suite tells you &lt;em&gt;what it promises&lt;/em&gt; — and for a numerical library, the promises live almost entirely in the edges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does &lt;code&gt;toFixed&lt;/code&gt; do when the rounding position falls before the first stored digit?&lt;/li&gt;
&lt;li&gt;What is the sign of a zero, and which methods preserve it versus drop it?&lt;/li&gt;
&lt;li&gt;What happens to &lt;code&gt;HALF_EVEN&lt;/code&gt; when the tie-break digit is the last one and rounding up grows the digit count?&lt;/li&gt;
&lt;li&gt;Does &lt;code&gt;Math.pow(1, NaN)&lt;/code&gt; return &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;NaN&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those is answered by reading &lt;code&gt;round()&lt;/code&gt;. All of them are answered by the tests.&lt;/p&gt;

&lt;p&gt;My original plan was to hand-translate each of the 34 test files into Rust &lt;code&gt;#[test]&lt;/code&gt;s. Once I actually measured the suite — 47,713 lines — that plan was dead. Not just infeasible in the time I had, but &lt;em&gt;lower value&lt;/em&gt; than the alternative, and this is the decision I'm most glad I made:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instead of re-stating the original's tests in Rust, I ran the original's tests for real and recorded everything they did.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The harness (&lt;code&gt;fuzz/capture_tester.js&lt;/code&gt;) monkey-patches &lt;code&gt;BigNumber.prototype&lt;/code&gt; purely to &lt;em&gt;observe&lt;/em&gt; — it doesn't alter behaviour — and captures, for every single call the real suite makes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;method&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;activeConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That produced &lt;strong&gt;139,907 real calls&lt;/strong&gt;. Those get replayed against the Rust port by &lt;code&gt;src/bin/replay_captured.rs&lt;/code&gt; and diffed.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;activeConfig&lt;/code&gt; field is there because of a genuine archaeological finding. &lt;code&gt;BigNumber.config()&lt;/code&gt; is a global mutable state in JavaScript — it lives in variables closed over by the &lt;code&gt;clone()&lt;/code&gt; factory. That means config &lt;strong&gt;leaks across test files&lt;/strong&gt; loaded later in the same process. A call in &lt;code&gt;toFixed.js&lt;/code&gt; might execute under a &lt;code&gt;DECIMAL_PLACES&lt;/code&gt; set by a file that ran twenty minutes earlier. If you replay those calls under the default config, you get hundreds of false divergences, and you'll spend a day chasing bugs that don't exist. Capturing config per-call was not a nicety; it was the difference between a usable signal and noise.&lt;/p&gt;

&lt;p&gt;One honest footnote, because it belongs in the record: instrumenting the prototype costs 12 assertions inside the JS suite itself — 65,715 pass under capture versus 65,727 unmodified. It's almost certainly a wrapper interaction with &lt;code&gt;squareRoot&lt;/code&gt;'s internal &lt;code&gt;t.times(t).eq(x)&lt;/code&gt; self-comparison, not a defect in bignumber.js. I documented it rather than quietly rounding it away.&lt;/p&gt;




&lt;h2&gt;
  
  
  A port is not a translation
&lt;/h2&gt;

&lt;p&gt;The gap between JavaScript and Rust that mattered here was almost never syntax. It was semantics, and the semantics hid in four places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Null is three different things.&lt;/strong&gt; JS stores a BigNumber as &lt;code&gt;{s, c, e}&lt;/code&gt; — sign, coefficient limbs, exponent — and uses &lt;code&gt;null&lt;/code&gt; in each field to encode special values. &lt;code&gt;s: null&lt;/code&gt; means NaN. &lt;code&gt;c: null&lt;/code&gt; with a non-null &lt;code&gt;s&lt;/code&gt; means ±Infinity. Rust has no &lt;code&gt;null&lt;/code&gt;, so the port models this as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;BigNumber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cd"&gt;/// `Some(1)` positive, `Some(-1)` negative, `None` = NaN.&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// Coefficient limbs, base 1e14, most-significant first. `None` = ±Infinity or NaN.&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="cd"&gt;/// Base-10 exponent of the most significant digit.&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;i64&lt;/span&gt;&lt;span class="o"&gt;&amp;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;I deliberately did &lt;strong&gt;not&lt;/strong&gt; collapse this into the enum every Rust reviewer's instinct reaches for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;BigNumber&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Finite&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="nf"&gt;Infinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Sign&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;NaN&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// the tempting version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That enum is better Rust. It is also a rewrite, not a port. Nearly every original algorithm — &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;normalise&lt;/code&gt;, &lt;code&gt;compare&lt;/code&gt; — branches on the specific null/non-null &lt;em&gt;combinations&lt;/em&gt; of &lt;code&gt;s&lt;/code&gt;/&lt;code&gt;c&lt;/code&gt;/&lt;code&gt;e&lt;/code&gt;, in an order that depends on exactly this shape. Re-deriving equivalent branching against a cleaner enum means re-proving each algorithm's correctness from scratch instead of porting it. With a hard deadline and a correctness bar of "byte-identical," that trade was not closed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Global mutable config doesn't survive the crossing.&lt;/strong&gt; JS's &lt;code&gt;BigNumber.config()&lt;/code&gt; mutates closure-captured module state shared by every instance from that constructor. Rust has no natural equivalent, and faking one with a &lt;code&gt;static mut&lt;/code&gt; or a lazy global would import the original's biggest weakness — the original is not thread-safe; you need two separate &lt;code&gt;BigNumber.clone()&lt;/code&gt; factories to get independent config in one process. So the port threads config explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="nf"&gt;.plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;// instead of x.plus(&amp;amp;y) reading a global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More idiomatic, trivially thread-safe, and a &lt;strong&gt;disclosed divergence&lt;/strong&gt;: &lt;code&gt;BigNumber.config()&lt;/code&gt;/&lt;code&gt;.set()&lt;/code&gt; are consequently not ported as such. That's in DECISIONS.md, not buried.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JavaScript's &lt;code&gt;Math.pow&lt;/code&gt; is not IEEE 754.&lt;/strong&gt; This one costs real time. &lt;code&gt;exponentiatedBy&lt;/code&gt; has a float-fallback path for NaN/±Infinity/±1/±0 operands, and Rust's &lt;code&gt;f64::powf&lt;/code&gt; disagrees with JS in exactly two places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Math.pow(x, NaN)&lt;/code&gt; is always &lt;code&gt;NaN&lt;/code&gt; in JS. Rust's &lt;code&gt;powf&lt;/code&gt; special-cases base &lt;code&gt;1.0&lt;/code&gt; and returns &lt;code&gt;1.0&lt;/code&gt; even for a NaN exponent.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Math.pow(±1, ±Infinity)&lt;/code&gt; is &lt;code&gt;NaN&lt;/code&gt; per ECMA-262's &lt;code&gt;Number::exponentiate&lt;/code&gt; — a deliberate JS-specific deviation from C99 &lt;code&gt;pow&lt;/code&gt;, which defines it as &lt;code&gt;1&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without a wrapper, &lt;code&gt;BigNumber(1).exponentiatedBy(NaN)&lt;/code&gt; returned &lt;code&gt;1&lt;/code&gt; instead of &lt;code&gt;NaN&lt;/code&gt; — and worse, it propagated &lt;em&gt;silently&lt;/em&gt; through the modular form, where the wrong &lt;code&gt;1&lt;/code&gt; fed into &lt;code&gt;.modulo(m)&lt;/code&gt; and produced &lt;code&gt;0&lt;/code&gt; instead of a &lt;code&gt;NaN&lt;/code&gt;. The fix is nine lines, and every one of them is a language-semantics fact, not an arithmetic one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;js_math_pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="nf"&gt;.is_nan&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="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&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="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;exponent&lt;/span&gt;&lt;span class="nf"&gt;.is_infinite&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="nn"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;NAN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="nf"&gt;.powf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exponent&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;&lt;strong&gt;Errors move from throw-sites to type signatures.&lt;/strong&gt; JS throws &lt;code&gt;Error('Exponent not an integer: ' + n)&lt;/code&gt; from anywhere. In Rust, that has to become &lt;code&gt;Result&amp;lt;BigNumber, BigNumberError&amp;gt;&lt;/code&gt;, which changes the signature, which changes every caller. Two of my fourteen bugs were &lt;em&gt;missing&lt;/em&gt; error paths — places where JS validated, and I hadn't, so instead of an error, the port returned a wrong answer or hung.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the number actually is
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BigNumber
├── s : sign        Some(1) | Some(-1) | None (NaN)
├── c : coefficient Vec&amp;lt;i64&amp;gt; of base-1e14 limbs, most-significant first
│                   None = ±Infinity or NaN
└── e : exponent    base-10 exponent of the most significant digit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Base 1e14 is the original's choice, and I kept it: it's the largest power of ten whose products stay inside JS's 2^53 safe-integer range when split via a 1e7 half-base during multiplication. Carrying that constant across meant carrying the exact same overflow boundaries, which meant carrying the exact same results.&lt;/p&gt;

&lt;p&gt;Two internals I &lt;em&gt;did&lt;/em&gt; rewrite, both consciously:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;round()&lt;/code&gt; operates on a flat digit string, not limb indices.&lt;/strong&gt; The original tracks, which base-1e14 limb the rounding digit falls in plus its offset within that limb (&lt;code&gt;ni&lt;/code&gt;, &lt;code&gt;i&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;d&lt;/code&gt; in the JS source). It's correct, and it's fast. My direct transliteration produced a genuine borrow-checker conflict and several off-by-one bugs before I scrapped it. The Rust version flattens the coefficient to a digit string, makes the rounding-mode decision and carries there, then rebuilds limbs aligned to the resulting exponent. Same observable semantics — verified against Node ground truth across all nine rounding modes, including &lt;code&gt;HALF_EVEN&lt;/code&gt;'s odd/even tie-break and carry-driven digit-count growth — different mechanism.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Division is schoolbook long division on digit strings.&lt;/strong&gt; JS's &lt;code&gt;div()&lt;/code&gt; works directly on base-1e14 limbs with a divisor-normalization trick. Mine extracts digit strings and does ordinary digit-by-digit long division. This was a correctness-over-performance call, and it has a price tag I'll put on the table later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fourteen bugs
&lt;/h2&gt;

&lt;p&gt;The replay harness found them. Here are the ones that taught me something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The exponent-alignment bug in division (&lt;code&gt;49 / 13&lt;/code&gt;).&lt;/strong&gt; Equal digit lengths, the dividend's leading digits exceeding the divisor's. Caught before it could propagate into &lt;code&gt;dividedBy&lt;/code&gt;, &lt;code&gt;modulo&lt;/code&gt;, &lt;code&gt;squareRoot&lt;/code&gt;, and &lt;code&gt;dividedToIntegerBy&lt;/code&gt; — which is the entire argument for building and verifying the primitives &lt;em&gt;first&lt;/em&gt;. I built the core representation and the two highest-risk primitives (&lt;code&gt;round()&lt;/code&gt; and decimal long division) against real Node ground truth before writing a single higher-level method. That ordering caught three bugs in the core before they could multiply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;toFraction()&lt;/code&gt; and a JavaScript aliasing detail.&lt;/strong&gt; The original's continued-fraction algorithm contains this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;d1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;n0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ONE&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;d1&lt;/code&gt; and &lt;code&gt;n0&lt;/code&gt; are the &lt;em&gt;same object&lt;/em&gt;. Later, mutating &lt;code&gt;n0.c[0]&lt;/code&gt; also zeroes &lt;code&gt;d1&lt;/code&gt;. In JS, that's just how object references work, and the algorithm quietly depends on it. In Rust, &lt;code&gt;let d1 = n0.clone()&lt;/code&gt; gives you two independent values, and the algorithm silently produces the wrong fraction. The fix was explicitly modelling the shared state — but finding it meant reading the original closely enough to notice that an assignment chain was load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;toFormat()&lt;/code&gt;'s digit grouping captured &lt;code&gt;groupSize&lt;/code&gt; after a swap instead of before&lt;/strong&gt;, producing Indian-style grouping where standard was correct, and vice versa. A one-line ordering bug that no amount of staring at my own code would have surfaced. The replay surfaced in seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And then the one that ate the most hours.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;round()&lt;/code&gt;'s handling of &lt;code&gt;sd &amp;lt; 1&lt;/code&gt; — the case where the rounding position falls &lt;em&gt;before&lt;/em&gt; the value's leading digit, in implicit-zero territory that isn't in the stored coefficient at all.&lt;/p&gt;

&lt;p&gt;My code treated "the rounding digit" as the value's own leading digit for any &lt;code&gt;sd &amp;lt;= 0&lt;/code&gt;. That looks right. It reads right. It is wrong. JS's digit-position formula reduces to "the leading digit" at &lt;strong&gt;exactly&lt;/strong&gt; &lt;code&gt;sd == 0&lt;/code&gt;; for &lt;code&gt;sd &amp;lt; 0&lt;/code&gt; it evaluates to &lt;code&gt;rd = 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The observable consequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;5e-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;ground&lt;/span&gt; &lt;span class="nx"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.00&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;my&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0.01&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because my version saw the rounding digit as &lt;code&gt;5&lt;/code&gt; and rounded up, when JS sees &lt;code&gt;0&lt;/code&gt; and stays at zero.&lt;/p&gt;

&lt;p&gt;I did not fix this by guessing. I traced JS's &lt;code&gt;round(x, sd, rm, r)&lt;/code&gt; line by line against the specific failing inputs, and the actual insight was structural: &lt;strong&gt;JS only consults the &lt;code&gt;sd &amp;lt; 1&lt;/code&gt; branch for the final write-out, never for computing the rounding digit.&lt;/strong&gt; I had the branch in the right function and at the wrong point in the control flow. The comment I left in &lt;a href="//src/parse.rs"&gt;src/parse.rs&lt;/a&gt; is the one I'd want a future maintainer to read first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Compute rd (the rounding digit) and r (whether any nonzero digit exists&lt;/span&gt;
&lt;span class="c1"&gt;// after it) unconditionally, BEFORE any special-casing on `sd &amp;lt; 1`, matching&lt;/span&gt;
&lt;span class="c1"&gt;// JS's structure where the `sd &amp;lt; 1 || !xc[0]` branch is only consulted for the&lt;/span&gt;
&lt;span class="c1"&gt;// final write-out, not for computing rd/r. Getting this order backwards was a&lt;/span&gt;
&lt;span class="c1"&gt;// real bug: treating "sd &amp;lt; 1" as an immediate special case for rd computation&lt;/span&gt;
&lt;span class="c1"&gt;// made e.g. "5e-8".toFixed(2) treat rd as 5 (round up) instead of 0.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was a sibling bug in the same function, and it's a nice illustration of how representation choices leak. After a value round-trips through base-1e14 limbs, &lt;code&gt;coeff_to_string&lt;/code&gt; strips trailing zeros — so &lt;code&gt;digit_count&lt;/code&gt; can legitimately end up &lt;em&gt;less&lt;/em&gt; than the &lt;code&gt;sd&lt;/code&gt; a caller asks for. Dividing at &lt;code&gt;DECIMAL_PLACES=47&lt;/code&gt; produced a 70-digit quotient that round-tripped to 67 stored digits and was then rounded to &lt;code&gt;sd=68&lt;/code&gt;. My &lt;code&gt;.take(68)&lt;/code&gt; on a 67-character string silently returned 67 characters, so a round-up carry landed on the last &lt;em&gt;real&lt;/em&gt; digit instead of an implicit zero at position 68 — quietly dropping the correct final digit. Rust's iterators will happily give you fewer items than you asked for and not say a word about it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two of the "divergences" were bugs in my own harness&lt;/strong&gt;, which is its own lesson. JS's &lt;code&gt;String(-0)&lt;/code&gt; is &lt;code&gt;"0"&lt;/code&gt; — and bignumber.js's &lt;code&gt;toString()&lt;/code&gt; deliberately drops the sign for zero (there's an explicit comment in the source saying so); only &lt;code&gt;valueOf()&lt;/code&gt; preserves it. My capture script used &lt;code&gt;String()&lt;/code&gt; on every value, destroying the sign of every negative zero before the Rust side ever saw one. Fixing the capture then exposed the mirror-image asymmetry: my replay tool used &lt;code&gt;to_string()&lt;/code&gt;, which drops the sign the same way, briefly reintroducing ~181 spurious divergences. &lt;strong&gt;A differential harness is a program, and it has bugs, and yours will look exactly like port bugs.&lt;/strong&gt; Re-run the whole replay after every harness change.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the fixed test suite couldn't catch
&lt;/h2&gt;

&lt;p&gt;139,907 calls is a lot, but it's a &lt;em&gt;fixed&lt;/em&gt; set — every input a group of humans thought to write down. So I built a second harness: a random differential fuzzer. &lt;code&gt;fuzz/generate_inputs.js&lt;/code&gt; emits random &lt;code&gt;op,a,b&lt;/code&gt; triples biased toward edge cases (zero, negative zero, extreme exponents, 15–40 digit multi-limb values, values sitting exactly on rounding boundaries), which get piped into both &lt;code&gt;fuzz_cli&lt;/code&gt; (Rust) and &lt;code&gt;node_driver.js&lt;/code&gt; (pinned original), and the outputs get &lt;code&gt;diff&lt;/code&gt;'d.&lt;/p&gt;

&lt;p&gt;It immediately found two bugs that the entire real test suite had never once exercised:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;exponentiatedBy&lt;/code&gt; hung forever on a non-integer exponent.&lt;/strong&gt; Input: &lt;code&gt;pow,056.56912,-339563.81514905096&lt;/code&gt;. JS validates unconditionally — &lt;code&gt;if (n.c &amp;amp;&amp;amp; !n.isInteger()) throw Error('Exponent not an integer: ' + n)&lt;/code&gt; — &lt;em&gt;before&lt;/em&gt; entering its exponentiation-by-squaring loop. I had no such check, so the integer-halving loop's termination condition was never satisfied. Not a wrong answer. An infinite loop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;toPrecision&lt;/code&gt;/&lt;code&gt;toExponential&lt;/code&gt;/&lt;code&gt;toFixed&lt;/code&gt; had no argument range validation at all.&lt;/strong&gt; &lt;code&gt;toprec,561616,0&lt;/code&gt; succeeded silently where JS raises &lt;code&gt;"Argument out of range: 0"&lt;/code&gt;. JS's &lt;code&gt;intCheck(sd, 1, MAX)&lt;/code&gt; calls are unconditional — not gated behind STRICT mode, which is what I'd assumed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both fixes changed function signatures to return &lt;code&gt;Result&lt;/code&gt;. Both got regression tests.&lt;/p&gt;

&lt;p&gt;Then I re-ran everything from a clean build. The replay held at 139,907/139,907, and two fresh fuzz batches — 60,000 and 500,000 cases — produced &lt;strong&gt;zero diff lines&lt;/strong&gt;. Not "zero failures on the operations where I'd found bugs." Zero differing output bytes across every operation the generator emits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final state: 139,907/139,907 replayed calls matched exactly. 0 panics, 0 divergences. 560,000 random fuzz cases, 0 diffs. 69 unit tests, each checked against captured Node output rather than hand-written expectations. 0 &lt;code&gt;unsafe&lt;/code&gt; blocks.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The performance section, told honestly
&lt;/h2&gt;

&lt;p&gt;Rust is faster. Except where it isn't, and that part matters more.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Node p50&lt;/th&gt;
&lt;th&gt;Rust p50&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;code&gt;plus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7.5 µs&lt;/td&gt;
&lt;td&gt;6.0 µs&lt;/td&gt;
&lt;td&gt;comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;minus&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.6 µs&lt;/td&gt;
&lt;td&gt;5.4 µs&lt;/td&gt;
&lt;td&gt;comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;multipliedBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.4 µs&lt;/td&gt;
&lt;td&gt;7.0 µs&lt;/td&gt;
&lt;td&gt;comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;toFixed&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5.8 µs&lt;/td&gt;
&lt;td&gt;6.2 µs&lt;/td&gt;
&lt;td&gt;comparable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;dividedBy&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20.7 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65.9 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~2.8x slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;squareRoot&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;49.7 µs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;239.7 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~4.8x slower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;startup&lt;/td&gt;
&lt;td&gt;15.58 ms&lt;/td&gt;
&lt;td&gt;0.038 ms&lt;/td&gt;
&lt;td&gt;~413x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;peak RSS&lt;/td&gt;
&lt;td&gt;64.1 MB&lt;/td&gt;
&lt;td&gt;6.6 MB&lt;/td&gt;
&lt;td&gt;~9.6x lower&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Startup and memory go to Rust by a wide margin, and that number deserves no applause — it's a native binary versus a JIT runtime, it's structural, and quoting it as a win about my code would be dishonest.&lt;/p&gt;

&lt;p&gt;The number that &lt;em&gt;is&lt;/em&gt; about my code is &lt;code&gt;dividedBy&lt;/code&gt;, and it's a loss. &lt;strong&gt;My port's division is ~2.8x slower than the JavaScript original&lt;/strong&gt;, and &lt;code&gt;squareRoot&lt;/code&gt; is ~4.8x slower because its Newton-Raphson iteration calls division repeatedly.&lt;/p&gt;

&lt;p&gt;That is the direct, predictable cost of choosing schoolbook digit-string long division over the original's normalised base-1e14 algorithm. I made that call deliberately: the digit-string version is dramatically easier to verify against ground truth, and it &lt;em&gt;was&lt;/em&gt; verified — that's how the &lt;code&gt;49/13&lt;/code&gt; exponent-alignment bug got caught before it contaminated four dependent methods. Given a hard deadline and a correctness bar of byte-identical output, I'd rather ship a slow-and-provably-right division than a fast one I couldn't fully verify.&lt;/p&gt;

&lt;p&gt;But I'm not going to dress a regression up as a feature. It's in the README, in DECISIONS.md, and in the benchmark methodology alongside its caveats (single run, no warm-up discard, machine not isolated from other loads). A benchmark table that only contains your wins isn't a benchmark; it's marketing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision I'd take back:&lt;/strong&gt; this one. Not the choice itself — under that deadline it was right — but the sequencing. I should have shipped the digit-string version to establish correctness, then used the now-verified implementation as an oracle to port the original's limb-based algorithm behind it, with the slow version as the differential reference. That's a few hours of work, and it would have closed the only real regression in the port. I ran out of the window before I ran out of plan.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell someone starting a port tomorrow
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Find the specification before you find the source.&lt;/strong&gt; For bignumber.js, it was 47,713 lines of tests, not 2,520 lines of implementation. Read the tests first. They encode the promises; the source only encodes one way of keeping them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build a differential harness before you build features.&lt;/strong&gt; Every one of my fourteen bugs came from the harness. Zero came from reading my own code. You cannot review your way to byte-identical output on a numerical library — I was confident and wrong fourteen separate times, and confidence was never the signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify the primitives before you build on them.&lt;/strong&gt; Core representation and the two highest-risk primitives went in first, checked against real Node output, before any higher-level method existed. Three bugs died there instead of reproducing into forty methods.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then fuzz, because your fixed test set is somebody's imagination.&lt;/strong&gt; 139,907 real calls did not contain a non-integer exponent. Random generation found it in the first 60,000 cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Distrust your harness as much as your port.&lt;/strong&gt; Two of my "divergences" were the observer, not the observed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down what you didn't do.&lt;/strong&gt; &lt;code&gt;toString(base)&lt;/code&gt; — decimal → base-&lt;code&gt;b&lt;/code&gt; string conversion — is not ported. The &lt;em&gt;reverse&lt;/em&gt; direction is, because the two-argument comparison methods need it. Nothing in the replay exercised the forward direction, so it stays a documented, deliberate scope cut with an &lt;code&gt;unimplemented!()&lt;/code&gt; marking the spot. &lt;code&gt;BigNumber.config()&lt;/code&gt; as global state, &lt;code&gt;isBigNumber&lt;/code&gt;, and &lt;code&gt;toBigInt&lt;/code&gt; are also out, each with a reason. A port with three honest gaps is more useful than one with three gaps you have to discover yourself.&lt;/p&gt;




&lt;p&gt;Porting is not translation. Translation preserves what the code says. A port has to preserve what the code &lt;em&gt;does&lt;/em&gt; — including the parts nobody wrote down, the parts that only exist because &lt;code&gt;Math.pow&lt;/code&gt; disagrees with C99, the parts that depend on two variable names pointing at one object, and the parts that only show up on the eight-thousandth call.&lt;/p&gt;

&lt;p&gt;The original library was the specification. The tests were in archaeology. The 139,907 calls were the proof.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Port:&lt;/strong&gt; &lt;a href="https://github.com/PrinceXDev/port-mortem-bignumber-rust" rel="noopener noreferrer"&gt;github.com/PrinceXDev/port-mortem-bignumber-rust&lt;/a&gt; — every architectural divergence and all fourteen bugs, with root causes, are in &lt;code&gt;DECISIONS.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original:&lt;/strong&gt; &lt;a href="https://github.com/MikeMcl/bignumber.js" rel="noopener noreferrer"&gt;MikeMcl/bignumber.js&lt;/a&gt; (MIT) — a library whose test suite taught me more about numerical correctness than its implementation did, which is the highest compliment I know how to pay a codebase.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>javascript</category>
      <category>hackathonraptors</category>
      <category>opensource</category>
    </item>
    <item>
      <title>ScaleScope: I Built a System That Makes Autoscaling Watchable — Here's What Broke</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:56:18 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/scalescope-watching-autoscaling-actually-happen-in-real-time-30ij</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/scalescope-watching-autoscaling-actually-happen-in-real-time-30ij</guid>
      <description>&lt;p&gt;The autoscaler is doing something. You can't see it.&lt;/p&gt;

&lt;p&gt;You set a CPU threshold. Load increases. A few minutes later, you refresh a dashboard, and the container count has changed. Somewhere in between, a decision happened:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;demand increased → threshold crossed → capacity changed → container started → latency recovered&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;But you weren't there to watch the chain happen. Instead, you reconstruct it afterwards from logs with different timestamps, dashboards with different sampling rates, and metrics that don't share the same clock.&lt;/p&gt;

&lt;p&gt;That gap is why autoscaling still feels like a black box — even when you're the person who configured it.&lt;/p&gt;

&lt;p&gt;So I wanted to build something different. Not another mocked autoscaling dashboard. Not a simulated container counter. I wanted &lt;strong&gt;real load&lt;/strong&gt; hitting a &lt;strong&gt;real service&lt;/strong&gt;, &lt;strong&gt;Zerops making the real scaling decisions&lt;/strong&gt;, and the entire causal chain rendered &lt;strong&gt;live&lt;/strong&gt; while it happens.&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;ScaleScope&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚀 Try It Yourself
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/PrinceXDev/ScaleScope-live-autoscaling-proving-ground" rel="noopener noreferrer"&gt;ScaleScope on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live Demo: &lt;a href="https://web-2e58.prg1.zerops.app/" rel="noopener noreferrer"&gt;scalescope&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&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%2Fl23tu6zyfuh7slggit2u.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%2Fl23tu6zyfuh7slggit2u.png" alt="ScaleScope dashboard showing live tick data and scaling timeline" width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is ScaleScope?
&lt;/h2&gt;

&lt;p&gt;ScaleScope is an experimental autoscaling observability system built around one idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make the entire scaling decision observable on one timeline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You pick a load profile — &lt;strong&gt;Steady&lt;/strong&gt;, &lt;strong&gt;Ramp&lt;/strong&gt;, &lt;strong&gt;Spike&lt;/strong&gt;, or &lt;strong&gt;Latency-target autopilot&lt;/strong&gt; — choose a duration, and start the experiment. From there:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fleet of worker containers sends real HTTP requests to a real target service.&lt;/li&gt;
&lt;li&gt;The target burns real CPU per request.&lt;/li&gt;
&lt;li&gt;Zerops' own autoscaler decides when to scale — nothing is simulated.&lt;/li&gt;
&lt;li&gt;Container count is measured independently, not pulled from a privileged platform API.&lt;/li&gt;
&lt;li&gt;A digital twin predicts the container count 15 seconds into the future.&lt;/li&gt;
&lt;li&gt;Every experiment is recorded as an append-only event stream.&lt;/li&gt;
&lt;li&gt;Runs can be replayed later through the exact same rendering pipeline used live.&lt;/li&gt;
&lt;li&gt;Chaos experiments can kill, degrade, or partition the target mid-run.&lt;/li&gt;
&lt;li&gt;A scheduler can run unattended experiment suites and compare results across runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are twelve services in the system, and yes — I checked that they all actually do something.&lt;/p&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&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%2Feyc8af3edqm4gezqq2rh.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%2Feyc8af3edqm4gezqq2rh.png" alt="System Architecture Diagram" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key architectural decision: &lt;strong&gt;Postgres, ClickHouse, and Valkey are projections of the event log — they are not independent sources of truth.&lt;/strong&gt; Everything traces back to one append-only stream in JetStream.&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%2Fn7gsuui2t5eivnmq78c5.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%2Fn7gsuui2t5eivnmq78c5.png" alt="Flowchart diagram" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's why replay is cheap: &lt;code&gt;replay.js&lt;/code&gt; re-emits the same events — preserving their original inter-arrival gaps — into the exact SSE pipeline used by live traffic. The frontend never branches on &lt;code&gt;isReplay&lt;/code&gt;. It just renders events. Live and replay speak the same language.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Live Experiment, Step by Step
&lt;/h2&gt;

&lt;p&gt;Here's what actually happens when you press &lt;strong&gt;Start Run&lt;/strong&gt;, based on the real implementation in &lt;code&gt;apps/gateway/src/orchestrator.js&lt;/code&gt; and &lt;code&gt;apps/worker/src/fleet.js&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Admission
&lt;/h3&gt;

&lt;p&gt;Before anything starts, the gateway checks two things stored in Valkey: the hourly credit budget and a single-active-run lock. The button is public, and the backend is credit-billed — the budget cannot be an afterthought.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Two-Phase Barrier
&lt;/h3&gt;

&lt;p&gt;This was one of the easiest places to accidentally introduce a race condition. The gateway first opens a subscription for &lt;code&gt;CTRL.READY&lt;/code&gt; acknowledgements, &lt;strong&gt;then&lt;/strong&gt; broadcasts &lt;code&gt;CTRL.PREPARE&lt;/code&gt;. That ordering is deliberate — a fast worker acknowledging before anyone is listening is exactly the kind of race that might never appear during a demo, then suddenly appear under real load.&lt;/p&gt;

&lt;p&gt;Each worker receives &lt;code&gt;PREPARE&lt;/code&gt; and performs one throwaway warm-up request, so DNS resolution and connection-pool initialization don't pollute the first real latency bucket.&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%2Fexda10uyd6v9leev9w9e.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%2Fexda10uyd6v9leev9w9e.png" alt="Sequence Diagram" width="514" height="788"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. One Shared T0
&lt;/h3&gt;

&lt;p&gt;Once enough workers respond (or the timeout fires), the gateway computes an absolute future timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;T0_LEAD_MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CTRL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;workers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every worker derives its per-second buckets from that same timestamp. Five different load-fleet containers can now agree on what "second 7" means without continuously coordinating — instead of &lt;code&gt;worker 1: "7.01s ago"&lt;/code&gt;, &lt;code&gt;worker 2: "6.94s ago"&lt;/code&gt;, &lt;code&gt;worker 3: "7.13s ago"&lt;/code&gt;, they all just tick from the same origin.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Load Starts
&lt;/h3&gt;

&lt;p&gt;Each worker maintains its own concurrency pool against the target, using a shared keep-alive HTTP agent (not &lt;code&gt;fetch&lt;/code&gt;) for deterministic connection reuse. Concurrency is derived from the selected load profile:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Profile&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spike&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Jump to full intensity and hold&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ramp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Increase linearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Step&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Move between predefined plateaus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Autopilot&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A PID controller adjusts request rate every second to hold a target p95 latency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The autopilot controller is intentionally asymmetric — it backs off quickly, but only increases load as fast as &lt;code&gt;maxRise&lt;/code&gt; allows. Ramping quickly is a surprisingly effective way to DoS your own target.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The Target Scales
&lt;/h3&gt;

&lt;p&gt;Every target response carries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-Instance-Id: 7f83c1a2
X-Instance-Age: 4217
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INSTANCE_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&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;BOOT_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Instance-Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;INSTANCE_ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Instance-Age&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;BOOT_MS&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 entire container-counting mechanism — no platform API, no privileged credentials, no hidden autoscaler endpoint. The collector tracks distinct instance IDs observed during a rolling 10-second window, and that becomes the live container count.&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%2Fhqs1vcigfovlt0yppuue.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%2Fhqs1vcigfovlt0yppuue.png" alt="Container count over time compared against the target load profile" width="799" height="372"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Samples Become Tick Frames
&lt;/h3&gt;

&lt;p&gt;Workers publish per-instance samples to NATS every second. The collector merges them into one &lt;code&gt;TickFrame&lt;/code&gt;. For percentiles, the system takes the &lt;strong&gt;worst&lt;/strong&gt; observed p95/p99 across workers rather than pretending percentiles can be averaged; for p50 it uses a request-weighted mean. The resulting frame goes to the gateway, the oracle, and ClickHouse — one second of telemetry becomes one shared representation of system state.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. The Gateway Broadcasts It
&lt;/h3&gt;

&lt;p&gt;The gateway forwards the same tick frame to connected SSE clients, filtered by &lt;code&gt;runId&lt;/code&gt; — a viewer watching a permalink to one experiment never receives events from another active run.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. The Event Log Becomes the Record
&lt;/h3&gt;

&lt;p&gt;Events — &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;armed&lt;/code&gt;, &lt;code&gt;started&lt;/code&gt;, &lt;code&gt;tick&lt;/code&gt;, &lt;code&gt;scaled&lt;/code&gt;, &lt;code&gt;chaos&lt;/code&gt;, &lt;code&gt;prediction&lt;/code&gt;, &lt;code&gt;slo&lt;/code&gt;, &lt;code&gt;completed&lt;/code&gt; — are appended to JetStream. At finalization, the gateway reads the full event stream and folds it into a summary using &lt;strong&gt;one reducer&lt;/strong&gt;: &lt;code&gt;foldRun()&lt;/code&gt;. That same reducer powers the REST API, replay, finalization, the scheduler, and suite success evaluation.&lt;/p&gt;

&lt;p&gt;One event stream. One reducer. Multiple projections. That decision ended up being one of the most important architectural choices in the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Engineering Decisions I'm Most Proud Of
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Counting containers without a platform API
&lt;/h3&gt;

&lt;p&gt;There's no simple application-layer Zerops endpoint that says "you currently have 4 containers" — and even if there were, depending on a privileged API on the hot path would introduce another dependency. Instead every container identifies itself, and distinct IDs in the rolling window become the count. Simple, observable, and independently measured.&lt;/p&gt;

&lt;h3&gt;
  
  
  Forcing horizontal scaling
&lt;/h3&gt;

&lt;p&gt;This one broke the demo before it worked. Zerops scales vertically before scaling horizontally — a sensible default for normal workloads. But ScaleScope's entire premise is &lt;em&gt;watch containers appear&lt;/em&gt;. If the platform keeps adding CPU to the existing container, the container count never changes.&lt;/p&gt;

&lt;p&gt;The fix was one line in the import YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;cpuMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;DEDICATED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dedicated CPU enables the horizontal CPU-trigger behavior the experiment depends on. An earlier version missed this; the system vertically scaled forever, the container count stayed at one, and the demo silently failed at its most important job. A very useful bug to find early.&lt;/p&gt;

&lt;h3&gt;
  
  
  The digital twin doesn't use machine learning
&lt;/h3&gt;

&lt;p&gt;It learns three parameters: capacity per container, scale-up lag, and scale-down lag. That's it. &lt;code&gt;AutoscalerTwin&lt;/code&gt; updates them with exponentially weighted estimates, and capacity updates are intentionally asymmetric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;observed&lt;/span&gt; &lt;span class="o"&gt;&amp;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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacityPerContainer&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;alpha&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;alpha&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&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;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacityPerContainer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt;
  &lt;span class="nx"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;observed&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="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;capacityPerContainer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One overloaded tick shouldn't permanently convince the model that every container is terrible. The model only learns from ticks that are demonstrably not saturated (&lt;code&gt;p95 &amp;lt;= setpoint * 1.1&lt;/code&gt;), and it refuses to persist learning from runs shorter than 20 ticks. I watched this guard fire during local testing for runs with 18, 14, and 2 ticks — no special test case, no prompt, it just refused to learn from insufficient data. Exactly what I wanted.&lt;/p&gt;

&lt;h3&gt;
  
  
  The chaos secret check
&lt;/h3&gt;

&lt;p&gt;The chaos endpoint accepts a secret, compared in a timing-safe way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;presented&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CHAOS_SECRET&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timingSafeEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;timingSafeEqual()&lt;/code&gt; requires equal-length buffers — comparing raw strings of different lengths can throw, and careless padding can leak secret-length information. Hashing both inputs first produces fixed 32-byte buffers before comparison. Small detail, worth getting right.&lt;/p&gt;

&lt;h3&gt;
  
  
  The scheduler doesn't bypass the gateway
&lt;/h3&gt;

&lt;p&gt;The scheduler &lt;em&gt;could&lt;/em&gt; publish &lt;code&gt;CTRL.*&lt;/code&gt; messages directly to NATS. It intentionally doesn't:&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%2Fq7bs6hbrnbtuf40xmwjd.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%2Fq7bs6hbrnbtuf40xmwjd.png" alt="The scheduler doesn't bypass the gateway" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the scheduler became a second writer to the NATS control subjects, the two paths would eventually drift, bypassing credit limits, active-run locking, admission checks, and barrier logic. So the scheduler behaves like any other client — it calls the public API and polls for progress.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Broke
&lt;/h2&gt;

&lt;p&gt;Three real bugs survived static checks and careful code reading, and were only discovered when the whole system actually ran.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. A Postgres type mismatch.&lt;/strong&gt; One update statement used the same parameter as a &lt;code&gt;bigint&lt;/code&gt; in one clause and implicitly as &lt;code&gt;numeric&lt;/code&gt; inside &lt;code&gt;to_timestamp()&lt;/code&gt; in another. It read fine, passed syntax checks, and the real driver rejected it on the very first &lt;code&gt;POST /api/runs&lt;/code&gt;. Nearly invisible in review — humans are good at inferring intent, databases are good at enforcing types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every log line said &lt;code&gt;[svc]&lt;/code&gt;.&lt;/strong&gt; The telemetry package cached &lt;code&gt;SCALESCOPE_SERVICE&lt;/code&gt; at module-load time. ES module imports evaluate before the entry point's own initialization code runs, so by the time each service set its environment variable, the logger had already cached the default. Result: eight services, every log line labelled &lt;code&gt;[svc]&lt;/code&gt;. Nothing technically broken, but debugging a distributed system where every service has the same name is painful. Fix: read the environment value at log time instead of caching it at import time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The status panel "fixed itself" into being wrong.&lt;/strong&gt; The architecture panel did a direct health check, then ~10 seconds later, some services would mysteriously flip to &lt;code&gt;unknown&lt;/code&gt;. A heartbeat reconciliation loop was overwriting the direct health result with data from a heartbeat table that nobody was populating for those services:&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%2Fw9i8hky0cdce8u8pdc20.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%2Fw9i8hky0cdce8u8pdc20.png" alt="FlowChart" width="799" height="87"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was technically reconciling state — just reconciling it with nothing. Fix: exclude those services from that reconciliation path.&lt;/p&gt;

&lt;p&gt;None of these three bugs was caught by &lt;code&gt;node --check&lt;/code&gt;. All twelve services passed syntax checks early. Once you have event streams, distributed workers, barriers, asynchronous services, databases, caches, and queues, you need to run the actual system — not just the files.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned Building ScaleScope
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Static checks tell you surprisingly little about distributed systems.&lt;/strong&gt; A service can be perfectly valid JavaScript and still be completely wrong when it interacts with seven other services. The bugs weren't syntax bugs — they were system interaction bugs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. One event log beats three sources of truth.&lt;/strong&gt; Making Postgres, ClickHouse, and Valkey projections of one JetStream event stream dramatically simplified replay. If every database independently decided what happened, replay would need a completely different code path. Instead, everything is a projection, and the event stream is the history.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;ScaleScope started with a simple question: &lt;em&gt;What if you could actually watch an autoscaler think?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The result is a distributed experiment platform for making scaling behaviour observable, replayable, predictable, and deliberately breakable. There's still work left to do, but the core loop works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start a run → generate real load → watch Zerops scale → observe the causal chain → replay it afterward.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And honestly, watching that second container appear for the first time was worth every bug that came before it.&lt;/p&gt;

</description>
      <category>wemakedevs</category>
      <category>distributedsystems</category>
      <category>node</category>
      <category>zerops</category>
    </item>
    <item>
      <title>Grandma's Kitchen Table — A Photorealistic Pie Scene</title>
      <dc:creator>Prince Panchani</dc:creator>
      <pubDate>Tue, 04 Aug 2026 10:03:49 +0000</pubDate>
      <link>https://dev.to/prince_panchani_f971a20ec/grandmas-kitchen-table-a-photorealistic-pie-scene-52j6</link>
      <guid>https://dev.to/prince_panchani_f971a20ec/grandmas-kitchen-table-a-photorealistic-pie-scene-52j6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is my submission for the &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge – Comfort Food Edition: CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Grandma's Kitchen Table
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Inspiration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Comfort food isn't just about what's on the plate—it's about the memories that come with it.&lt;/p&gt;

&lt;p&gt;Growing up, Sunday mornings at my grandmother's house always meant the same scene: a freshly baked lattice pie cooling by the window, tea that had long gone cold because she was busy taking care of everyone else, handwritten recipe cards scattered across the table, and cookies that somehow always appeared beside every cup.&lt;/p&gt;

&lt;p&gt;Rather than creating a perfect food photograph, I wanted to recreate the feeling of sitting at that table.&lt;/p&gt;

&lt;p&gt;That's why the pie already has a slice missing. There are crumbs where they'd naturally fall, a lace napkin doubling as a coaster, coffee rings on the recipe card, and a handwritten note tucked into the corner that reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Eat more pie! Love, Grandma ♥"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My goal wasn't simply to draw food—it was to capture nostalgia using nothing but CSS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://princexdev.github.io/css-art-comfort-food/" rel="noopener noreferrer"&gt;https://princexdev.github.io/css-art-comfort-food/&lt;/a&gt;&lt;br&gt;
💻 &lt;strong&gt;Source Code:&lt;/strong&gt; &lt;a href="https://github.com/PrinceXDev/css-art-comfort-food" rel="noopener noreferrer"&gt;https://github.com/PrinceXDev/css-art-comfort-food&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The entire artwork is built using &lt;strong&gt;pure CSS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ No images&lt;/li&gt;
&lt;li&gt;❌ No SVG&lt;/li&gt;
&lt;li&gt;❌ No Canvas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is created using gradients, masks, pseudo-elements, transforms, shadows, and modern CSS features.&lt;/p&gt;

&lt;p&gt;A small amount of JavaScript is used only for subtle interactions, including parallax, drifting steam, and a real-time pocket watch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;What began as a simple pie illustration gradually evolved into an entire lived-in kitchen table.&lt;/p&gt;

&lt;p&gt;Instead of asking, &lt;em&gt;"What food should I draw?"&lt;/em&gt; I kept asking, &lt;em&gt;"What would actually be sitting on Grandma's table?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That question transformed the project into a detailed composition containing nearly twenty handcrafted objects, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lattice pie with a sliced serving&lt;/li&gt;
&lt;li&gt;Melting ice cream&lt;/li&gt;
&lt;li&gt;Floral teacup&lt;/li&gt;
&lt;li&gt;Cookies&lt;/li&gt;
&lt;li&gt;Sugar bowl&lt;/li&gt;
&lt;li&gt;Jam pot&lt;/li&gt;
&lt;li&gt;Milk jug&lt;/li&gt;
&lt;li&gt;Pie server&lt;/li&gt;
&lt;li&gt;Folded newspaper&lt;/li&gt;
&lt;li&gt;Reading glasses&lt;/li&gt;
&lt;li&gt;Pocket watch&lt;/li&gt;
&lt;li&gt;Brass keys&lt;/li&gt;
&lt;li&gt;Wildflowers&lt;/li&gt;
&lt;li&gt;Handwritten recipe card&lt;/li&gt;
&lt;li&gt;Grandma's note&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every object was built individually using CSS before being composed into a single scene.&lt;/p&gt;

&lt;h3&gt;
  
  
  Highlights
&lt;/h3&gt;

&lt;p&gt;🥧 &lt;strong&gt;A Real Woven Pie Lattice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest technical challenges was creating a convincing woven lattice.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;clip-path&lt;/code&gt; wasn't enough because it couldn't produce alternating visible sections without merging them into a single shape.&lt;/p&gt;

&lt;p&gt;Instead, I used CSS &lt;code&gt;mask&lt;/code&gt; with hard-stop gradients to selectively hide portions of each strip, allowing the vertical and horizontal pieces to weave naturally over and under one another—just like a real pie.&lt;/p&gt;




</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
