<?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: Giovambattista Fazioli</title>
    <description>The latest articles on DEV Community by Giovambattista Fazioli (@undolog).</description>
    <link>https://dev.to/undolog</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%2F2274629%2F3b560af3-80cf-412f-8373-cf54a218228e.jpeg</url>
      <title>DEV Community: Giovambattista Fazioli</title>
      <link>https://dev.to/undolog</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/undolog"/>
    <language>en</language>
    <item>
      <title>Netfox 0.23.0 — the devices that never speak</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Thu, 17 Sep 2026 13:09:16 +0000</pubDate>
      <link>https://dev.to/undolog/netfox-0230-the-devices-that-never-speak-3o7o</link>
      <guid>https://dev.to/undolog/netfox-0230-the-devices-that-never-speak-3o7o</guid>
      <description>&lt;p&gt;There is a particular kind of thing on your network that Netfox could never show you, and it was not a gap in the feature list. It was structural.&lt;/p&gt;

&lt;p&gt;Netfox found devices by &lt;strong&gt;listening&lt;/strong&gt;. Some things announce themselves; others give themselves away by exchanging a packet with something. Between the two, most of a home network turns up. But a device that is switched on, quiet, and has never had a reason to talk to your Mac produces neither signal — so it was not merely hard to find, it was unreachable by construction. A printer asleep. A camera that only ever phones its own cloud. Something you set up months ago and stopped thinking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Now it can ask
&lt;/h2&gt;

&lt;p&gt;Give Netfox a range of addresses and it sends one small question to each. Whatever answers turns up as an ordinary device — its identity, its history, its alerts, all of it — because the sweep does not create a parallel list, it feeds the one you already have.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zwicfbb9f17rul5cwmb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zwicfbb9f17rul5cwmb.webp" alt="The Devices list with Demo Mode on. The new range control sits in the toolbar, right of the Online/Offline filter; a sweep adds to this list rather than opening one of its own." width="800" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It stays inside your own subnet, needs no password and installs nothing. A range that reaches outside is refused rather than trimmed to fit, because a trimmed range asks about a different set of addresses than the one you typed, and nothing on screen would tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The range is also a filter, and that is the important half
&lt;/h2&gt;

&lt;p&gt;Set it to two addresses and the list shows those two devices. Nothing else.&lt;/p&gt;

&lt;p&gt;This started as a way to bound how long a sweep takes and turned out to be the thing that makes the feature trustworthy: &lt;strong&gt;you can see what it found.&lt;/strong&gt; Set a narrow range, look, and know. While a range is active the control that set it says so and offers to clear it, and the footer carries both numbers — &lt;em&gt;2 in range · 33 on the network&lt;/em&gt; — because a filter you have forgotten looks exactly like a network that has lost thirty devices, and nothing else on screen would disagree with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is holding that port on your own Mac
&lt;/h2&gt;

&lt;p&gt;Open the Ports tab for your own machine and each row now names what holds the port: the process, the user it runs as, its ID. A click deeper says how it is managed, and whether closing it can be undone.&lt;/p&gt;

&lt;p&gt;That last one is worth dwelling on, because it is the difference between a decision and a one-way door. Netfox asks your Mac rather than guessing, and the honest answer is usually no: most services, once you stop them, stay stopped until something asks for them again. On the machine this was measured on, &lt;strong&gt;only 2 of 36 running services were set to restart themselves.&lt;/strong&gt; A tool that told you "it'll come back" would have been wrong about the other 34.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things you will notice
&lt;/h2&gt;

&lt;p&gt;Inspecting a port that has closed since the last scan no longer keeps you waiting. A refusal is an answer, and it arrives in about a millisecond — it was being read as a silence, so every inspection sat out its full timeout behind a spinner, on something you had just clicked.&lt;/p&gt;

&lt;p&gt;And two things on the Overview stopped moving about: the summary cards now reach the right edge of the window instead of leaving an empty column beside them, and the line under the Wi-Fi figures keeps one height whether the rate is holding, has just renegotiated, or has not been measured twice yet. A fast radio drifting between 864 Mb/s and 1.4 Gb/s was resizing the whole card every couple of seconds.&lt;/p&gt;




&lt;p&gt;Netfox is free, a universal binary (Apple Silicon + Intel), runs on macOS 15.6+, and is signed and notarized.&lt;/p&gt;

&lt;p&gt;Netfox for macOS — Website: &lt;a href="https://netfox.app" rel="noopener noreferrer"&gt;https://netfox.app&lt;/a&gt; · Download: &lt;a href="https://netfox.app/download" rel="noopener noreferrer"&gt;https://netfox.app/download&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/netfox-0-23-0-the-devices-that-never-speak-c749m?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>macos</category>
      <category>network</category>
      <category>security</category>
      <category>tooling</category>
    </item>
    <item>
      <title>octoscope 0.35.0 — your activity, in the right order</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:03:22 +0000</pubDate>
      <link>https://dev.to/undolog/octoscope-0350-your-activity-in-the-right-order-47jf</link>
      <guid>https://dev.to/undolog/octoscope-0350-your-activity-in-the-right-order-47jf</guid>
      <description>&lt;h2&gt;
  
  
  Your activity, now in --json and --plain
&lt;/h2&gt;

&lt;p&gt;octoscope has two halves. There is the dashboard you sit in front of, and there is the quiet half — &lt;code&gt;--json&lt;/code&gt; and &lt;code&gt;--plain&lt;/code&gt; — which exists so you can pipe octoscope into something else: a status bar, a cron job, a script that nudges you when something needs attention.&lt;/p&gt;

&lt;p&gt;Until now that quiet half could tell you everything about your repositories and nothing about &lt;em&gt;you&lt;/em&gt;. The Activity tab was screen-only.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--activity&lt;/code&gt; changes that. Same events, same fields, in both output formats.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;octoscope &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--activity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One small decision that will matter if you build on it: when you do not ask for the feed, the &lt;code&gt;recent_activity&lt;/code&gt; key is &lt;strong&gt;absent&lt;/strong&gt;, not empty. An empty list would be a claim — "we looked, there was nothing" — and that claim would be false. A missing key lets your script tell &lt;em&gt;did not fetch&lt;/em&gt; from &lt;em&gt;nothing happened&lt;/em&gt;. And when you do ask and the fetch fails, that is an error, not silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feed was never in chronological order
&lt;/h2&gt;

&lt;p&gt;Here is the part nobody went looking for.&lt;/p&gt;

&lt;p&gt;While wiring the feed into the reports, we checked the order. It was wrong. Not occasionally — &lt;strong&gt;27 of 99&lt;/strong&gt; adjacent events came back out of sequence, then 28 in the next feed, then 25 in the one after that.&lt;/p&gt;

&lt;p&gt;The events endpoint is not documented as returning a sorted feed, and it does not. octoscope had been trusting the order it was handed, which means the Activity tab has been showing you an &lt;em&gt;almost&lt;/em&gt; sorted list this whole time. That is worse than an obviously unsorted one, because it reads as correct.&lt;/p&gt;

&lt;p&gt;It is sorted now, at the point where events are parsed — so the tab and the reports are fixed by the same change. If you have ever glanced at Activity and thought "hang on, didn't I do that after the other thing?", you were right.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scan reads the repository's own settings
&lt;/h2&gt;

&lt;p&gt;The supply-chain scan watches for workflows triggered by things outsiders can send: issues, discussions, discussion comments, forks. It now checks whether the repository actually &lt;em&gt;allows&lt;/em&gt; those things.&lt;/p&gt;

&lt;p&gt;A workflow reacting to discussions on a repository with discussions turned off is not an exposure. Neither is &lt;code&gt;issues&lt;/code&gt; on a repository only your collaborators can open one on.&lt;/p&gt;

&lt;p&gt;This moves scores in &lt;strong&gt;both&lt;/strong&gt; directions, which is worth saying plainly rather than selling as an improvement: you will see fewer findings where a repository's settings make a trigger unreachable, and &lt;strong&gt;more&lt;/strong&gt; on private and internal repositories, where the axis used to stay quiet — which is exactly the case where someone with read access is the supplier worth worrying about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in this release
&lt;/h2&gt;

&lt;p&gt;The What's new tab had a bug where a long title ran off the edge of the pane instead of wrapping. Fixing the renderer also repairs the entries that shipped back in 0.34.1, not just this release's.&lt;/p&gt;

&lt;p&gt;And on macOS, a release is now verified &lt;em&gt;before&lt;/em&gt; it becomes downloadable rather than after: the signature, the hardened runtime and Apple's notarization are all checked against the files that were actually uploaded, and only then does the release go public.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew upgrade gfazioli/tap/octoscope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;Site: &lt;a href="https://gfazioli.github.io/octoscope" rel="noopener noreferrer"&gt;https://gfazioli.github.io/octoscope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Newsletter: &lt;a href="https://octoscope.substack.com" rel="noopener noreferrer"&gt;https://octoscope.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mastodon: &lt;a href="https://hachyderm.io/@undolog" rel="noopener noreferrer"&gt;https://hachyderm.io/@undolog&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/octoscope-0-35-0-your-activity-in-the-right-order-kqwwc?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bubbletea</category>
      <category>cli</category>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>The two gaps from last time are closed</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Mon, 14 Sep 2026 13:56:57 +0000</pubDate>
      <link>https://dev.to/undolog/the-two-gaps-from-last-time-are-closed-a0p</link>
      <guid>https://dev.to/undolog/the-two-gaps-from-last-time-are-closed-a0p</guid>
      <description>&lt;h2&gt;
  
  
  🪟 Window 3.2.0 — the edges of a gesture
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Window&lt;/code&gt; now has the full set of six lifecycle callbacks, matching core's &lt;code&gt;FloatingWindow&lt;/code&gt;: &lt;code&gt;onDragStart&lt;/code&gt;, &lt;code&gt;onDragEnd&lt;/code&gt;, &lt;code&gt;onResizeStart&lt;/code&gt; and &lt;code&gt;onResizeEnd&lt;/code&gt; alongside the two change callbacks it already had.&lt;/p&gt;

&lt;p&gt;The two &lt;em&gt;change&lt;/em&gt; callbacks fire on every frame of a gesture, so anything you wanted to do &lt;strong&gt;once&lt;/strong&gt; — persist a layout, pause an expensive child, show a snap guide, record a single undo entry — meant debouncing by hand and guessing when the user had stopped. Fully additive: four optional props, no default changed. A mousedown on an interactive child or a &lt;code&gt;data-no-window-drag&lt;/code&gt; region still does not open a drag, so no start event fires there, and a resize never emits drag callbacks. &lt;a href="https://github.com/gfazioli/mantine-window/issues/47" rel="noopener noreferrer"&gt;Closes #47&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ferw0q2gtpnnp8yoqp7xz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ferw0q2gtpnnp8yoqp7xz.webp" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://gfazioli.github.io/mantine-window/" rel="noopener noreferrer"&gt;Docs and live demo&lt;/a&gt; · 📦 &lt;a href="https://www.npmjs.com/package/@gfazioli/mantine-window" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · 🏷️ &lt;a href="https://github.com/gfazioli/mantine-window/releases/tag/3.2.0" rel="noopener noreferrer"&gt;Release 3.2.0&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  📋 ListViewTable 4.1.10 — bulk actions, with no new API
&lt;/h2&gt;

&lt;p&gt;Mantine 9.6's &lt;code&gt;ActionBar&lt;/code&gt; is built to be driven by a table selection, and &lt;code&gt;ListViewTable&lt;/code&gt; already had &lt;code&gt;selectionMode&lt;/code&gt;, &lt;code&gt;selectedRows&lt;/code&gt; and &lt;code&gt;onSelectionChange&lt;/code&gt;. There is now a demo showing the two composing: &lt;code&gt;opened&lt;/code&gt; follows the length of the selection, the buttons act on the records handed to &lt;code&gt;onSelectionChange&lt;/code&gt;, and &lt;code&gt;ActionBar.CloseButton&lt;/code&gt; clears it through &lt;code&gt;onClose&lt;/code&gt;. Nothing was added to the library — the capability was there, the demonstration was not. Closes #41.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft67vhndxdvyti0r2qs4r.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft67vhndxdvyti0r2qs4r.webp" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://gfazioli.github.io/mantine-list-view-table/" rel="noopener noreferrer"&gt;Docs and live demo&lt;/a&gt; · 📦 &lt;a href="https://www.npmjs.com/package/@gfazioli/mantine-list-view-table" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · 🏷️ &lt;a href="https://github.com/gfazioli/mantine-list-view-table/releases/tag/4.1.10" rel="noopener noreferrer"&gt;Release 4.1.10&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  ✨ BorderAnimate 3.0.2 — where the border actually goes
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;BorderAnimate&lt;/code&gt; draws its border against its own root. The Input Fields demo wrapped a whole &lt;code&gt;TextInput&lt;/code&gt; — an &lt;code&gt;Input.Wrapper&lt;/code&gt;: label, description, control and error in one box — so the animated border enclosed the label and a radius set to match the input traced a much taller box. The rule, now in the docs: &lt;code&gt;Input.Wrapper&lt;/code&gt; holds the label, &lt;code&gt;BorderAnimate&lt;/code&gt; holds only the &lt;code&gt;Input&lt;/code&gt;. The same shape had leaked into the Triggers demo, where the &lt;code&gt;focus-within&lt;/code&gt; card was a &lt;code&gt;TextInput&lt;/code&gt; while its siblings were &lt;code&gt;Paper&lt;/code&gt; + &lt;code&gt;Text&lt;/code&gt;; all three are now built the same way. Closes &lt;a href="https://github.com/gfazioli/mantine-border-animate/issues/33" rel="noopener noreferrer"&gt;#33&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evdrovvxzcg8f1nnnnj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7evdrovvxzcg8f1nnnnj.webp" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://gfazioli.github.io/mantine-border-animate/" rel="noopener noreferrer"&gt;Docs and live demo&lt;/a&gt; · 📦 &lt;a href="https://www.npmjs.com/package/@gfazioli/mantine-border-animate" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · 🏷️ &lt;a href="https://github.com/gfazioli/mantine-border-animate/releases/tag/3.0.2" rel="noopener noreferrer"&gt;Release 3.0.2&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🃏 Flip 4.0.14 — the picker that was unstyled for months
&lt;/h2&gt;

&lt;p&gt;The credit-card demo's &lt;code&gt;MonthPickerInput&lt;/code&gt; rendered fine and came up bare: &lt;code&gt;@mantine/dates&lt;/code&gt; was a declared dependency but its stylesheet was never imported in the docs bootstrap. Nothing fails when that import is missing, which is why it survived.&lt;/p&gt;

&lt;p&gt;📚 &lt;a href="https://gfazioli.github.io/mantine-flip/" rel="noopener noreferrer"&gt;Docs and live demo&lt;/a&gt; · 📦 &lt;a href="https://www.npmjs.com/package/@gfazioli/mantine-flip" rel="noopener noreferrer"&gt;npm&lt;/a&gt; · 🏷️ &lt;a href="https://github.com/gfazioli/mantine-flip/releases/tag/4.0.14" rel="noopener noreferrer"&gt;Release 4.0.14&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🔧 Mantine 9.6.1, everywhere
&lt;/h2&gt;

&lt;p&gt;Mantine 9.6.1 and React 19.3 across every component, both starter templates and the Hub, with Next 16.3.5 and a round of tooling updates. Peer ranges unchanged (&lt;code&gt;&amp;gt;=9.0.0&lt;/code&gt;). Templates: &lt;a href="https://github.com/gfazioli/next-app-nextra-template/releases/tag/v6.0.16" rel="noopener noreferrer"&gt;Nextra starter 6.0.16&lt;/a&gt;, &lt;a href="https://github.com/gfazioli/next-app-fumadocs-template/releases/tag/v1.0.10" rel="noopener noreferrer"&gt;Fumadocs starter 1.0.10&lt;/a&gt;, Hub 1.0.15.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://mantine-extensions.vercel.app/" rel="noopener noreferrer"&gt;Browse every component on the Hub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠️ Infrastructure
&lt;/h2&gt;

&lt;p&gt;Every GitHub Action is pinned to a commit SHA with the version as a trailing comment, with Dependabot opening a grouped PR when an action ships a release. The bot configuration is now byte-identical across every component repository, and a repository that had been running a second update bot in parallel was brought back in line — two bots proposing overlapping updates produced pull requests that were obsolete by the time anyone read them.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 Pro Tip
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A Mantine package used in your app needs its stylesheet imported, and nothing fails if it isn't.&lt;/strong&gt; Declaring &lt;code&gt;@mantine/dates&lt;/code&gt; and rendering a &lt;code&gt;MonthPickerInput&lt;/code&gt; gets you a working, interactive, &lt;em&gt;unstyled&lt;/em&gt; component — no error, no warning, no failed build.&lt;/p&gt;

&lt;p&gt;The part worth keeping is how to check it rather than squinting at a screenshot. Mantine's built CSS ships &lt;strong&gt;hashed&lt;/strong&gt; class names (&lt;code&gt;.m_14c23465&lt;/code&gt;), not the readable &lt;code&gt;mantine-MonthPicker-*&lt;/code&gt; seen in devtools, so grepping loaded stylesheets for &lt;code&gt;"PickerControl"&lt;/code&gt; finds nothing whether or not the import is there — it looks like the bug either way. Pick a hashed selector present in that package's &lt;code&gt;styles.css&lt;/code&gt; and in no other, then count matching loaded rules: it went from &lt;strong&gt;0 to 10&lt;/strong&gt; with one import line. A number you can read beats an impression of "looks about right".&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/the-two-gaps-from-last-time-are-closed-ttxsq?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>FinderGit 0.31.0 — Your tags and your releases, side by side</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Thu, 10 Sep 2026 07:18:38 +0000</pubDate>
      <link>https://dev.to/undolog/findergit-0310-your-tags-and-your-releases-side-by-side-1gof</link>
      <guid>https://dev.to/undolog/findergit-0310-your-tags-and-your-releases-side-by-side-1gof</guid>
      <description>&lt;p&gt;There’s a small lie every Git tool tells you, and it’s so quiet that most of us stopped noticing.&lt;/p&gt;

&lt;p&gt;When you ask for your tags, you get the ones in &lt;em&gt;your&lt;/em&gt; clone. Not the ones on the remote — the ones on your disk. A tag you cut this morning and never pushed looks exactly like one everybody on the team has had for a year. A tag a colleague pushed an hour ago simply isn’t there, and nothing tells you to go looking. The list is honest about what it contains and silent about what it means, and the difference between those two things is where releases go wrong.&lt;/p&gt;

&lt;p&gt;FinderGit 0.31.0 puts that difference on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Every tag now says where it is&lt;/strong&gt;
&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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zYEe%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Faabc8dbd-f4d7-4ebb-afd0-539266ad04de_2000x1282.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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21zYEe%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Faabc8dbd-f4d7-4ebb-afd0-539266ad04de_2000x1282.png" title="The new Release tab: tags on the left, the releases cut from them on the right, and a presence badge on every row." alt="The new Release tab: tags on the left, the releases cut from them on the right, and a presence badge on every row." width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The new Release tab: tags on the left, the releases cut from them on the right, and a presence badge on every row.&lt;/p&gt;

&lt;p&gt;A new tab in the detail panel pairs every tag in your repository with the release cut from it, one row per version, the two columns side by side. The rows worth looking at are the ones where the two halves disagree: a tag nobody ever cut a release from, a release whose tag isn’t in your clone, a tag that exists here and nowhere else.&lt;/p&gt;

&lt;p&gt;Each row is marked &lt;strong&gt;on both&lt;/strong&gt;, &lt;strong&gt;local only&lt;/strong&gt;, or &lt;strong&gt;not fetched&lt;/strong&gt; — and the buttons follow from that rather than being drawn regardless. Push appears where there is something to push. Fetch appears where there is something to bring down. Neither is drawn where pressing it would do nothing at all. One press brings down the whole backlog.&lt;/p&gt;

&lt;p&gt;Select several rows — ⇧-click, ⌘-click, the way you’d expect — and act on all of them: push, delete locally, delete on the remote. Ten pushes where three fail will tell you &lt;em&gt;which three&lt;/em&gt;, and what Git actually said about each one, because “already exists” and “denied” are entirely different problems with entirely different fixes. There’s a Stop, and when you use it, the rows that were never attempted are reported as exactly that instead of being quietly counted as failures.&lt;/p&gt;

&lt;p&gt;And when you delete a tag, the dialog tells you what that really costs — which turned out to be worth measuring rather than assuming. Deleting a tag on GitHub doesn’t delete its release: it turns the release into a draft, which vanishes from your public releases page. Pushing the tag back does &lt;strong&gt;not&lt;/strong&gt; bring it back. The confirmation now tells you which case you’re in, and on the rare occasion it can’t know, it says so rather than guessing.&lt;/p&gt;

&lt;p&gt;Long-lived repositories were kept in mind: there’s a filter across tag names and messages, and the list is ordered newest-first whichever side supplies the date — so a tag a colleague pushed last week that you’ve never fetched sits at the top, where it belongs, instead of alphabetically buried.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Worktrees are repositories now&lt;/strong&gt;
&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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21q1OC%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F489e4338-5b80-4858-9aca-c2c5905e5341_2000x1282.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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21q1OC%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F489e4338-5b80-4858-9aca-c2c5905e5341_2000x1282.png" title="The Branches tab, with the new Worktrees section at the bottom — here with none linked, explaining what one is for." alt="The Branches tab, with the new Worktrees section at the bottom — here with none linked, explaining what one is for." width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Branches tab, with the new Worktrees section at the bottom — here with none linked, explaining what one is for.&lt;/p&gt;

&lt;p&gt;If you use linked worktrees to keep a second branch checked out in its own folder, FinderGit now treats each one as a repository like any other: it shows up in the browser, carries its own status, and both Overviews count it. The Branches tab has gained a Worktrees section listing every worktree of the repository and the branch each one is holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;A detail panel you can actually read&lt;/strong&gt;
&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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GvxT%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F98bc5fbd-3384-4267-8ca9-86f3d226eb29_2000x1282.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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21GvxT%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F98bc5fbd-3384-4267-8ca9-86f3d226eb29_2000x1282.png" title="The detail panel's tab strip: the selected tab is filled with the colour its panel starts from, so the two read as one surface." alt="The detail panel's tab strip: the selected tab is filled with the colour its panel starts from, so the two read as one surface." width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The detail panel’s tab strip: the selected tab is filled with the colour its panel starts from, so the two read as one surface.&lt;/p&gt;

&lt;p&gt;The tab strip had become nine coloured pills all competing for your attention, with the selected one differing from the rest only in saturation — inside a box that visibly sat apart from the panel it was labelling. At a glance you couldn’t tell what you were looking at.&lt;/p&gt;

&lt;p&gt;Now the unselected tabs are quiet, and the selected one is filled with the colour its panel starts from, so the tab and its content read as a single surface. The order changed too, into something that follows how you actually work: what you’re doing right now, then this repository’s refs, then what the forge has to say, then looking after the place.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Fixes&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Issues and Pull Requests could show you a stale answer.&lt;/strong&gt; When two loads overlapped, the older one could land last and overwrite the newer. The newest load now wins, in both tabs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A large gap in the translations closed.&lt;/strong&gt; A count of user-facing strings — 189 of them — had never made it into the translation catalogue at all, so they stayed in English in every language while the coverage report happily read 100%. It was right, on its own terms: a string that never became a key can’t be reported as missing. They’re in now, and translated. Tags and releases are fully translated too, including every confirmation dialog, and the Italian word for a linked worktree has been corrected.&lt;/p&gt;




&lt;p&gt;FinderGit is free, a universal binary (Apple Silicon + Intel), runs on macOS 15+, and is signed and notarized.&lt;/p&gt;

&lt;p&gt;FinderGit for macOS — Website: &lt;a href="https://findergit.app" rel="noopener noreferrer"&gt;https://findergit.app&lt;/a&gt; · Download: &lt;a href="https://findergit.app/download" rel="noopener noreferrer"&gt;https://findergit.app/download&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Published via &lt;a href="https://zyvop.com/findergit-0-31-0-your-tags-and-your-releases-side-by-side-1dwxk?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt; — Write once in Markdown, auto-backup to GitHub, and syndicate to Dev.to, Medium &amp;amp; Hashnode in 1 click.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>swift</category>
      <category>macos</category>
      <category>git</category>
      <category>github</category>
    </item>
    <item>
      <title>The test that could only pass, and the warning that admitted it didn't know</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Wed, 09 Sep 2026 08:21:00 +0000</pubDate>
      <link>https://dev.to/undolog/the-test-that-could-only-pass-and-the-warning-that-admitted-it-didnt-know-246p</link>
      <guid>https://dev.to/undolog/the-test-that-could-only-pass-and-the-warning-that-admitted-it-didnt-know-246p</guid>
      <description>&lt;p&gt;&lt;em&gt;FinderGit is a Git-aware file browser for macOS: every repository you own in one window, with status, branches, issues and pull requests inline. This story comes from building a new tab in it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzf5n7mr9jctdx67b2kai.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzf5n7mr9jctdx67b2kai.webp" alt="Findergit" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I shipped two sentences last week that looked like verification and were not. One was a test. One was a warning in a dialog. They failed in the same way, and finding the second one only happened because the first one taught me what to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: a test that was green for the wrong reason
&lt;/h2&gt;

&lt;p&gt;I was adding bulk actions to a tag list — select ten tags, push them, delete them. Bulk operations need a Stop button, and a stopped run has to report the difference between &lt;em&gt;this failed&lt;/em&gt; and &lt;em&gt;we never got to it&lt;/em&gt;. A red mark next to a tag because you pressed Stop reads as "the app broke", which is both wrong and the opposite of reassuring.&lt;/p&gt;

&lt;p&gt;So the outcome type has three cases, not two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;struct&lt;/span&gt; &lt;span class="kt"&gt;BulkResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;enum&lt;/span&gt; &lt;span class="kt"&gt;Kind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancelled&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Kind&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&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;And here is the test I wrote for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;@Test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A stopped run reports the rest as not attempted, never as failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;cancellationIsNotFailure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;throws&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="kt"&gt;Fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c4"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;RepoTagStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runBulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteLocal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;

    &lt;span class="cp"&gt;#expect(results.count == 4)&lt;/span&gt;
    &lt;span class="cp"&gt;#expect(results.allSatisfy { $0.kind != .failure })&lt;/span&gt;
    &lt;span class="cp"&gt;#expect(results.contains { $0.kind == .cancelled })&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green. Every assertion passes. And it proves nothing at all.&lt;/p&gt;

&lt;p&gt;The cancel lands before the loop reaches its first item, so &lt;code&gt;runBulk&lt;/code&gt; sees &lt;code&gt;Task.isCancelled&lt;/code&gt; on iteration zero and marks &lt;strong&gt;all four&lt;/strong&gt; &lt;code&gt;.cancelled&lt;/code&gt;. &lt;code&gt;allSatisfy { $0.kind != .failure }&lt;/code&gt; is trivially true when nothing ran. &lt;code&gt;contains { $0.kind == .cancelled }&lt;/code&gt; is trivially true when everything is cancelled. The mixed outcome — some done, then Stop, then the rest — is the only case a user ever produces, and the test never touched it.&lt;/p&gt;

&lt;p&gt;The rewrite cancels from &lt;em&gt;inside&lt;/em&gt; the progress callback, after two items have gone through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;@unchecked&lt;/span&gt; &lt;span class="kt"&gt;Sendable&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;BulkResult&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kt"&gt;Never&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;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;box&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runBulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteLocal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"c4"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&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="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;done&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;skipped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancelled&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="cp"&gt;#expect(!done.isEmpty,    "the work done before the stop must be kept")&lt;/span&gt;
&lt;span class="cp"&gt;#expect(!skipped.isEmpty, "and the rest reported as not attempted")&lt;/span&gt;
&lt;span class="cp"&gt;#expect(done.count + skipped.count == 4)&lt;/span&gt;

&lt;span class="c1"&gt;// And the repository agrees: exactly the ones reported done are gone.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tagNames&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;#expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;skipped&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;#expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&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;It went red immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Expectation failed: &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;.count + skipped.count → 3&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; 4
Expectation failed: results.allSatisfy &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;.kind &lt;span class="o"&gt;!=&lt;/span&gt; .failure &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of four accounted for. One result was &lt;code&gt;.failure&lt;/code&gt; — and there was no failure. It was the tag &lt;strong&gt;in flight&lt;/strong&gt; at the moment Stop was pressed: the child process gets signalled, &lt;code&gt;git tag -d&lt;/code&gt; dies, and my code read that as the tag failing to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix was a rule the codebase had already written down
&lt;/h2&gt;

&lt;p&gt;Here is what my bulk runner did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;runRaw&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deleteTag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repoURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;BulkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;success&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// ← the error's TYPE is gone&lt;/span&gt;
    &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failure&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;runRaw&lt;/code&gt; caught everything and turned it into a &lt;code&gt;String?&lt;/code&gt;. Convenient, and it throws away the one piece of information that separates a stop from a breakage.&lt;/p&gt;

&lt;p&gt;The obvious repair is to ask the task: &lt;em&gt;were we cancelled? then call it cancelled.&lt;/em&gt; That is exactly wrong, and this project had already learned why — in a comment I had read more than once, in a different file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// Reads the **error only**, and that took two goes to get right.&lt;/span&gt;
&lt;span class="c1"&gt;///&lt;/span&gt;
&lt;span class="c1"&gt;/// [...] consulting the caller's task handle worked for the common case and&lt;/span&gt;
&lt;span class="c1"&gt;/// quietly broke a rarer one: a genuine failure — a rejected credential, an&lt;/span&gt;
&lt;span class="c1"&gt;/// unreachable host — that raced with a late stop was reported as the stop, so&lt;/span&gt;
&lt;span class="c1"&gt;/// the user pressed a button and lost the reason their push had actually failed.&lt;/span&gt;
&lt;span class="c1"&gt;///&lt;/span&gt;
&lt;span class="c1"&gt;/// So the information now comes from where it is known: `awaitExit` throws&lt;/span&gt;
&lt;span class="c1"&gt;/// `CancellationError` when *it* signalled the child. An error that isn't one is&lt;/span&gt;
&lt;span class="c1"&gt;/// a real failure, whether or not a stop arrived while it was being thrown.&lt;/span&gt;
&lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;Kind&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;CancellationError&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;cancelled&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error already carries the truth. So the runner rethrows instead of stringifying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repoURL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;BulkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;CancellationError&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cancelled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;BulkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancelled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;BulkResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;for&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error&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;Green, and green for the right reason this time.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing worth carrying:&lt;/strong&gt; a cancellation test that cancels before the work starts is the equivalent of testing a parser on an empty string. Ask what the failing case would need to look like, then check your test actually builds it. If cancelling at a different moment changes the result, the moment is part of the test.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Two: a warning that admitted it didn't know
&lt;/h2&gt;

&lt;p&gt;Same feature, different sentence. Deleting a tag on the remote is the most destructive thing that tab can do, so it gets a dialog. Mine said:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;v1.0.0&lt;/code&gt; is deleted on origin. Anyone who has already fetched it keeps their copy, and a release built from it &lt;strong&gt;may survive&lt;/strong&gt; on the forge — this cannot be undone from here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that again. "May survive" is not a warning; it is me not knowing, formatted to look like caution. And it is the sentence a person reads two seconds before doing something irreversible.&lt;/p&gt;

&lt;p&gt;The first half was cheap to settle without touching anything — &lt;code&gt;gh&lt;/code&gt; documents it in its own help text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gh release delete &lt;span class="nt"&gt;--help&lt;/span&gt;
FLAGS
      &lt;span class="nt"&gt;--cleanup-tag&lt;/span&gt;   Delete the specified tag &lt;span class="k"&gt;in &lt;/span&gt;addition to its release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;In addition to.&lt;/em&gt; Deleting a release leaves the tag standing; removing the tag as well is an explicit opt-in. And &lt;code&gt;gh release create&lt;/code&gt; says the mirror image: &lt;em&gt;"If a matching git tag does not yet exist, one will automatically get created."&lt;/em&gt; Two objects with an optional link, not two faces of one thing.&lt;/p&gt;

&lt;p&gt;The other direction — what happens to a &lt;strong&gt;release&lt;/strong&gt; when you delete its &lt;strong&gt;tag&lt;/strong&gt; — needed a real repository. So: a throwaway private repo, two annotated tags, two published releases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gh release list
v2.0.0  &lt;span class="nv"&gt;draft&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false  &lt;/span&gt;Release due
v1.0.0  &lt;span class="nv"&gt;draft&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false  &lt;/span&gt;Release uno

&lt;span class="nv"&gt;$ &lt;/span&gt;git push origin &lt;span class="nt"&gt;--delete&lt;/span&gt; refs/tags/v1.0.0
 - &lt;span class="o"&gt;[&lt;/span&gt;deleted]         v1.0.0

&lt;span class="nv"&gt;$ &lt;/span&gt;gh release view v1.0.0 &lt;span class="nt"&gt;--json&lt;/span&gt; tagName,isDraft,url
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"isDraft"&lt;/span&gt;:true,&lt;span class="s2"&gt;"tagName"&lt;/span&gt;:&lt;span class="s2"&gt;"v1.0.0"&lt;/span&gt;,
 &lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;".../releases/tag/untagged-180ba3b4711b3e212eb7"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The release is &lt;strong&gt;not deleted&lt;/strong&gt;. GitHub demotes it to a &lt;strong&gt;draft&lt;/strong&gt; and re-homes it under an &lt;code&gt;untagged-…&lt;/code&gt; URL. It vanishes from the public releases page while still existing in the API.&lt;/p&gt;

&lt;p&gt;And then the measurement that actually changed the product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;git push origin refs/tags/v1.0.0     &lt;span class="c"&gt;# put the tag back&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;gh release view v1.0.0 &lt;span class="nt"&gt;--json&lt;/span&gt; isDraft,url
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"isDraft"&lt;/span&gt;:true,
 &lt;span class="s2"&gt;"url"&lt;/span&gt;:&lt;span class="s2"&gt;".../releases/tag/untagged-180ba3b4711b3e212eb7"&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still a draft. Restoring the tag does &lt;strong&gt;not&lt;/strong&gt; restore the release. Someone has to open GitHub and re-publish it by hand.&lt;/p&gt;

&lt;p&gt;So the honest sentence is not "it may survive". It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;v1.0.0&lt;/code&gt; is deleted on origin, and the release cut from it becomes a &lt;strong&gt;draft&lt;/strong&gt;: it disappears from the public releases page, and pushing the tag back does not bring it back — someone has to re-publish it by hand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a different warning. The first one says &lt;em&gt;something bad might happen&lt;/em&gt;. The second one tells you what, and what it will cost to undo. It is also, incidentally, only computable because the tab already knows which tags carry a release — so a tag with none gets a shorter, calmer sentence, and a partially-loaded release list gets an explicit "this could not be checked here" instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the two have in common
&lt;/h2&gt;

&lt;p&gt;A test asserts. A dialog warns. Both are sentences that &lt;em&gt;look&lt;/em&gt; like knowledge, and neither is compiled, linted, or type-checked. The only thing standing between them and being wrong is whether somebody went and measured.&lt;/p&gt;

&lt;p&gt;The test was green because it never built the case it named. The dialog was hedged because I never ran the experiment. Same shape, ten minutes apart in the same afternoon — and the second one was easier to spot because the first had just cost me an hour.&lt;/p&gt;

&lt;p&gt;When you find yourself writing "may", "might", or "could" into user-facing copy about a destructive action, that is not caution. That is a to-do item wearing a disguise.&lt;/p&gt;




&lt;p&gt;This work is on a development branch and is not released yet — the current public build is &lt;strong&gt;FinderGit 0.30.0&lt;/strong&gt;. FinderGit is free, a Universal binary for Apple silicon and Intel, notarized by Apple, and needs macOS 15 or later.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://findergit.app" rel="noopener noreferrer"&gt;https://findergit.app&lt;/a&gt; · Download: &lt;a href="https://findergit.app/download" rel="noopener noreferrer"&gt;https://findergit.app/download&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/the-test-that-could-only-pass-and-the-warning-that-admitted-it-didn-t-know-plouj?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>swift</category>
      <category>swiftui</category>
      <category>test</category>
      <category>coding</category>
    </item>
    <item>
      <title>octoscope 0.33.0 — the part of a pull request nobody reads line by line</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Tue, 08 Sep 2026 12:17:54 +0000</pubDate>
      <link>https://dev.to/undolog/octoscope-0330-the-part-of-a-pull-request-nobody-reads-line-by-line-35o4</link>
      <guid>https://dev.to/undolog/octoscope-0330-the-part-of-a-pull-request-nobody-reads-line-by-line-35o4</guid>
      <description>&lt;p&gt;You read the diff. The changed functions, the new test, maybe the workflow file. Then there is a four-hundred-kilobyte block of hashes at the bottom, and you scroll past it — because nobody reads that, and there is nothing in it a reviewer would recognise as suspicious anyway.&lt;/p&gt;

&lt;p&gt;Your lockfile is the one part of a pull request nobody reads line by line. octoscope 0.33.0 reads it for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyc1ekqodrbg11h0hhkz9.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyc1ekqodrbg11h0hhkz9.webp" alt="octoscope 0.33.0 — the Overview tab, banner reading the new version" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can see now
&lt;/h2&gt;

&lt;p&gt;Since v0.27.0 the scan could tell you that something in &lt;em&gt;your own&lt;/em&gt; repository had started auto-executing — a hook that appeared, a workflow whose contents moved. It could not see the other half: a dependency that starts running code the moment you install it. That is not a new file and it is not a code change. It is a lockfile diff, and branch protection does nothing about it.&lt;/p&gt;

&lt;p&gt;Now the scan reads &lt;code&gt;package-lock.json&lt;/code&gt; (or &lt;code&gt;npm-shrinkwrap.json&lt;/code&gt;) on your default branch and compares the subset of your dependencies that carry an install script. Press &lt;code&gt;s&lt;/code&gt; on any repository in the Repos tab, exactly as before.&lt;/p&gt;

&lt;p&gt;Three things it can tell you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a package that &lt;strong&gt;did not&lt;/strong&gt; run code at install and now does — something in your dependency tree began executing on your machine;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the &lt;strong&gt;same version&lt;/strong&gt; whose content changed. That version was republished, and no upgrade explains it. It is the heaviest thing this part of the scan will say;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an ordinary version bump, or a package that stopped running code at install. Both are listed, neither is scored.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why the subset, and not the whole file
&lt;/h2&gt;

&lt;p&gt;Because a lockfile changes constantly and the interesting part of it does not.&lt;/p&gt;

&lt;p&gt;I measured the last twenty revisions of the lockfile in three busy repositories — axios, npm/cli and undici, fifty-seven revisions in all. The files churned the entire time. The subset that runs code at install moved &lt;strong&gt;twice&lt;/strong&gt;. The same version was republished &lt;strong&gt;zero&lt;/strong&gt; times.&lt;/p&gt;

&lt;p&gt;That zero is the point. It is a measured rate of false alarms, not a guess, and it is what lets the sharp case actually mean something when it fires. An axis that goes off constantly is one you learn to skip, and then it is worth nothing on the day it is right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not check, it tells you
&lt;/h2&gt;

&lt;p&gt;This is npm only, and the report says so rather than going quiet. pnpm used to declare install scripts in its lockfile and dropped the field; Yarn never had one. Building on something that has already disappeared once would ship a rule that quietly decays, so if your lockfile is theirs, you get a line naming it and saying your dependency surface was &lt;strong&gt;not&lt;/strong&gt; compared.&lt;/p&gt;

&lt;p&gt;The same goes for a repository with no lockfile at all, one too large to read, or a format this scan has not measured. Silence would look exactly like &lt;em&gt;nothing here runs code at install&lt;/em&gt;, and that is the one thing it must never imply.&lt;/p&gt;

&lt;p&gt;And it never looks at the registry. The claim is that your dependencies' auto-execute surface &lt;em&gt;changed&lt;/em&gt; — never that a dependency is malicious. What to do about it is yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrading
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew upgrade gfazioli/tap/octoscope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or &lt;code&gt;gh extension upgrade octoscope&lt;/code&gt;, &lt;code&gt;go install github.com/gfazioli/octoscope@latest&lt;/code&gt;, or pull the container image.&lt;/p&gt;

&lt;p&gt;The first scan after upgrading will say it is the &lt;em&gt;first comparison&lt;/em&gt; of your dependency surface rather than claiming nothing changed — there is nothing recorded to diff against yet. The one after that is the one that talks.&lt;/p&gt;




&lt;p&gt;Site: &lt;a href="https://gfazioli.github.io/octoscope" rel="noopener noreferrer"&gt;https://gfazioli.github.io/octoscope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Newsletter: &lt;a href="https://octoscope.substack.com" rel="noopener noreferrer"&gt;https://octoscope.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mastodon: &lt;a href="https://hachyderm.io/@undolog" rel="noopener noreferrer"&gt;https://hachyderm.io/@undolog&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/octoscope-0-33-0-the-part-of-a-pull-request-nobody-reads-line-by-line-8x3b5?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>babeltea</category>
      <category>cli</category>
      <category>github</category>
      <category>tui</category>
    </item>
    <item>
      <title>Netfox 0.19.0 — what a certificate says about itself</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Mon, 07 Sep 2026 10:03:08 +0000</pubDate>
      <link>https://dev.to/undolog/netfox-0190-what-a-certificate-says-about-itself-22n1</link>
      <guid>https://dev.to/undolog/netfox-0190-what-a-certificate-says-about-itself-22n1</guid>
      <description>&lt;p&gt;Every home network has a few of them: the NAS admin page, the printer's web UI, the router itself, a Home Assistant box. They all speak TLS, they all present a certificate, and almost nobody ever looks at it — until a browser refuses to open one and you are left guessing whether the device is broken, misconfigured, or just doing what home devices do.&lt;/p&gt;

&lt;p&gt;Netfox 0.19.0 reads that certificate for you and says what it says.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you will see
&lt;/h2&gt;

&lt;p&gt;Open a device, pick a port that speaks TLS from the first byte — 443, 8443, 993, 995 and their kind — and press Read certificate. The card underneath shows who the certificate was issued to, who issued it, and when it expires. Then it states the two things worth knowing without making you work them out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Expired.&lt;/strong&gt; Named as such, with the date.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signed by itself.&lt;/strong&gt; Nothing vouches for it. That is normal on a home device that generated its own, and worth a second look anywhere else — and the card says exactly that, so the verdict arrives with its context.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two more lines appear only when they matter: a certificate that runs out within three weeks, and one that is not valid yet — which on a home network almost always means the clock on the device is wrong, not the certificate.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1wtdjom83q2tq5mi8te.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn1wtdjom83q2tq5mi8te.webp" alt="The service lookup on an open port. The certificate card sits right beside it, behind the same privacy switch." width="630" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The service lookup on an open port. The certificate card sits right beside it, behind the same privacy switch.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It does not start anything on its own. The read happens when you press the button, and only if the same privacy setting that governs looking up service info is on. Ports that start in plain text and upgrade to TLS later are deliberately left alone: a greeting aimed at them is not a greeting.&lt;/p&gt;

&lt;p&gt;And in Demo Mode the card is hidden entirely. A certificate's name on a home network is very often the device's hostname, which is exactly the kind of thing Demo Mode exists to keep off a screenshot.&lt;/p&gt;

&lt;h2&gt;
  
  
  A correction
&lt;/h2&gt;

&lt;p&gt;The What's New panel of 0.18.1 said nothing in the app itself had changed. That was wrong, and the panel inside the app was the one place it could not be fixed on the day. Besides the signing fix, 0.18.1 also graded the radio's headroom by the same threshold the Wi-Fi tool uses, named the interface the route leaves through, and rewrote two sentences of the link panel. This release's panel says so.&lt;/p&gt;




&lt;p&gt;Netfox is free, a universal binary (Apple Silicon + Intel), runs on macOS 15.6+, and is signed and notarized.&lt;/p&gt;

&lt;p&gt;Netfox for macOS — Website: &lt;a href="https://netfox.app" rel="noopener noreferrer"&gt;https://netfox.app&lt;/a&gt; · Download: &lt;a href="https://netfox.app/download" rel="noopener noreferrer"&gt;https://netfox.app/download&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/netfox-0-19-0-what-a-certificate-says-about-itself-qnygb?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>macos</category>
      <category>network</category>
      <category>tools</category>
    </item>
    <item>
      <title>octoscope 0.32.0 — the Overview tab: profile, social and activity counters, languages bar, top repositories and network</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Mon, 07 Sep 2026 06:17:42 +0000</pubDate>
      <link>https://dev.to/undolog/octoscope-0320-the-overview-tab-profile-social-and-activity-counters-languages-bar-top-1j3</link>
      <guid>https://dev.to/undolog/octoscope-0320-the-overview-tab-profile-social-and-activity-counters-languages-bar-top-1j3</guid>
      <description>&lt;p&gt;There is a question the Repos tab could not answer until now. Not "how much did I commit to this one?" — the drill-in has told you that for a while — but "which of my repositories do I actually commit to most?", which is a question about the &lt;em&gt;ordering&lt;/em&gt; of the whole list, not about any one row.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzp3wwg71ph4v8jvtknz.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmzp3wwg71ph4v8jvtknz.webp" alt="A Commits column for the Repos tab — your commits per repository over the last year, sortable — and the measurement that decided why it is opt-in." width="800" height="714"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;octoscope 0.32.0 answers it with a &lt;strong&gt;Commits&lt;/strong&gt; column: how many commits you authored on each repository's default branch in the last 365 days, with its own sort in the &lt;code&gt;s&lt;/code&gt; cycle and in &lt;code&gt;default_sort&lt;/code&gt;. Turn it on from the settings panel (&lt;code&gt;,&lt;/code&gt;) — the tab refetches at once — or with &lt;code&gt;commit_counts = true&lt;/code&gt; in your config.&lt;/p&gt;

&lt;p&gt;It is off by default, and I want to tell you why, because the reason is a number rather than a preference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The column that had to be measured first
&lt;/h2&gt;

&lt;p&gt;GitHub counts each repository's history on request. When I asked for that count &lt;em&gt;inline&lt;/em&gt;, on the query that already feeds the Repos tab, the query went from about six and a half seconds to somewhere between eight and eleven on my 91-repository account — and GitHub cuts any request it cannot process in ten seconds. Five runs: three of them died at the gateway. The first one, for what it is worth, passed at 9.8 seconds. A single green run of a query that close to the clock proves nothing, and I nearly shipped on it.&lt;/p&gt;

&lt;p&gt;So the column runs as a query of its own, in parallel with the six the dashboard already makes, paging at fifty repositories rather than a hundred. On that same account it takes four to six seconds — a cost every refresh pays, which is exactly why it is yours to switch on and not mine to impose.&lt;/p&gt;

&lt;p&gt;It is also best-effort by construction. If it ever times out, you lose the column for one refresh, the header falls back to the previous sort and says so, and the dashboard is untouched. A timeout on GitHub's side is not free either — they dock your rate limit for the following hour — which is one more reason the branch prefers small requests to one that flirts with the cut-off.&lt;/p&gt;

&lt;p&gt;A zero, when the column is on, is a real zero. The tab distinguishes "you made no commits here" from "the count did not arrive", and never shows you the second dressed as the first. On someone else's public profile the column stays hidden: there is no viewer to attribute commits to, and a column of zeros would be a lie.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toggle does what the file could not
&lt;/h2&gt;

&lt;p&gt;Flipping &lt;strong&gt;Commit counts&lt;/strong&gt; in the panel sets the flag and refetches immediately, in either direction. If a refresh is already running when you save, the change is remembered and one more fetch follows the moment it lands — so what you see is a result taken &lt;em&gt;after&lt;/em&gt; your change, never one that happened to read the old setting on its way out.&lt;/p&gt;

&lt;h2&gt;
  
  
  For scripts
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;octoscope --json&lt;/code&gt; carries &lt;code&gt;commits_last_year&lt;/code&gt; on each repository when the column is on: an explicit &lt;code&gt;0&lt;/code&gt; where you made no commits, and the key absent entirely when the count was not fetched — so a script can tell the two apart. Watched repositories never carry it; the branch counts only what you own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew upgrade gfazioli/tap/octoscope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;octoscope on the web: &lt;a href="https://gfazioli.github.io/octoscope" rel="noopener noreferrer"&gt;https://gfazioli.github.io/octoscope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This newsletter: &lt;a href="https://octoscope.substack.com" rel="noopener noreferrer"&gt;https://octoscope.substack.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Mastodon: &lt;a href="https://hachyderm.io/@undolog" rel="noopener noreferrer"&gt;https://hachyderm.io/@undolog&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/octoscope-0-32-0-the-overview-tab-profile-social-and-activity-counters-languages-bar-top-repositories-and-network-25d0h?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter?utm_source=devto&amp;amp;utm_medium=crosspost&amp;amp;utm_campaign=syndication-footer" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>cli</category>
      <category>tui</category>
      <category>bubbletea</category>
      <category>git</category>
    </item>
    <item>
      <title>FinderGit 0.29.0 — open a commit like a folder</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Fri, 04 Sep 2026 12:29:46 +0000</pubDate>
      <link>https://dev.to/undolog/findergit-0290-open-a-commit-like-a-folder-efh</link>
      <guid>https://dev.to/undolog/findergit-0290-open-a-commit-like-a-folder-efh</guid>
      <description>&lt;p&gt;There’s a moment, reading a commit list, where you want to know one small thing: &lt;em&gt;which files did that one touch?&lt;/em&gt; Until this week FinderGit’s answer was to send you somewhere else. Now every commit has a chevron, and opening it is exactly like opening a folder.&lt;/p&gt;

&lt;p&gt;Underneath you get the files that commit changed, with git’s own status letter beside each one, the old name when something was renamed, and that file’s own line counts. Click a file and its diff opens — scoped to what that single commit did to that single file, read-only, nothing to accidentally stage or revert. Deleted files open too, because what a commit removed is often the part worth reading. Merge commits finally list what they brought in, instead of the nothing they used to show.&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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M_2Y%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa69ec7a9-1bc6-4682-9f0a-dc4ff868c7e1_2000x1282.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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21M_2Y%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252Fa69ec7a9-1bc6-4682-9f0a-dc4ff868c7e1_2000x1282.png" title="Open a commit and its files unfold underneath" alt="Open a commit and its files unfold underneath" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The second thing you’ll notice is smaller and, honestly, more overdue. Rows light up under the pointer now. Everywhere — the branch list, the changed files, the stashes, the clone picker, the settings, the breadcrumb. Several of those rows already did something when you clicked them, and nothing on screen ever suggested they would. That’s the difference between a list you read and a list you use. While I was in there, whole rows became clickable where only a word used to be: a file in the Unstage tab opens its diff from anywhere along the row, not just from the characters of its path.&lt;/p&gt;

&lt;p&gt;The Branches tab got rebuilt around the same idea. The branch you’re on is pinned at the top in its own card, with its upstream and how far ahead or behind you are, so it never scrolls out of sight. Local and Remote are now two separate groups, each with a count and proper separated rows, instead of two captions floating over undifferentiated text.&lt;/p&gt;

&lt;p&gt;And then the one I’m most pleased about: issues and pull requests open in place. The full description, rendered properly — headings, lists, links, code — with the author and the date, then the discussion underneath. A long description folds behind a Show more so one verbose issue can’t push everything else off screen.&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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21hRnV%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F61d7135b-fda7-414c-85be-850a979cc147_2000x1282.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%2Fsubstackcdn.com%2Fimage%2Ffetch%2F%24s_%21hRnV%21%2Cw_1456%2Cc_limit%2Cf_auto%2Cq_auto%3Agood%2Cfl_progressive%3Asteep%2Fhttps%253A%252F%252Fsubstack-post-media.s3.amazonaws.com%252Fpublic%252Fimages%252F61d7135b-fda7-414c-85be-850a979cc147_2000x1282.png" title="An issue opens in place, description and discussion" alt="An issue opens in place, description and discussion" width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a pull request that means the &lt;em&gt;whole&lt;/em&gt; conversation, not just the comments tab: the body a reviewer submitted with their verdict, and the notes pinned to individual lines of a file, each labelled for what it is. A pull request reviewed entirely inline — which is how most of them are — used to look as though nobody had said a word.&lt;/p&gt;

&lt;p&gt;One deliberate omission there. Images referenced in a description are not downloaded. An issue can be filed by anyone, and quietly fetching an image it names would tell whoever wrote it your IP address and the moment you read it. Open in browser and Copy link sit underneath for the parts FinderGit doesn’t render itself.&lt;/p&gt;

&lt;p&gt;Everything above is translated into Italian, French, German and Spanish.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The bug I’d rather tell you about&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Discarding a file whose name contains &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt; or square brackets could revert a &lt;em&gt;different&lt;/em&gt; file — one that happened to match the pattern. Discard has no undo.&lt;/p&gt;

&lt;p&gt;The mechanism is a nasty little corner of git. &lt;code&gt;git restore -- 'a[1].txt'&lt;/code&gt; doesn’t restore the file called &lt;code&gt;a[1].txt&lt;/code&gt;; the brackets are read as a pattern, so it matched &lt;code&gt;a1.txt&lt;/code&gt; and reverted the uncommitted work there instead. The same slip made Stage act on more files than you’d selected, and made a single file’s diff show a neighbour’s changes under the wrong name. Every path FinderGit hands to git is now taken literally.&lt;/p&gt;

&lt;p&gt;If you’ve never named a file that way, this never touched you. If you have, it did so without saying anything, which is the part that bothered me.&lt;/p&gt;

&lt;p&gt;Three smaller fixes came along with it. The Branches tab used to list &lt;code&gt;origin&lt;/code&gt; among the remote branches, complete with a Checkout button git would have refused — that was the remote’s own name leaking through a filter that could never match it. A file-status tooltip was sharing a translation with the panel that counts dirty repositories, so it read as a plural in Italian, French and Spanish; English looked fine, which is why it survived this long. And a commit whose file list can’t be decoded is now refused outright rather than shown, because clicking a row in that state would have opened the wrong file’s diff, or none at all.&lt;/p&gt;




&lt;p&gt;FinderGit is free, a universal binary (Apple Silicon + Intel), runs on macOS 15+, and is signed and notarized.&lt;/p&gt;

&lt;p&gt;FinderGit for macOS — Website: &lt;a href="https://findergit.app" rel="noopener noreferrer"&gt;https://findergit.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Download: &lt;a href="https://findergit.app/download" rel="noopener noreferrer"&gt;https://findergit.app/download&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://zyvop.com/findergit-0-29-0-open-a-commit-like-a-folder-3fv6f" rel="noopener noreferrer"&gt;ZyVOP&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;💡 For more articles like this, &lt;a href="https://zyvop.com/newsletter" rel="noopener noreferrer"&gt;subscribe to the ZyVOP newsletter&lt;/a&gt;!&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Netfox 0.18.0 — is it my connection, or the internet?</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:33:43 +0000</pubDate>
      <link>https://dev.to/undolog/netfox-0180-is-it-my-connection-or-the-internet-24d0</link>
      <guid>https://dev.to/undolog/netfox-0180-is-it-my-connection-or-the-internet-24d0</guid>
      <description>&lt;p&gt;There is a question that comes up every time something feels slow, and until now Netfox could not answer it: is the problem my own connection, or is it everything past my door?&lt;/p&gt;

&lt;p&gt;That is what 0.18.0 does.&lt;/p&gt;

&lt;p&gt;The Overview has a new panel, and it reads the layer underneath the IP address — where the cable and the radio actually live.&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%2Fjyitkwkdhl9gphtb6wdo.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%2Fjyitkwkdhl9gphtb6wdo.png" alt="The Netfox Overview with the new Link panel: the wired half, the radio and its headroom, the error counters, and the first-hop split" width="800" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the cable negotiated, against what it could have.&lt;/strong&gt; A 10-gigabit port that settled for 1 Gb/s is not broken, and nothing in macOS will tell you it happened. Netfox names it, along with the two usual reasons: the cable, or the switch at the other end. Cat 5e runs 1 Gb/s; 10 needs Cat 6a and a switch that speaks it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the radio is doing, and whether it is holding.&lt;/strong&gt; "Wi-Fi 7 at 720 Mb/s" means little on its own — "39 dB of headroom, signal against noise" means you have room to spare. And because a radio's reported speed is what the link is doing right now rather than a property of it, the panel takes two samples and tells you what changed between them. That is not decoration: while this was being built, the radio moved half a gigabit in two seconds, on the same network, in the same room. A single reading would have presented that as a fact about your hardware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whether anything is being dropped&lt;/strong&gt; — in the last two seconds, and since your Mac booted. Two separate claims, deliberately, because neither implies the other. A link that has been clean for weeks can start dropping frames this second; one that dropped a handful during a cable swap last month is clean now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And how far away the delay starts.&lt;/strong&gt; "First hop 3.6 ms, then 11 ms more past the router." Your own link, and the internet beyond it, as two numbers instead of one. That split is the difference between a problem you can fix and one you can only wait out.&lt;/p&gt;




&lt;p&gt;The part I am most pleased with is what the panel refuses to say.&lt;/p&gt;

&lt;p&gt;A radio that is switched off has no band and no channel, so those are left out rather than filled in with "Unknown" — an absence rendered as two facts is worse than a gap. A link nothing crossed gets no clean bill of health: it has not been shown to be fine, it has not been shown anything. And a router that ignores pings is reported as untimeable rather than broken, because plenty of them drop pings by policy.&lt;/p&gt;

&lt;p&gt;Every latency figure is a minimum rather than an average, and that was measured rather than assumed. Across five bursts the minimum moved by under a millisecond while the average moved by more than a hundred, because one delayed reply drags a mean of five readings and moves a minimum not at all. My first explanation for that was wrong, confidently, and four more measurements killed it.&lt;/p&gt;

&lt;p&gt;The Overview also fits on one screen again — the banner that counted devices needing attention became the first card of the summary and brought its Smart Scan button with it, and the network and link panels sit side by side when there is room.&lt;/p&gt;

&lt;p&gt;Nothing new is asked of you: no permission prompt, no privileged helper, and no traffic beyond the latency check, which rides the same switch as the public-IP lookup. Six languages, in step.&lt;/p&gt;

&lt;p&gt;Update from Netfox → Check for Updates, or download it at &lt;a href="https://netfox.app" rel="noopener noreferrer"&gt;https://netfox.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As always, tell me what you would like to see next.&lt;/p&gt;




&lt;p&gt;Netfox is free, a universal binary (Apple Silicon + Intel), runs on macOS 15.6+, and is signed and notarized.&lt;/p&gt;

&lt;p&gt;Netfox for macOS — Website: &lt;a href="https://netfox.app" rel="noopener noreferrer"&gt;https://netfox.app&lt;/a&gt; · Download: &lt;a href="https://netfox.app/download" rel="noopener noreferrer"&gt;https://netfox.app/download&lt;/a&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>tooling</category>
      <category>security</category>
    </item>
    <item>
      <title>Refactoring Amplitude Integration in Next.js: From Build-Time to Runtime Configuration</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Wed, 26 Aug 2026 11:58:55 +0000</pubDate>
      <link>https://dev.to/undolog/refactoring-amplitude-integration-in-nextjs-from-build-time-to-runtime-configuration-1iam</link>
      <guid>https://dev.to/undolog/refactoring-amplitude-integration-in-nextjs-from-build-time-to-runtime-configuration-1iam</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;How we solved environment variable issues in a production Next.js application using Amplitude's unified SDK and strategic architecture patterns&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Executive Summary
&lt;/h2&gt;

&lt;p&gt;When integrating analytics and feature flags into a Next.js application, the standard approach of using build-time environment variables (&lt;code&gt;NEXT_PUBLIC_*&lt;/code&gt;) works well for simple cases. However, when dealing with Docker containerization, dynamic environments, and secure API keys, this approach breaks down. This article explores how we refactored our Amplitude integration to support &lt;strong&gt;runtime environment variables&lt;/strong&gt; while maintaining security, performance, and developer experience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Improvements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Runtime environment variable loading (no rebuild required)&lt;/li&gt;
&lt;li&gt;✅ Secure API key management with server-side proxying&lt;/li&gt;
&lt;li&gt;✅ Better separation of concerns between tracking and feature flags&lt;/li&gt;
&lt;li&gt;✅ Type-safe, centralized configuration management&lt;/li&gt;
&lt;li&gt;✅ Zero bundle size increase for sensitive credentials&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Problem: Build-Time Environment Variables in a Runtime World
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Original Implementation Issues
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://amplitude.com/docs/sdks/frameworks/nextjs-installation-guide" rel="noopener noreferrer"&gt;official Amplitude Next.js guide&lt;/a&gt; recommends this pattern:&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;// ❌ OLD: amplitude.ts - Build-time initialization&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;amplitude&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;@amplitude/unified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initAmplitude&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;autocapture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="nf"&gt;initAmplitude&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ⚠️ Runs at module evaluation time&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;amplitude&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;Why This Breaks:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Build-Time Binding&lt;/strong&gt;: &lt;code&gt;process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/code&gt; is resolved at &lt;strong&gt;build time&lt;/strong&gt; and inlined into the JavaScript bundle. In Docker environments where the same image is deployed to multiple environments (staging, production), this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can't use the same Docker image for different environments&lt;/li&gt;
&lt;li&gt;Changing API keys requires rebuilding the entire application&lt;/li&gt;
&lt;li&gt;The environment variable is "frozen" into the bundle&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Module Evaluation Side Effects&lt;/strong&gt;: The &lt;code&gt;initAmplitude()&lt;/code&gt; function runs &lt;strong&gt;immediately&lt;/strong&gt; when the module is imported, before:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The React component tree is mounted&lt;/li&gt;
&lt;li&gt;Runtime environment variables are available&lt;/li&gt;
&lt;li&gt;The user context is established&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Feature Flags Security Risk&lt;/strong&gt;: The original implementation exposed the Amplitude Experiment API key client-side:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ OLD: Client-side feature flag initialization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// ⚠️ Exposed to browser&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&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 meant &lt;strong&gt;anyone&lt;/strong&gt; could open DevTools, extract the deployment key, and access your feature flag configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: Three-Tier Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Runtime Environment Provider (EnvironmentProvider)
&lt;/h3&gt;

&lt;p&gt;The foundation of our solution is Next.js 15's &lt;code&gt;connection()&lt;/code&gt; API, which enables &lt;strong&gt;dynamic server-side rendering&lt;/strong&gt; with runtime environment variable access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ NEW: app/layout.tsx - Runtime configuration&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connection&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;next/server&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ✅ Forces Next.js to evaluate environment variables at request time&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;connection&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_ENV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;applicationUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_APPLICATION_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
    &lt;span class="na"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amplitudeFeatureFlagsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sequoiaDebug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_SEQUOIA_DEBUG&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&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="k"&gt;return &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;html&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"en"&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;body&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="nc"&gt;MantineProvider&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;theme&lt;/span&gt;&lt;span class="si"&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="nc"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* Children receive runtime configuration */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;EnvironmentProvider&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="nc"&gt;MantineProvider&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;body&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;html&lt;/span&gt;&lt;span class="p"&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;&lt;strong&gt;Key Insight&lt;/strong&gt;: The &lt;code&gt;connection()&lt;/code&gt; API is Next.js's opt-in mechanism for &lt;strong&gt;partial prerendering&lt;/strong&gt;. When called in a Server Component, it ensures that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The component is &lt;strong&gt;dynamically rendered&lt;/strong&gt; on each request&lt;/li&gt;
&lt;li&gt;Environment variables are read from the &lt;strong&gt;current process environment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No values are baked into the static build&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This allows the same Docker image to work across environments by simply changing the runtime environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Staging&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;staging_key ...

&lt;span class="c"&gt;# Production&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;prod_key ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Client-Side Initialization (AmplitudeInitializer)
&lt;/h3&gt;

&lt;p&gt;With runtime configuration available, we can now initialize Amplitude &lt;strong&gt;reactively&lt;/strong&gt; using React's lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ NEW: AmplitudeInitializer.tsx - Client Component&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&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;react&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="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;amplitude&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;@amplitude/unified&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEnvironmentContext&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;@/components/Environment&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AmplitudeInitializer&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;amplitudeApiKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEnvironmentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Amplitude] Missing API key - tracking disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&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="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;analytics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;autocapture&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Architecture Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lazy Initialization&lt;/strong&gt;: Amplitude SDK is initialized &lt;strong&gt;after&lt;/strong&gt; React mounts, ensuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Runtime environment variables are available&lt;/li&gt;
&lt;li&gt;Window object exists&lt;/li&gt;
&lt;li&gt;Proper error handling can occur&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Graceful Degradation&lt;/strong&gt;: If the API key is missing, tracking is simply disabled with a warning instead of crashing the app&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Type Safety&lt;/strong&gt;: TypeScript enforces that &lt;code&gt;amplitudeApiKey&lt;/code&gt; exists in the &lt;code&gt;EnvironmentContext&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Secure Feature Flags Proxy (API Route)
&lt;/h3&gt;

&lt;p&gt;The most critical security improvement is moving feature flag fetching to a &lt;strong&gt;server-side API route&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="c1"&gt;// ✅ NEW: app/api/amplitude/flags/route.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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;next/server&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Experiment&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;@amplitude/experiment-node-server&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;user_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user_id&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="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_id is required&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ✅ Server-only environment variable (NO NEXT_PUBLIC_ prefix)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Amplitude Flags API] Missing deployment key&lt;/span&gt;&lt;span class="dl"&gt;'&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Feature flags unavailable&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Initialize Experiment client (server-side only)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;fetchTimeoutMillis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fetchRetries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&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;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchV2&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user_id&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed to fetch feature flags&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Client-Side Hook:&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="c1"&gt;// ✅ NEW: use-amplitude-feature-flags.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useAmplitudeFeatureFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SequoiaUser&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&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;userPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&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;return&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;email&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;fetcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User ID required&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&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;userPayload&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`API error: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;userPayload&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/amplitude/flags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;revalidateOnFocus&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;dedupingInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5 minutes&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;featureFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&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;Security Advantages:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Old Approach&lt;/th&gt;
&lt;th&gt;New Approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment Key Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Client bundle (exposed)&lt;/td&gt;
&lt;td&gt;Server-only (hidden)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct from browser&lt;/td&gt;
&lt;td&gt;Proxied through Next.js API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Key Rotation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rebuild required&lt;/td&gt;
&lt;td&gt;Update ENV + restart&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-user IP&lt;/td&gt;
&lt;td&gt;Per-server (easier to control)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request Validation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Server-side validation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Comparison: Old vs New Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Old Architecture (Build-Time)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  Docker Build Process               │
│                                     │
│  1. Read .env file                  │
│  2. Inline NEXT_PUBLIC_* into JS    │
│  3. Bundle with API keys baked in   │
└─────────────────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│  Runtime (Browser)                  │
│                                     │
│  - amplitude.ts runs immediately    │
│  - Uses build-time API key          │
│  - Feature flags fetch directly     │
│    from Amplitude (key exposed)     │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Problems:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Same build can't be used across environments&lt;/li&gt;
&lt;li&gt;❌ API key rotation requires rebuild and redeploy&lt;/li&gt;
&lt;li&gt;❌ Deployment keys exposed in browser DevTools&lt;/li&gt;
&lt;li&gt;❌ No validation or rate limiting control&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  New Architecture (Runtime)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────┐
│  Docker Build Process               │
│                                     │
│  1. No environment variable inlining│
│  2. Create generic Docker image     │
│  3. Same image for all environments │
└─────────────────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│  Server (Next.js)                   │
│                                     │
│  1. connection() → read runtime ENV │
│  2. Pass to EnvironmentProvider     │
│  3. Serve to client components      │
│                                     │
│  /api/amplitude/flags endpoint      │
│  - Keeps deployment key server-side │
│  - Validates requests               │
│  - Proxies to Amplitude             │
└─────────────────────────────────────┘
           ↓
┌─────────────────────────────────────┐
│  Client (Browser)                   │
│                                     │
│  1. AmplitudeInitializer receives   │
│     runtime API key from context    │
│  2. Initializes SDK with correct key│
│  3. Feature flags fetched via       │
│     /api/amplitude/flags (secure)   │
└─────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Benefits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Single Docker image for all environments&lt;/li&gt;
&lt;li&gt;✅ Hot-swappable configuration via ENV&lt;/li&gt;
&lt;li&gt;✅ Deployment keys never exposed to browser&lt;/li&gt;
&lt;li&gt;✅ Server-side request validation and rate limiting&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementation Highlights
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Centralized Configuration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Old Pattern:&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="c1"&gt;// Scattered across multiple files&lt;/span&gt;
&lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="o"&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;New Pattern:&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="c1"&gt;// Single source of truth in EnvironmentProvider&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amplitudeFeatureFlagsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Consumed via typed context&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;amplitudeApiKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEnvironmentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Separation of Concerns
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;AmplitudeProvider&lt;/strong&gt; now focuses purely on &lt;strong&gt;business logic&lt;/strong&gt; (tracking user login, fetching feature flags):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AmplitudeProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;children&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&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;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isUserLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useFetchUser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useTrackLogin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// ← Tracks login events&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;featureFlags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAmplitudeFeatureFlags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// ← Fetches flags&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;featureFlags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;isUserLoading&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AmplitudeContextProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AmplitudeContextProvider&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;While &lt;strong&gt;AmplitudeInitializer&lt;/strong&gt; handles &lt;strong&gt;SDK setup&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;envVars&lt;/span&gt;&lt;span class="si"&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="nc"&gt;AmplitudeInitializer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ← SDK initialization */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AmplitudeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ← Business logic */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;AmplitudeProvider&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="nc"&gt;EnvironmentProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation makes testing easier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mock &lt;code&gt;EnvironmentProvider&lt;/code&gt; for unit tests&lt;/li&gt;
&lt;li&gt;Mock API route for integration tests&lt;/li&gt;
&lt;li&gt;No need to mock Amplitude SDK internals&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Type Safety
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;EnvironmentContext&lt;/code&gt; interface enforces compile-time checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EnvironmentContext&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;env&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;applicationUrl&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amplitudeFeatureFlagsApiKey&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... other config&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// TypeScript enforces correct usage&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;amplitudeApiKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEnvironmentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ✅ Type-safe&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;invalidKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEnvironmentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// ❌ Compile error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Error Handling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Old:&lt;/strong&gt; Silent failures or app crashes&lt;br&gt;
&lt;strong&gt;New:&lt;/strong&gt; Graceful degradation with warnings&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[Amplitude] Missing API key - tracking disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// ✅ App continues without tracking&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Performance Considerations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bundle Size Impact
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Old Implementation&lt;/th&gt;
&lt;th&gt;New Implementation&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client Bundle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;145 KB&lt;/td&gt;
&lt;td&gt;142 KB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-3 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Key Exposure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Public (in bundle)&lt;/td&gt;
&lt;td&gt;Hidden (server-only)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Improved&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Initialization Time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~50ms (module load)&lt;/td&gt;
&lt;td&gt;~30ms (useEffect)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Why the improvement?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Removed &lt;code&gt;@amplitude/experiment-node-server&lt;/code&gt; from client bundle (now server-only)&lt;/li&gt;
&lt;li&gt;Conditional initialization reduces blocking JavaScript&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Runtime Performance
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;SWR Caching Strategy:&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;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/amplitude/flags&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;revalidateOnFocus&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="c1"&gt;// Fresh data when user returns&lt;/span&gt;
  &lt;span class="na"&gt;dedupingInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 5-minute cache&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First render: ~200ms (network request)&lt;/li&gt;
&lt;li&gt;Subsequent renders: ~0ms (cached)&lt;/li&gt;
&lt;li&gt;Background revalidation: Non-blocking&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Migration Guide
&lt;/h2&gt;

&lt;p&gt;If you're using the old Amplitude pattern, here's how to migrate:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add Runtime ENV Support
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/layout.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connection&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;next/server&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;RootLayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;children&lt;/span&gt; &lt;span class="p"&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;connection&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// ← Add this&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;envVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amplitudeFeatureFlagsApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&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="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;html&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;body&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="nc"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;envVars&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;EnvironmentProvider&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;body&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;html&lt;/span&gt;&lt;span class="p"&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;h3&gt;
  
  
  Step 2: Replace Module-Level Initialization
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Remove:&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="c1"&gt;// ❌ OLD: amplitude.ts&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;initAmplitude&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;initAmplitude&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;Add:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ✅ NEW: AmplitudeInitializer.tsx&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use client&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;AmplitudeInitializer&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;amplitudeApiKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useEnvironmentContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amplitudeApiKey&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;undefined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* config */&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="nx"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Create Feature Flags API Proxy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/api/amplitude/flags/route.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&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;user_id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&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;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;apiKey&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;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchV2&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user_id&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flags&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;h3&gt;
  
  
  Step 4: Update Client-Side Hook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Replace:&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="c1"&gt;// ❌ OLD: Direct Experiment initialization&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initializeRemote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/span&gt;&lt;span class="o"&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;With:&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="c1"&gt;// ✅ NEW: Fetch via API proxy&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;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSWR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/amplitude/flags&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="nx"&gt;url&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&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="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;h3&gt;
  
  
  Step 5: Update Layout Component Tree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;envVars&lt;/span&gt;&lt;span class="si"&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="nc"&gt;AmplitudeInitializer&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ← Add */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AmplitudeProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;           &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ← Keep */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SWRProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;SWRProvider&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="nc"&gt;AmplitudeProvider&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="nc"&gt;EnvironmentProvider&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Strategy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Unit Tests
&lt;/h3&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="s1"&gt;AmplitudeInitializer&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="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="s1"&gt;initializes with valid API key&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mockEnv&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AmplitudeInitializer&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/EnvironmentProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;amplitude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;initAll&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test-key&lt;/span&gt;&lt;span class="dl"&gt;'&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;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Object&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="s1"&gt;warns when API key is missing&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;consoleWarnSpy&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;spyOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;amplitudeApiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvironmentProvider&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;mockEnv&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AmplitudeInitializer&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/EnvironmentProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;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;consoleWarnSpy&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="nx"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringContaining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Missing API key&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Integration Tests
&lt;/h3&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="s1"&gt;Feature Flags API&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="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="s1"&gt;returns flags for valid user&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/amplitude/flags&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&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="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;test@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;flags&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toHaveProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;feature-name&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="s1"&gt;returns 400 for missing user_id&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/amplitude/flags&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;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&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;Content-Type&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="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&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="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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="mi"&gt;400&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;h2&gt;
  
  
  Deployment Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[ ] &lt;strong&gt;Environment Variables Set&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NEXT_PUBLIC_AMPLITUDE_API_KEY&lt;/code&gt; (client-safe, for analytics)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEXT_PUBLIC_AMPLITUDE_FEATURE_FLAGS_API_KEY&lt;/code&gt; (server-only, for feature flags)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] &lt;strong&gt;Docker Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Remove &lt;code&gt;.env&lt;/code&gt; from Docker image&lt;/li&gt;
&lt;li&gt;Pass environment variables at runtime via &lt;code&gt;-e&lt;/code&gt; flags or orchestration secrets&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] &lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add logging for missing API keys&lt;/li&gt;
&lt;li&gt;Track feature flag fetch failures&lt;/li&gt;
&lt;li&gt;Monitor API route response times&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;[ ] &lt;strong&gt;Testing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify same Docker image works in staging and production&lt;/li&gt;
&lt;li&gt;Test feature flag updates without rebuild&lt;/li&gt;
&lt;li&gt;Validate graceful degradation when Amplitude is unavailable&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;By refactoring from build-time to runtime configuration, we achieved:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Operational Flexibility&lt;/strong&gt;: Deploy once, configure anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enhanced Security&lt;/strong&gt;: Keep deployment keys server-side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better Developer Experience&lt;/strong&gt;: Type-safe, centralized configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved Performance&lt;/strong&gt;: Smaller client bundles, lazy initialization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Resilience&lt;/strong&gt;: Graceful degradation and better error handling&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;While the official Amplitude guide provides a good starting point, production applications often require more sophisticated patterns. The key insight is recognizing when &lt;strong&gt;build-time assumptions don't match runtime requirements&lt;/strong&gt;, and architecting accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Takeaways
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use Next.js 15's &lt;code&gt;connection()&lt;/code&gt; API for runtime environment variable access&lt;/li&gt;
&lt;li&gt;Separate SDK initialization (infrastructure) from business logic (tracking, feature flags)&lt;/li&gt;
&lt;li&gt;Proxy sensitive API keys through server-side routes&lt;/li&gt;
&lt;li&gt;Embrace React's lifecycle for SDK initialization instead of module-level side effects&lt;/li&gt;
&lt;li&gt;Test with realistic deployment scenarios (Docker, multiple environments)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/building-your-application/configuring/environment-variables" rel="noopener noreferrer"&gt;Next.js Environment Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://nextjs.org/docs/app/api-reference/functions/connection" rel="noopener noreferrer"&gt;Next.js Dynamic Rendering with connection()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.docs.developers.amplitude.com/data/sdks/typescript-browser/" rel="noopener noreferrer"&gt;Amplitude Unified SDK Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://swr.vercel.app/" rel="noopener noreferrer"&gt;SWR Data Fetching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>nextjs</category>
      <category>typescript</category>
      <category>programming</category>
    </item>
    <item>
      <title>octoscope 0.27.0 — a scan that notices, not just describes</title>
      <dc:creator>Giovambattista Fazioli</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:54:49 +0000</pubDate>
      <link>https://dev.to/undolog/octoscope-0270-a-scan-that-notices-not-just-describes-1h5d</link>
      <guid>https://dev.to/undolog/octoscope-0270-a-scan-that-notices-not-just-describes-1h5d</guid>
      <description>&lt;p&gt;There is a question a security tool ought to be able to answer, and octoscope's could not: &lt;strong&gt;did something change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The supply-chain scan has been able to describe a repository since 0.20.0 — here is what auto-executes, here is a commit tip that looks forged. That catches an implant you happen to scan while it is sitting there. It is no help at all with the two questions you actually have when something is going around: did anything move, and what could whoever got in reach?&lt;/p&gt;

&lt;p&gt;This release answers both.&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%2Frx290w0kba55d6phtu1m.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%2Frx290w0kba55d6phtu1m.png" alt="Octoscope" width="800" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed since the last scan
&lt;/h2&gt;

&lt;p&gt;Every scan now quietly records a fingerprint of what auto-executes in the repository. The next one diffs against it and tells you what moved: a file that auto-executes &lt;strong&gt;appeared&lt;/strong&gt;, an existing one's &lt;strong&gt;contents changed&lt;/strong&gt;, a branch tip that used to be signed &lt;strong&gt;no longer is&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What makes this the most durable check in the whole scan is that it cares about neither the filename nor the contents. A variant that renames its dropper and obfuscates it differently walks straight past every content check — but it still has to &lt;em&gt;appear&lt;/em&gt;, and appearing is the thing being watched.&lt;/p&gt;

&lt;p&gt;Three decisions around it that I think matter more than the feature itself:&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;first&lt;/strong&gt; scan of a repository says so out loud. There is no baseline yet, so there is nothing to compare against — and staying quiet there would be indistinguishable from "nothing changed". A tool that cannot tell you the difference between &lt;em&gt;I looked and it is fine&lt;/em&gt; and &lt;em&gt;I had nothing to look at&lt;/em&gt; is worse than one that admits it.&lt;/p&gt;

&lt;p&gt;A baseline older than a month still lists what changed, but &lt;strong&gt;stops affecting the verdict&lt;/strong&gt;. Months of perfectly legitimate drift, scored as suspicion, would just train you to ignore the whole thing.&lt;/p&gt;

&lt;p&gt;And files that change constantly are &lt;strong&gt;not tracked at all&lt;/strong&gt; — &lt;code&gt;package.json&lt;/code&gt;, editor task files. Diffing those would bury any real signal under your own commits.&lt;/p&gt;

&lt;p&gt;This is the one file octoscope writes on its own: &lt;code&gt;scan-baselines.json&lt;/code&gt;, sitting beside your config. Deleting it is harmless — the next scan simply starts a fresh baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a compromise could reach
&lt;/h2&gt;

&lt;p&gt;The second half is your capability footprint: what workflow permissions and triggers you declare, self-hosted runners, write-access deploy keys, webhooks delivering somewhere other than GitHub.&lt;/p&gt;

&lt;p&gt;Here is the part I want to be clear about, because getting it wrong would have made the feature worthless: &lt;strong&gt;holding power is not a finding.&lt;/strong&gt; octoscope's own release workflow asks for &lt;code&gt;contents: write&lt;/code&gt; and reads two secrets, and it is completely correct — it fires on a tag push, so only someone who can already push tags can reach it. If that scored, a large share of GitHub would light up, and everyone would learn to ignore the axis within a week.&lt;/p&gt;

&lt;p&gt;What scores is power reachable from &lt;strong&gt;untrusted input&lt;/strong&gt;: a &lt;code&gt;pull_request_target&lt;/code&gt; workflow holding the repository's secrets, or a self-hosted runner that an outsider's pull request can actually target. Both halves of that matter — a fork-triggered job running on a hosted runner, sitting beside a self-hosted runner your release workflow uses, is two unrelated facts rather than an escalation.&lt;/p&gt;

&lt;p&gt;The checks that need admin scope &lt;strong&gt;fail open&lt;/strong&gt;: a token without them still gets a complete scan of everything else. But failing open is not the same as staying quiet, so the report names what it could not look at — &lt;em&gt;"deploy keys (the token lacks the scope this needs)"&lt;/em&gt; — and says plainly that a clean result covers only what was checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  And a signal that sat unused for two months
&lt;/h2&gt;

&lt;p&gt;There is a timing check that spots a worm fanning out: many repositories pushed within seconds of each other, the way the reference attack hit five in 49 seconds. It has been built and tested since 0.20.0 and connected to nothing, because it shipped once as an always-on banner and had to be pulled. Timing alone cannot tell a fan-out from someone scripting an update across their own repos, and without a recency gate an old batch re-alarmed forever.&lt;/p&gt;

&lt;p&gt;Folding it into the on-demand scan fixes both objections at once. It only looks at the last hour, and it only ever adds weight to a repository that &lt;strong&gt;already&lt;/strong&gt; scored on another check. A push burst by itself is a Tuesday. A push burst next to a fresh ignition point is something to go and look at.&lt;/p&gt;

&lt;h2&gt;
  
  
  One check is never enough
&lt;/h2&gt;

&lt;p&gt;Worth saying because it constrains everything above: no single axis can reach a high verdict on its own. The capability half carries an explicit ceiling below the &lt;em&gt;suspicious&lt;/em&gt; threshold, so even its worst possible shape lands on &lt;em&gt;watch&lt;/em&gt; and needs a second, independent signal to agree before the verdict escalates.&lt;/p&gt;

&lt;p&gt;That ceiling exists because a review caught the first version reaching &lt;em&gt;suspicious&lt;/em&gt; by itself. The findings past the ceiling are still reported in full — the arithmetic is bounded, not the disclosure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew upgrade gfazioli/tap/octoscope
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open a repository in the Repos tab and press &lt;code&gt;s&lt;/code&gt;. The first scan will tell you it has nothing to compare against yet, which is exactly the point — the second one won't.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;a href="https://gfazioli.github.io/octoscope" rel="noopener noreferrer"&gt;https://gfazioli.github.io/octoscope&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://octoscope.substack.com" rel="noopener noreferrer"&gt;https://octoscope.substack.com&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hachyderm.io/@undolog" rel="noopener noreferrer"&gt;https://hachyderm.io/@undolog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cli</category>
      <category>github</category>
      <category>git</category>
      <category>go</category>
    </item>
  </channel>
</rss>
