<?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: Asuran</title>
    <description>The latest articles on DEV Community by Asuran (@zkasuran).</description>
    <link>https://dev.to/zkasuran</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%2F4072471%2F23ca2796-c59f-4181-abb0-4dec9252b824.png</url>
      <title>DEV Community: Asuran</title>
      <link>https://dev.to/zkasuran</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zkasuran"/>
    <language>en</language>
    <item>
      <title>A charity does not close because its overhead was too high. I checked 328,186 tax returns.</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:04:16 +0000</pubDate>
      <link>https://dev.to/zkasuran/a-charity-does-not-close-because-its-overhead-was-too-high-i-checked-328186-tax-returns-kha</link>
      <guid>https://dev.to/zkasuran/a-charity-does-not-close-because-its-overhead-was-too-high-i-checked-328186-tax-returns-kha</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I have given money to charities for years and I have never once known whether the one I gave to was about to close. The number everybody puts in front of you is overhead, the share of spending that does not go to programmes. Every rating site leads with it. I assumed it meant something.&lt;/p&gt;

&lt;p&gt;So this weekend I downloaded the IRS Statistics of Income extracts of Form 990 and Form 990-EZ, which is 3,697,515 filings covering 700,873 organisations across fiscal 2017 to 2024, then asked a narrower question. Not whether a charity is efficient. Whether it is going to still be there.&lt;/p&gt;

&lt;p&gt;I took the 328,186 organisations that filed a long-form Form 990 for fiscal 2018 or 2019, then checked which of them appear on any filing for fiscal 2022 or later. &lt;strong&gt;8.88% do not.&lt;/strong&gt; Then I scored two candidate signals inside each of ten spending deciles, so that nothing I found could turn out to be a story about big charities outliving small ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feeding America spends $4.93 billion a year and holds 1.04 months of it in cash.&lt;/strong&gt; The Greater Chicago Food Depository spends $261 million and holds half a month. Second Harvest Food Bank of Central Florida holds nine days. None of that is on any rating site, because the number nobody computes for you is the one that decides whether the lights stay on.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;&lt;a href="https://keepalive.zkasuran.dev" rel="noopener noreferrer"&gt;Keepalive&lt;/a&gt;&lt;/strong&gt; computes months of runway for 579,178 US nonprofits, straight off their own filings. Runway is cash plus savings divided by one month of spending. It is the number a finance director lives by and the number a donor never sees.&lt;/p&gt;

&lt;p&gt;Across 1,573,687 long-form filings the median is &lt;strong&gt;5.5 months&lt;/strong&gt;. &lt;strong&gt;33.5% hold under three months.&lt;/strong&gt; 15.1% hold under one.&lt;/p&gt;

&lt;p&gt;The site does four things with that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where does my gift buy the most time.&lt;/strong&gt; Say what you can give, pick a cause and a state. It ranks real organisations by days of runway your money adds, which is &lt;code&gt;365 * yearly gift / annual spending&lt;/code&gt;. $25 a month is 12 hours of runway at a $260 million food bank and 9.4 days at a $23,000 one. The same money, three orders of magnitude apart in effect. That single line changed how I give.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look up any of the 579,178&lt;/strong&gt; by name or EIN, with seven years of revenue, spending and cash on one chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the brief.&lt;/strong&gt; Gemini writes it from a fixed packet of that filer's own figures and is handed nothing else, so there is nothing for it to invent. It has to return a line naming what a tax filing cannot tell you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check it.&lt;/strong&gt; One click publishes the SHA-256 of the figures on screen and the brief just written as a memo on Solana devnet, so what Keepalive said about an organisation on a given date lives somewhere its author cannot quietly edit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The finding
&lt;/h3&gt;

&lt;p&gt;Both tables below hold inside every one of ten spending deciles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runway predicts survival.&lt;/strong&gt; Thin runway is the worst of the three bands in all ten deciles. Pooled with deciles weighted equally: &lt;strong&gt;86.79%&lt;/strong&gt; of the under-three-months group was still filing, against 92.61% at three to twelve months and 93.56% above a year. The gap is 10.3 points in the smallest decile and 1.6 in the largest, so it bites hardest exactly where the money is smallest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fundraising thrift points the wrong way.&lt;/strong&gt; The organisations that spent under five cents to raise a dollar survived &lt;em&gt;less&lt;/em&gt; than the five-to-fifteen-cent band, in all ten deciles: &lt;strong&gt;91.16%&lt;/strong&gt; against 94.86%. Spending more on fundraising went with surviving more. In nine of the ten deciles the thriftiest band is the worst of the three outright. Only one of these two numbers is on the rating sites. It is the one that is backwards.&lt;/p&gt;

&lt;p&gt;That is the starvation cycle, visible in tax returns. Judge a charity on how little it spends on itself and you reward the ones with no capacity to raise next year's money.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live: &lt;a href="https://keepalive.zkasuran.dev" rel="noopener noreferrer"&gt;https://keepalive.zkasuran.dev&lt;/a&gt;&lt;/strong&gt; No sign-in, nothing to install.&lt;/p&gt;

&lt;p&gt;Try it in 30 seconds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the site. The two survival tables under &lt;strong&gt;The finding&lt;/strong&gt; are the whole result, drawn from the same query.&lt;/li&gt;
&lt;li&gt;In the search box type &lt;strong&gt;greater chicago food depository&lt;/strong&gt;. Open it. The runway card reads &lt;strong&gt;0.5 months&lt;/strong&gt; on a &lt;strong&gt;$261m&lt;/strong&gt; budget. The peer card shows &lt;strong&gt;95.6%&lt;/strong&gt; of organisations in its own spending decile and runway band still filing.&lt;/li&gt;
&lt;li&gt;Scroll to &lt;strong&gt;The honest brief&lt;/strong&gt; and wait about eight seconds. Gemini writes it live. Read the last line, the one that starts "What these filings cannot tell you".&lt;/li&gt;
&lt;li&gt;Press &lt;strong&gt;Publish a checkable receipt&lt;/strong&gt;. About four seconds later you get a Solana devnet signature. Open it. The memo on chain holds the SHA-256 of exactly what you just read.&lt;/li&gt;
&lt;li&gt;Back at the top, set the amount to &lt;strong&gt;25 per month&lt;/strong&gt;, cause &lt;strong&gt;Food, agriculture and nutrition&lt;/strong&gt;, state &lt;strong&gt;TN&lt;/strong&gt;. The list re-ranks by days of runway your $300 a year adds.&lt;/li&gt;
&lt;/ol&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/zkasuran" rel="noopener noreferrer"&gt;
        zkasuran
      &lt;/a&gt; / &lt;a href="https://github.com/zkasuran/keepalive" rel="noopener noreferrer"&gt;
        keepalive
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Months of runway for 579,178 US nonprofits, read off their own IRS filings
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Keepalive&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;A charity does not close because its overhead was too high. It closes because it ran out
of months.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Keepalive reads 3,697,515 IRS filings and computes, for 579,178 US nonprofits, the one
number no rating site puts in front of you: how many months of spending they hold in cash
Then it tells you what your gift does to that number.&lt;/p&gt;
&lt;p&gt;Live: &lt;strong&gt;&lt;a href="https://keepalive.zkasuran.dev" rel="nofollow noopener noreferrer"&gt;https://keepalive.zkasuran.dev&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/challenges/weekend-2026-09-03" rel="nofollow"&gt;DEV Weekend Challenge: Generosity Edition&lt;/a&gt;
4 to 7 September 2026. Everything in this repository was written inside that window.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The finding&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Two candidate signals, measured on the same 328,186 organisations that filed a long-form
Form 990 for fiscal 2018 or 2019, scored on whether the same EIN appears on any filing for
fiscal 2022 or later. 8.88% do not.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Months of runway predicts survival.&lt;/strong&gt; Under three months is the worst of the three bands
in all ten spending deciles, with no exception.&lt;/p&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/zkasuran/keepalive" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;code&gt;npm run data&lt;/code&gt; reproduces every number: it pulls the IRS extracts, loads 3.7 million filings, prints the finding and writes the static shards the site reads. It needs no credentials and no account.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;There is no runtime database. The site is 4,542 static files on a CDN plus four small functions that hold keys a browser cannot have.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Search is a static inverted index.&lt;/strong&gt; 579,178 names tokenise to 1,910,797 postings, hashed into 2,048 shard files. The browser hashes your query tokens, fetches only the shards it needs and intersects them. Typing "greater chicago food depository" costs three fetches and no server. Each posting list is ordered by spending, which matters more than it sounds: truncate a common token like "food" and the intersection silently stops finding real names. That was my first real bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Organisation records are sharded by EIN modulo 1024&lt;/strong&gt;, so one lookup is one 250 KB fetch that the CDN then holds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google AI&lt;/strong&gt; writes the brief under a JSON schema, from a packet containing every number it is permitted to mention and no others. Absent fields are dropped rather than sent as zero, because "not reported" and "$0" are different claims and a model handed a zero will tell you the charity has no employees. The system prompt forbids computing a new ratio. One measured detail: Gemini 3.5 Flash spends its thinking tokens out of the same &lt;code&gt;maxOutputTokens&lt;/code&gt; budget, about 2,200 of them here, so a tight cap truncates the JSON and the parse fails. That looks exactly like a bad model and is not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solana&lt;/strong&gt; is the only place the receipts live. Keepalive keeps no copy. Remove Solana and the app can no longer prove that today's number is the number it computed from the filing, which is the honest answer to "how do I know your AI did not make this up".&lt;/p&gt;

&lt;p&gt;The transaction is built against the wire format rather than with a client library. A memo transaction is one signature, two account keys and one instruction. Ed25519 is already in &lt;code&gt;crypto.subtle&lt;/code&gt;, so an SDK would have added megabytes to buy nothing. &lt;a href="https://github.com/zkasuran/keepalive/blob/main/test/attest.wire.test.mjs" rel="noopener noreferrer"&gt;&lt;code&gt;test/attest.wire.test.mjs&lt;/code&gt;&lt;/a&gt; proves those bytes equal the ones &lt;code&gt;@solana/web3.js&lt;/code&gt; produces: message, signature and full serialized transaction, byte for byte, including a memo that crosses the compact-u16 boundary.&lt;/p&gt;

&lt;p&gt;One thing I want to state plainly rather than hide. The signature is made on the server, where the key is. The transaction is submitted by your browser. That is not a design flourish. The public devnet RPC answers Cloudflare's egress with &lt;code&gt;403 Your IP or provider is blocked from this endpoint&lt;/code&gt; while a visitor's own connection is fine. That RPC allows any origin. So the key stays server side and the network call happens where the network works. The only thing a client controls is the blockhash, which is length checked and which cannot turn a memo into a transfer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Snowflake&lt;/strong&gt; is the warehouse path, in &lt;a href="https://github.com/zkasuran/keepalive/blob/main/data/snowflake.sql" rel="noopener noreferrer"&gt;&lt;code&gt;data/snowflake.sql&lt;/code&gt;&lt;/a&gt;: the same cohort plus one statement that cuts it into deciles then scores both signals inside every decile at once. Cortex does the language work next to the data, with &lt;code&gt;AI_CLASSIFY&lt;/code&gt; over names the IRS only gave a letter code and &lt;code&gt;AI_AGG&lt;/code&gt; writing one sentence per filer from all of its returns together. There is a licensing line I held to: Marketplace terms do not permit redistributing a dataset, so every per-organisation figure this site serves comes from my own public domain IRS download instead. Only aggregates ever leave a warehouse. Sources and the exact granting sentence for each are in &lt;a href="https://github.com/zkasuran/keepalive/blob/main/DATA-SOURCES.md" rel="noopener noreferrer"&gt;DATA-SOURCES.md&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ElevenLabs&lt;/strong&gt; reads it out, because the reason a lot of people cannot use a giving tool is that it is a wall of financial text. The free tier is about ten minutes of audio a month, so architecture had to do the work rather than budget: everything that never changes is pre-rendered once at build time by &lt;code&gt;tools/narrate.mjs&lt;/code&gt; into static mp3. A visitor who plays the tour or the finding spends nothing. Only a brief about one specific filer is synthesised live, cached in the Worker by a SHA-256 of the text, so the second person to read the same organisation is free too. You can watch it happen: the response carries &lt;code&gt;x-keepalive-audio: fresh&lt;/code&gt; the first time and &lt;code&gt;cached&lt;/code&gt; after.&lt;/p&gt;

&lt;p&gt;The tour is in English, Hindi, Tamil and Spanish. Gemini does the translation with an instruction to keep every figure exactly as stated. ElevenLabs speaks it. The English stays the source of truth. That is the one part of this build I would call generous rather than clever. A US tax filing is hard to read in your first language. The people most likely to be deciding about a small charity back home are not all reading English.&lt;/p&gt;

&lt;p&gt;Two things that will bite anyone doing this on a free key. A scoped key answers &lt;code&gt;401 missing_permissions&lt;/code&gt; on &lt;code&gt;GET /v2/voices&lt;/code&gt;, so a voice id has to be configured rather than discovered, with a short list of defaults to fall back through. Free accounts also get &lt;code&gt;402 paid_plan_required&lt;/code&gt; on library voices, so a voice id copied out of a blog post is a dead end. The four in the list are ones I verified synthesise on this key.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is real and what is not
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Every figure on an organisation page is that organisation's own reported number, labelled with the fiscal year, computed in your browser from a static file. Nothing is modelled and nothing is imputed.&lt;/li&gt;
&lt;li&gt;Form 990-EZ does not break out cash, so EZ filers show no runway rather than a guess.&lt;/li&gt;
&lt;li&gt;The survival tables are associations, not causation, controlled for size by decile and for nothing else.&lt;/li&gt;
&lt;li&gt;Solana devnet, not mainnet. No money moves anywhere in this project. Keepalive is not a payment processor and never touches a donation.&lt;/li&gt;
&lt;li&gt;Keepalive is not a charity rating. It can tell you what an organisation's finances look like. It cannot tell you whether they are good at the work. Neither can any rating site.&lt;/li&gt;
&lt;li&gt;AI assistance (Claude) was used in building this. The design, the analysis and the verification are mine. Every number above came out of SQL I can rerun in front of you.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Google AI.&lt;/strong&gt; Gemini writes every brief under a JSON schema from a packet of that filer's own figures, with a rule that a missing field means not reported. It also writes the narration translations without touching a single figure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Solana.&lt;/strong&gt; A devnet memo is the only store of record for what this site published. Remove it and no claim on the page is checkable. The transaction is hand built and proved byte-identical to the reference implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs.&lt;/strong&gt; The tour and the finding are pre-rendered at build time in four languages, so listening costs no credits and needs no key. A brief about one organisation is spoken live, then cached by a hash of the text.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Snowflake path is written and in the repository as &lt;a href="https://github.com/zkasuran/keepalive/blob/main/data/snowflake.sql" rel="noopener noreferrer"&gt;&lt;code&gt;data/snowflake.sql&lt;/code&gt;&lt;/a&gt;, and the live deploy tells you plainly that its credential is not bound: &lt;code&gt;/api/status&lt;/code&gt; returns &lt;code&gt;snowflake: false&lt;/code&gt;. The finding tables you are reading were produced by the same SQL run locally in DuckDB rather than in a warehouse. I would rather say that than claim a category a judge cannot see working. If it changes before the deadline I will say so here in an edit, with the timestamp.&lt;/p&gt;

&lt;p&gt;Thanks for a theme worth building for. The most useful thing I learned is that the kindest gift to a small charity is not the largest one, it is the predictable one. The number to ask about is months.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>My checkout said it was ready for a month. Nobody could have paid.</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 04 Sep 2026 22:23:29 +0000</pubDate>
      <link>https://dev.to/zkasuran/my-checkout-said-it-was-ready-for-a-month-nobody-could-have-paid-3lb9</link>
      <guid>https://dev.to/zkasuran/my-checkout-said-it-was-ready-for-a-month-nobody-could-have-paid-3lb9</guid>
      <description>&lt;p&gt;The dashboard for my payment provider reads, for the whole life of the account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Revenue since inception   USD 0.00
Transaction History       Total count: 0
Users                     Total count: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live mode and sandbox mode, same numbers. The easy reading is that nobody wanted it.&lt;/p&gt;

&lt;p&gt;Here is the harder reading. Over the eight days my analytics still retains, 21 of 228 real visits reached the pricing page. Not one of those 21 had anything to click. The buy button was there. It was styled, it was enabled, it said "Sign in to start the trial". It had no click handler on it at all.&lt;/p&gt;

&lt;p&gt;Three separate defects, none of them visible from reading the code, all three provable from the network tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  One: my own content security policy blocked the SDK
&lt;/h2&gt;

&lt;p&gt;The policy I shipped was tight on purpose. The whole frontend is first party, so I named exactly what it needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;default-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;script-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt; &lt;span class="s"&gt;https://esm.sh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;style-src&lt;/span&gt;  &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;img-src&lt;/span&gt;    &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;data:&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;connect-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;https://esm.sh&lt;/span&gt; &lt;span class="s"&gt;https://api.tiun.live&lt;/span&gt; &lt;span class="s"&gt;https://api-sandbox.tiun.live&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that and it looks right. The SDK module comes from esm.sh, so esm.sh is a script source. The SDK talks to the API host, so the API host is a connect source. Done.&lt;/p&gt;

&lt;p&gt;An SDK is not one request. Here is what &lt;code&gt;init()&lt;/code&gt; actually does, from the shipped bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;link&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;stylesheet&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cssUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scriptUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It appends a stylesheet and a script pointing at the API host. Those are &lt;code&gt;style-src&lt;/code&gt; and &lt;code&gt;script-src&lt;/code&gt; decisions. The sign in surface renders in a frame, which is &lt;code&gt;frame-src&lt;/code&gt;. With no &lt;code&gt;frame-src&lt;/code&gt; the browser falls back to &lt;code&gt;default-src 'self'&lt;/code&gt;. The one place I named the host was the one place it did not matter.&lt;/p&gt;

&lt;p&gt;Driving the page over the DevTools protocol says it plainly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;requestWillBeSent   .../snippets/&amp;lt;id&amp;gt;&lt;/span&gt;/background_css
&lt;span class="gp"&gt;requestWillBeSent   .../snippets/&amp;lt;id&amp;gt;&lt;/span&gt;/background_js
&lt;span class="go"&gt;Network.loadingFailed
Network.loadingFailed
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both fetch fine with curl. 84 KB of CSS, 506 KB of JS, 200 each. The refusal was mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reason nobody noticed
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;waitForReady()&lt;/code&gt; resolves when the snippet posts back that it initialised. With the runtime bundle refused, that message never arrives, so the promise never settles. My code awaits it at module top level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitForReady&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// everything below here never ran&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[data-buy]&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;button&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&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="nx"&gt;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The page did not throw. It did not show an error. It stopped, silently, above the line that wires the buttons. It kept the markup the HTML shipped with. That markup shows the sign in prompt and hides the buy controls, because that is the correct signed out state. A correct looking page with no behaviour behind it.&lt;/p&gt;

&lt;p&gt;I had written a &lt;code&gt;checkoutUnavailable()&lt;/code&gt; path for exactly this and it never fired, because nothing rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two: getUser is synchronous
&lt;/h2&gt;

&lt;p&gt;Fixing the policy moved the failure one layer up. From the SDK source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;getUser&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;isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_isAuthenticated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_user&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;No promise. My code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&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;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUser&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&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;.catch&lt;/code&gt; on a plain object is a TypeError. It is thrown inside an &lt;code&gt;async function&lt;/code&gt;, so it does not crash the page, it becomes a rejected promise nobody is awaiting. That function is the one that decides what the top bar and the buy controls look like. It died on its first line, on every call, for a month. The only trace was a single &lt;code&gt;Uncaught (in promise)&lt;/code&gt; line in a console nobody had open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three: the purchases are one level down
&lt;/h2&gt;

&lt;p&gt;Same call, look at the shape again. It is &lt;code&gt;{ isAuthenticated, user }&lt;/code&gt;. The email and the entitlements live on the inner object. I read them off the outer one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&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;productAccess&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always &lt;code&gt;{}&lt;/code&gt;. Always false. So the control that hands a paying customer their CLI licence could never appear, for anyone, ever. If someone had pushed through the other two bugs and paid me money, they would have got nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  And a fourth, for free
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;logout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkout:complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user:updated&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;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;on&lt;/span&gt;&lt;span class="p"&gt;?.(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;paint&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;The SDK emits seven event names. &lt;code&gt;checkout:complete&lt;/code&gt; is not one of them. &lt;code&gt;user:updated&lt;/code&gt; is not one of them. The real names are &lt;code&gt;ready&lt;/code&gt;, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;, &lt;code&gt;userChange&lt;/code&gt;, &lt;code&gt;paywallShow&lt;/code&gt;, &lt;code&gt;paywallHide&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;. Subscribing to a name that is never emitted is not an error anywhere. It is a listener that waits forever. A completed purchase repainted nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;on()&lt;/code&gt; is called with &lt;code&gt;?.&lt;/code&gt; too, so even if the method vanished the line would stay quiet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually got wrong
&lt;/h2&gt;

&lt;p&gt;Not the policy. Policies are fiddly and that mistake is cheap to make.&lt;/p&gt;

&lt;p&gt;What I got wrong is that I treated &lt;code&gt;waitForReady()&lt;/code&gt; resolving as proof the checkout worked. It is not proof of anything. It is one signal from one layer. Every layer under it can be broken while it still resolves. I built my whole readiness story on a check that could not fail.&lt;/p&gt;

&lt;p&gt;I write a tool that finds exactly this in test suites. Tests that call a function and assert nothing. Assertions that cannot be false. Gates no workflow invokes. Every one of them is a signal that stays green because it is not actually connected to the thing it claims to measure. I had one of those in my own shop, on the page where people pay me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The policy now names the hosts everywhere the SDK reaches through. Nowhere else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;script-src&lt;/span&gt; &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt; &lt;span class="s"&gt;https://esm.sh&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;api&lt;/span&gt; &lt;span class="s"&gt;hosts&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;style-src&lt;/span&gt;  &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;'unsafe-inline'&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;api&lt;/span&gt; &lt;span class="s"&gt;hosts&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;img-src&lt;/span&gt;    &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;data:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;api&lt;/span&gt; &lt;span class="s"&gt;hosts&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;https://assets.tiun.dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;font-src&lt;/span&gt;   &lt;span class="s"&gt;'self'&lt;/span&gt; &lt;span class="s"&gt;data:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;api&lt;/span&gt; &lt;span class="s"&gt;hosts&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;https://assets.tiun.dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;frame-src&lt;/span&gt;  &lt;span class="s"&gt;&amp;lt;api&lt;/span&gt; &lt;span class="s"&gt;hosts&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The font host turned up on the next run, because the stylesheet asks it for one webfont. It gets &lt;code&gt;font-src&lt;/code&gt; and &lt;code&gt;img-src&lt;/code&gt; and nothing else. A font host has no business being a script source. Nothing is widened to a bare &lt;code&gt;https:&lt;/code&gt;, which would allow every host on the internet and is the shortcut I did not want.&lt;/p&gt;

&lt;p&gt;Then a readiness check that can fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;assertSdkArrived&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="nx"&gt;cspBlocked&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="nx"&gt;CSP_BLOCKED&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;loaded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getEntriesByType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resource&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;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/background_js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseEnd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;loaded&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="s2"&gt;tiun runtime bundle never loaded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;tiun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkout&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;function&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;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="s2"&gt;tiun sdk is missing login or checkout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A resource the policy blocked never enters the Resource Timing buffer, so asking the buffer is a real question with a real answer. Alongside it, a &lt;code&gt;securitypolicyviolation&lt;/code&gt; listener records the directive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FATAL_DIRECTIVES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;script-src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;style-src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;frame-src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;connect-src&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;securitypolicyviolation&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;e&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="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockedURI&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tiun&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;FATAL_DIRECTIVES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effectiveDirective&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;cspBlocked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effectiveDirective&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; blocked &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;blockedURI&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That set matters. My first version treated any blocked provider URL as fatal, so the blocked webfont disabled a checkout that had just started working. A blocked font is ugly. A blocked script is a dead checkout. Only one of those should stop the sale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests, since the whole point is not trusting a green light
&lt;/h2&gt;

&lt;p&gt;19 of them. They pin facts rather than behaviour I cannot exercise headlessly.&lt;/p&gt;

&lt;p&gt;One file reads the policy and asserts every directive the SDK needs names both hosts, that the font host is a font source but not a script or frame source, that nothing is a bare scheme, then that the directives with nothing to do with payments still read &lt;code&gt;'none'&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The other reads the frontend as text and holds it to the SDK: no &lt;code&gt;.catch&lt;/code&gt; on &lt;code&gt;getUser()&lt;/code&gt;, purchases read from the nested object by name, then every event subscribed to is one of the seven the SDK actually emits. It blanks comments before matching, because the comments describe the old wrong call and would otherwise match themselves.&lt;/p&gt;

&lt;p&gt;128 tests pass now. They would all have passed before too, which is the point: none of them was looking here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;The sign in modal opens on &lt;a href="https://margyn.xyz/pricing" rel="noopener noreferrer"&gt;margyn.xyz/pricing&lt;/a&gt;. Clicking that button did nothing yesterday.&lt;/p&gt;

&lt;p&gt;I still have zero sales. That number has not moved. What changed is that it now means something about my product instead of something about my content security policy. Those are very different problems to have. For a month I was working on the wrong one.&lt;/p&gt;

&lt;p&gt;The scanner is &lt;a href="https://margyn.xyz" rel="noopener noreferrer"&gt;margyn&lt;/a&gt;, free for four checks, no account and no network call. Source is on &lt;a href="https://github.com/zkasuran/margyn" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>A test said the server started. I deleted the server. It still passed.</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 28 Aug 2026 03:18:57 +0000</pubDate>
      <link>https://dev.to/zkasuran/a-test-said-the-server-started-i-deleted-the-server-it-still-passed-5gch</link>
      <guid>https://dev.to/zkasuran/a-test-said-the-server-started-i-deleted-the-server-it-still-passed-5gch</guid>
      <description>&lt;p&gt;Here is a test from a real, well run Node project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server starts&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;t&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;server started&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads fine in review. It runs green. Now delete the body of &lt;code&gt;build()&lt;/code&gt; so the server never comes up. The test is still green, because the only thing it asserts is &lt;code&gt;true&lt;/code&gt;. In the same file two more of these caught the error in a &lt;code&gt;catch&lt;/code&gt; and asserted &lt;code&gt;true&lt;/code&gt; there too, so even the failure path was green.&lt;/p&gt;

&lt;p&gt;That is not a made up example. I found it in fastify at a pinned commit and opened a PR to fix it. More on that at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  A whole class of tests cannot fail
&lt;/h2&gt;

&lt;p&gt;Once you start looking, the pattern turns up in a few shapes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A literal: &lt;code&gt;assert.ok(true)&lt;/code&gt;, &lt;code&gt;expect(1).toBe(1)&lt;/code&gt;, a snapshot of a constant.&lt;/li&gt;
&lt;li&gt;An assertion parked in a &lt;code&gt;catch&lt;/code&gt; the happy path never reaches, so nothing is checked when the code works and nothing is checked when it breaks.&lt;/li&gt;
&lt;li&gt;A status list that accepts both outcomes: &lt;code&gt;assert.ok([200, 500].includes(res.status))&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each one runs, counts toward coverage and guards nothing. Coverage is the trap. The line executed, so the tool that counts executed lines is happy. Whether the line would go red on a regression is a different question. It is the one that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why review misses it
&lt;/h2&gt;

&lt;p&gt;A reviewer reading the diff sees a test called &lt;code&gt;server starts&lt;/code&gt;, an &lt;code&gt;await listen&lt;/code&gt; and a green tick. The name states intent. The assertion is what actually runs, yet &lt;code&gt;ok(true)&lt;/code&gt; does not look like a problem until you stop and ask what would ever turn this test red. A missing check does not show up in a diff the way a wrong line does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding them
&lt;/h2&gt;

&lt;p&gt;I wrote a small scanner for this. No account, no config file, no network call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx margyn-scan /path/to/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of its checks is &lt;code&gt;cannot-fail&lt;/code&gt;: tests whose assertions hold whatever the code does. It also flags tests that assert nothing at all, files the build reads that git never committed, gates declared in &lt;code&gt;package.json&lt;/code&gt; that no workflow invokes and linter exclusions that quietly drop tracked source. Every finding prints the command that reproduces it, so you confirm each one yourself instead of trusting a report.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fastify case
&lt;/h2&gt;

&lt;p&gt;The two tests above asserted &lt;code&gt;ok(true)&lt;/code&gt; right after starting a server. I opened &lt;a href="https://github.com/fastify/fastify/pull/6962" rel="noopener noreferrer"&gt;fastify/fastify#6962&lt;/a&gt; to assert the server is actually listening instead. A maintainer reviewed it and suggested tightening it further to &lt;code&gt;t.assert.ok(server.listening)&lt;/code&gt; with &lt;code&gt;t.after&lt;/code&gt; for teardown. Both are in and the PR is open for review. The point is not that fastify is careless. It is one of the most carefully tested projects in the ecosystem, which is exactly why it matters that this still slips through.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;A green suite tells you the tests ran. It does not tell you they would go red if the code they cover regressed. Those are two different claims. Teams merge on the first while believing the second. The gap is worth auditing, by hand or with something like &lt;a href="https://margyn.xyz" rel="noopener noreferrer"&gt;margyn&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>node</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a scanner that audits your test suite instead of your code</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Tue, 18 Aug 2026 01:14:39 +0000</pubDate>
      <link>https://dev.to/zkasuran/i-built-a-scanner-that-audits-your-test-suite-instead-of-your-code-5c1g</link>
      <guid>https://dev.to/zkasuran/i-built-a-scanner-that-audits-your-test-suite-instead-of-your-code-5c1g</guid>
      <description>&lt;p&gt;A passing test suite is supposed to mean something. Here are three ways it lies. All three stay green forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test that asserts nothing.&lt;/strong&gt; It calls the function, nothing throws, the runner prints a tick. It counts toward coverage and guards nothing. fastify at commit &lt;code&gt;6e95cb9&lt;/code&gt; has seven of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An assertion that cannot fail.&lt;/strong&gt; A literal answered inside a catch, a swallowed assertion, a status list that accepts both the success and the failure. The test reads as rigorous and holds whatever the code does. fastify has two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A gate nobody invokes.&lt;/strong&gt; A &lt;code&gt;test-cov&lt;/code&gt; or &lt;code&gt;verify&lt;/code&gt; script sits in package.json, reads as coverage to every reviewer, then no workflow ever calls it. express declares three.&lt;/p&gt;

&lt;p&gt;None of that is visible to a code reviewer, to a coverage percentage or to an AI reading the diff, because the absence of a check does not appear in a diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  So the tool reads the checks rather than the code
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx margyn-scan /path/to/repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No account, no config file, zero dependencies, nothing uploaded. It answers in about a second. Every finding prints one command that reproduces it on your machine.&lt;/p&gt;

&lt;p&gt;Six checks: &lt;code&gt;no-assertion&lt;/code&gt;, &lt;code&gt;cannot-fail&lt;/code&gt;, &lt;code&gt;ignored-source&lt;/code&gt; (a file the build reads that git never committed, so a clean clone cannot build), &lt;code&gt;unrun-check&lt;/code&gt;, &lt;code&gt;lint-blindspot&lt;/code&gt; (linter exclusions that come from the ignore file rather than the linter's own config) and a mutation proof.&lt;/p&gt;

&lt;p&gt;Measured at pinned commits, nothing tuned for the demo and nothing left out because the number was inconvenient:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repository&lt;/th&gt;
&lt;th&gt;Commit&lt;/th&gt;
&lt;th&gt;Findings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;chalk/chalk&lt;/td&gt;
&lt;td&gt;&lt;code&gt;661317e&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sindresorhus/execa&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8017b27&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sindresorhus/got&lt;/td&gt;
&lt;td&gt;&lt;code&gt;e3924aa&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1 unrun gate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;expressjs/express&lt;/td&gt;
&lt;td&gt;&lt;code&gt;a371447&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3 unrun gates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fastify/fastify&lt;/td&gt;
&lt;td&gt;&lt;code&gt;6e95cb9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;13 (7 assert nothing, 2 cannot fail, 4 unrun)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Proof mode, because a scanner should not be believed either
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx margyn-scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--prove&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs each finding's own command, reads the output for the markers that finding predicted, then certifies it REPRODUCED. Anything it cannot reproduce is RETRACTED and dropped, so a CI gate never fails a build on a claim the tool could not show on your own tree. On fastify today: 13 findings, 13 reproduced.&lt;/p&gt;

&lt;p&gt;That mode exists because the tool was badly wrong once. An earlier version of &lt;code&gt;ignored-source&lt;/code&gt; produced 22 false positives across four real repositories. Proof mode certified every one of them, because the check and its proof had inherited the same wrong premise. &lt;a href="https://dev.to/zkasuran/my-scanner-was-wrong-22-times-and-its-own-proof-certified-every-one-2m5n"&gt;I wrote that up separately&lt;/a&gt;, fixture included, since it is the more interesting half of building this.&lt;/p&gt;

&lt;h2&gt;
  
  
  It runs in CI and posts on the pull request
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zkasuran/margyn@v0&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;sarif&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It comments on the request, edits that same comment in place on every push, writes the report to the job summary and uploads the findings to code scanning, all with your own &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; and no service of mine in the path.&lt;/p&gt;

&lt;p&gt;There is a live run to click rather than a screenshot to trust: &lt;a href="https://github.com/zkasuran/margyn/pull/1" rel="noopener noreferrer"&gt;pull request 1&lt;/a&gt; plants two faults on purpose, a test that calls the scanner and asserts nothing plus a script no workflow invokes. The suite still reads 109 tests passing. The comment lists both findings with a reproduction each, both appear in the Security tab, then the job exits 1, which is the gate working.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does to itself
&lt;/h2&gt;

&lt;p&gt;The mutation proof inverts a line, runs your suite, then reports the suite that stayed green anyway. Over this repository: 40 files tracked as source, 27 carry a mutation it knows how to make, 27 mutated, 27 caught, no survivors, across 108 tests.&lt;/p&gt;

&lt;p&gt;Two honest numbers to go with that. It takes 208 seconds, because every mutation runs the whole suite once, which is why the default cap is four rather than every file. And zero survivors is a claim about this suite against seven operators, not a claim that the code is correct.&lt;/p&gt;

&lt;p&gt;The first time it ran on itself it found eight survivors, one of them inside the mutation checker. The one that mattered most was in the worker: inverted, the entitlement branch handed a licence to every customer who had not paid. Nothing was watching that line.&lt;/p&gt;

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

&lt;p&gt;It does not test your product, it audits the suite you already have. It does not upload your code, because there is no endpoint that accepts it. It will not tell you your code is correct. It tells you which of your existing checks cannot fail, which is a smaller claim and a checkable one.&lt;/p&gt;

&lt;p&gt;The five static checks are free forever. The mutation proof is the paid one, because it runs your suite N times and that is the part with a real cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on something you own
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx margyn-scan &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MIT, no telemetry, no account: &lt;a href="https://margyn.xyz" rel="noopener noreferrer"&gt;margyn.xyz&lt;/a&gt; and &lt;a href="https://github.com/zkasuran/margyn" rel="noopener noreferrer"&gt;github.com/zkasuran/margyn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The question I would genuinely like answered, because the default is still open: when a tool finds a hollow test in your suite, should it fail the build or only comment on the pull request? Margyn defaults to failing. I am not certain that is right.&lt;/p&gt;

&lt;p&gt;Built during the tiun x Microlaunch challenge on &lt;a href="https://hackwithus.dev" rel="noopener noreferrer"&gt;hackwithus.dev&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used while building this and while writing this post. The design, the review and every number above are mine. Each number ships with the command that re-derives it.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>javascript</category>
      <category>node</category>
      <category>devops</category>
    </item>
    <item>
      <title>My scanner was wrong 22 times and its own proof certified every one</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Tue, 18 Aug 2026 00:53:06 +0000</pubDate>
      <link>https://dev.to/zkasuran/my-scanner-was-wrong-22-times-and-its-own-proof-certified-every-one-2m5n</link>
      <guid>https://dev.to/zkasuran/my-scanner-was-wrong-22-times-and-its-own-proof-certified-every-one-2m5n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I built a scanner that audits the verification layer of a repository rather than the code. One of its checks looks for a file the build reads that git never committed: the file is on your disk untracked, so every local run is green while a clean clone or a CI runner cannot read it at all. The diff looks innocent because the defect is an absence.&lt;/p&gt;

&lt;p&gt;Because a scanner that only asserts is not much better than the tests it audits, it also has a proof mode. Every finding carries a machine-checkable command. &lt;code&gt;--prove&lt;/code&gt; runs that command, reads the output for the markers the finding predicted, then certifies the finding as REPRODUCED. Anything it cannot reproduce is retracted and dropped, so a CI gate never fails a build on a claim the tool could not show on your own tree.&lt;/p&gt;

&lt;p&gt;Then I pointed it at five real repositories I had lying around. It produced 22 findings on four of them. Every single one was wrong. Here is the part that still bothers me: the proof certified all 22.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the false positive looked like
&lt;/h2&gt;

&lt;p&gt;Reduced to the shape it kept taking, on a repository anybody can build in ten seconds:&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; web/public/tour web/src &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git init
&lt;span class="nb"&gt;echo &lt;/span&gt;webm-bytes &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; web/public/tour/clip.webm
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export const clip = "/tour/clip.webm";'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; web/src/app.js
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'node_modules\nweb/dist\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .gitignore
git add &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"app plus the clip it plays, dist ignored"&lt;/span&gt;
npm run build     &lt;span class="c"&gt;# or just: mkdir -p web/dist/tour &amp;amp;&amp;amp; cp web/public/tour/clip.webm web/dist/tour/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;clip.webm&lt;/code&gt; is committed. It sits in &lt;code&gt;web/public&lt;/code&gt;, which is exactly where a Vite or Next project keeps assets it wants copied verbatim. The build then writes a second copy into &lt;code&gt;web/dist&lt;/code&gt;, which is gitignored because a clean clone rebuilds it.&lt;/p&gt;

&lt;p&gt;My check walked the source, found &lt;code&gt;"/tour/clip.webm"&lt;/code&gt;, resolved it against the files on disk, hit &lt;code&gt;web/dist/tour/clip.webm&lt;/code&gt; first, saw that git ignores it and reported HIGH: the build reads a file that is not in the commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. web/dist/tour/clip.webm is read by web/src/app.js but git ignores it, so it is not in the commit
   HIGH  ignored-source  web/dist/tour/clip.webm
   ignore rule: .gitignore:2:web/dist
   why: A clean clone or a CI runner cannot read this file.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read on its own, that finding is defensible. The file it names really is untracked. Every fact in it is true. It is still wrong, because the thing the reader asked for is in the commit. A clean clone answers the request perfectly.&lt;/p&gt;

&lt;p&gt;On the real repositories it was the same fault at scale: a Vite app reporting its whole public directory, a Foundry project reporting &lt;code&gt;contracts/out&lt;/code&gt; (declared &lt;code&gt;out = "out"&lt;/code&gt; in its own &lt;code&gt;foundry.toml&lt;/code&gt;), a CLI reporting &lt;code&gt;dist/cli.mjs&lt;/code&gt; (declared &lt;code&gt;outdir: "dist"&lt;/code&gt; in its own build script). 22 findings, 4 repositories, zero real defects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that actually scared me
&lt;/h2&gt;

&lt;p&gt;Here is the same fixture through proof mode, on the unfixed scanner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. web/dist/tour/clip.webm is read by web/src/app.js but git ignores it, so it is not in the commit
   HIGH  ignored-source  web/dist/tour/clip.webm  REPRODUCED
   MARGYN_ABSENT_FROM_HEAD
   MARGYN_PRESENT_ON_DISK

2 findings: 2 reproduced.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;REPRODUCED. The proof ran, both markers matched and the tool certified its own mistake with a green badge.&lt;/p&gt;

&lt;p&gt;The proof was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git archive HEAD | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qx&lt;/span&gt; &lt;span class="s1"&gt;'web/dist/tour/clip.webm'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'ABSENT from HEAD: web/dist/tour/clip.webm'&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'web/dist/tour/clip.webm'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'PRESENT on disk: web/dist/tour/clip.webm'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both lines are correct. &lt;code&gt;web/dist/tour/clip.webm&lt;/code&gt; genuinely is absent from HEAD and genuinely is present on disk. The proof passes because it asks the same wrong question the check asked: &lt;strong&gt;is this path committed&lt;/strong&gt;, when the question that decides the bug is &lt;strong&gt;does anything in the commit answer the path the reader asked for&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the lesson I paid for. A check and its proof can agree, both be internally valid and both be wrong, because they inherited the same premise. Independent verification means asking a different question, not running the assertion twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A reader asks for a path, not for a file on your disk.&lt;/strong&gt; The check now collects every needle a reader names, then reports only when no committed file answers it. &lt;code&gt;web/public/tour/clip.webm&lt;/code&gt; in the commit satisfies &lt;code&gt;/tour/clip.webm&lt;/code&gt;, whatever the build left in &lt;code&gt;dist&lt;/code&gt;. The proof asks that question too, so a finding that slips through retracts itself instead of failing somebody's build over a copy of a committed file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regenerated output is not missing source.&lt;/strong&gt; The check skips output a tool in the repository declares it writes: Foundry's &lt;code&gt;out&lt;/code&gt; from &lt;code&gt;foundry.toml&lt;/code&gt;, a Vite or Next or Cargo target from the script that runs it, an &lt;code&gt;outdir&lt;/code&gt; in a build script or a &lt;code&gt;tsconfig&lt;/code&gt;. Read from declarations and resolved to real paths, never from the directory happening to be called &lt;code&gt;dist&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That second rule matters more than it looks. One of the five repositories had vendored real source into &lt;code&gt;vendor/dist&lt;/code&gt;, a path no tool there declares. That is exactly the defect this check exists for. A crude "ignore anything named dist" would have deleted the only true positive in the set.&lt;/p&gt;

&lt;p&gt;So the fixture keeps both cases. Same tree, one import added, scanner at HEAD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;1. vendor/dist/real.mjs is &lt;span class="nb"&gt;read &lt;/span&gt;by web/src/vendorised.js but git ignores it, so it is not &lt;span class="k"&gt;in &lt;/span&gt;the commit
   HIGH  ignored-source  vendor/dist/real.mjs
   reproduce:
     git archive HEAD | &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qE&lt;/span&gt; &lt;span class="s1"&gt;'(^|/)dist/real\.mjs$'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'NOTHING in HEAD answers dist/real.mjs'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build copy is gone. The vendored module is still reported. And look at the new reproduce line: it asks whether anything in HEAD answers the reference, which is the question the fix is built on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;4 regression tests in &lt;code&gt;test/checks.test.mjs&lt;/code&gt;, one per direction: quiet when the commit answers the path, still loud when nothing does, quiet on declared build output, still loud on vendored source under a path that only looks built.&lt;/li&gt;
&lt;li&gt;The fixture above, run against the pre-fix commit and against HEAD: 2 findings before, 1 after, then the one that survives is the real one.&lt;/li&gt;
&lt;li&gt;Full suite 108 tests passing. The tool's own mutation proof mutates all 27 mutable files of 40 tracked and reports no survivors.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While I was in there, one more thing turned up that nothing was watching: the SARIF &lt;code&gt;helpUri&lt;/code&gt; for the mutation rule pointed at &lt;code&gt;/docs#mutation&lt;/code&gt; while the docs heading carried &lt;code&gt;id="mutation-check"&lt;/code&gt;, so every uploaded finding linked to nothing. There is a test now that resolves the &lt;code&gt;helpUri&lt;/code&gt; of every check the scanner can emit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell my past self
&lt;/h2&gt;

&lt;p&gt;Write the check and the proof from different premises. Otherwise the proof is a spell-check on the check's own reasoning. Mine was, for 22 findings, with a green badge on each.&lt;/p&gt;

&lt;p&gt;Code: the fix is one commit, &lt;code&gt;efc5657&lt;/code&gt;, in &lt;a href="https://github.com/zkasuran/margyn" rel="noopener noreferrer"&gt;github.com/zkasuran/margyn&lt;/a&gt;. The scanner runs with no account and no config: &lt;code&gt;npx margyn-scan /path/to/repo&lt;/code&gt;. &lt;code&gt;--prove&lt;/code&gt; is the mode that got humbled here.&lt;/p&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used while writing this post and while implementing the fix. The bug, the reduced fixture, the two rules and every number above were verified by me by running them: the outputs quoted are captured stdout from the pre-fix commit and from HEAD, not retyped.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>testing</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Pup Card: what's your dog really thinking? (Gemini 3.5 Flash)</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 14 Aug 2026 14:10:06 +0000</pubDate>
      <link>https://dev.to/zkasuran/pup-card-whats-your-dog-really-thinking-gemini-35-flash-3bhm</link>
      <guid>https://dev.to/zkasuran/pup-card-whats-your-dog-really-thinking-gemini-35-flash-3bhm</guid>
      <description>&lt;p&gt;This is a submission for &lt;a href="https://dev.to/devteam/join-our-dev-weekend-challenge-dog-days-edition-1000-in-prizes-across-five-winners-submissions-1g4i"&gt;Weekend Challenge: Dog Days Edition&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Pup Card turns one dog photo into a shareable card. Drop in a photo or hit &lt;strong&gt;Surprise me&lt;/strong&gt; for a random dog. Gemini 3.5 Flash reads the picture and hands back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the likely &lt;strong&gt;breed&lt;/strong&gt; with a confidence score, plus the mix if it is not purebred&lt;/li&gt;
&lt;li&gt;the dog's &lt;strong&gt;mood&lt;/strong&gt; and a plain read of its &lt;strong&gt;body language&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;first-person caption&lt;/strong&gt;, as if the dog is talking&lt;/li&gt;
&lt;li&gt;two or three breed-appropriate &lt;strong&gt;care tips&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;fun fact&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It all renders as a card you can download as a PNG and share. I wanted something that was genuinely fun to use on a Saturday and that leaned on the one thing a multimodal model is really good at: looking at an image and saying something specific and true about it.&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%2Fsb3m9rlz9su3c4lr6i8u.gif" 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%2Fsb3m9rlz9su3c4lr6i8u.gif" alt="Pup Card reading a random dog, start to finish" width="460" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live demo: &lt;a href="https://pupcard.vercel.app" rel="noopener noreferrer"&gt;https://pupcard.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It needs no login and runs on your phone. If you do not have a dog photo on hand, the &lt;strong&gt;Surprise me&lt;/strong&gt; button pulls a random one so you can try it right now.&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%2Fxni6liyasl32dgm11qmg.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%2Fxni6liyasl32dgm11qmg.png" alt="A finished Pup Card" width="800" height="1512"&gt;&lt;/a&gt;&lt;/p&gt;

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


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/zkasuran" rel="noopener noreferrer"&gt;
        zkasuran
      &lt;/a&gt; / &lt;a href="https://github.com/zkasuran/pupcard" rel="noopener noreferrer"&gt;
        pupcard
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Snap a dog photo, get an AI Pup Card: breed, mood and what they're thinking. Powered by Gemini 3.5 Flash.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🐶 Pup Card&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Snap a dog photo and get back a Pup Card: the likely breed, a read on the dog's mood and
body language, a first-person line of what they are probably thinking and a few care
notes. One multimodal call to Google Gemini 3.5 Flash does the whole read.&lt;/p&gt;
&lt;p&gt;Built for the &lt;a href="https://dev.to/devteam" rel="nofollow"&gt;DEV Weekend Challenge: Dog Days&lt;/a&gt;, for the &lt;strong&gt;Best
use of Google AI&lt;/strong&gt; category.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo: &lt;a href="https://pupcard.vercel.app" rel="nofollow noopener noreferrer"&gt;https://pupcard.vercel.app&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/zkasuran/pupcard/docs/pup-card.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fzkasuran%2Fpupcard%2FHEAD%2Fdocs%2Fpup-card.png" alt="A Pup Card for a Bluetick Coonhound"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Upload a dog photo (or hit &lt;strong&gt;Surprise me&lt;/strong&gt; to pull a random dog from &lt;a href="https://dog.ceo" rel="nofollow noopener noreferrer"&gt;dog.ceo&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;Gemini 3.5 Flash looks at the image and returns a structured read: breed guess with a
confidence score, likely mix, mood, body language, a playful first-person caption, 2-3
care tips and a fun fact.&lt;/li&gt;
&lt;li&gt;The result renders as a card you can download as a PNG and share.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No dog photo on hand? The &lt;strong&gt;Surprise me&lt;/strong&gt; button means anyone can…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/zkasuran/pupcard" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;The whole read is a single &lt;code&gt;generateContent&lt;/code&gt; call to &lt;code&gt;gemini-3.5-flash&lt;/code&gt;. The photo goes in as inline image data next to a prompt that asks for JSON in a fixed shape. A &lt;a href="https://zod.dev" rel="noopener noreferrer"&gt;zod&lt;/a&gt; schema validates the response so the front end always gets the same fields. If the model returns malformed JSON the server retries once with the parse error fed back. If the primary model is overloaded or out of quota it falls back to &lt;code&gt;gemini-2.5-flash&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The stack is deliberately small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3.5 Flash&lt;/strong&gt; through &lt;code&gt;@google/genai&lt;/code&gt; for the read&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript&lt;/strong&gt; everywhere, with &lt;strong&gt;Hono&lt;/strong&gt; for the local server&lt;/li&gt;
&lt;li&gt;plain &lt;strong&gt;Node serverless functions&lt;/strong&gt; on &lt;strong&gt;Vercel&lt;/strong&gt; for &lt;code&gt;/api/analyze&lt;/code&gt; and &lt;code&gt;/api/random-dog&lt;/code&gt;, so there is no long-lived server to run&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;vanilla&lt;/strong&gt; HTML, CSS and JS front end with a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; export for the downloadable card, so there is no build step and nothing shipped to the browser but three static files&lt;/li&gt;
&lt;li&gt;the free &lt;strong&gt;dog.ceo&lt;/strong&gt; API for the random dog&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two details I liked. Photos are downscaled in the browser before upload, which keeps the request tiny and the read fast. And the caption is where 3.5 Flash shows off: it picks up on the actual photo, the odd-colored eyes, the golden-hour light, the paw draped over a toy, instead of writing something generic.&lt;/p&gt;

&lt;p&gt;The Gemini key stays on the server. The public endpoint has no login, so it is guarded by a size cap and a simple per-IP rate limit instead.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Google AI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Gemini is not a feature bolted onto Pup Card, it is the product. One multimodal Gemini 3.5 Flash call does the breed identification, the mood and body-language read, the writing and the care tips, all returned as structured JSON that the card renders directly. There is no separate model, no rules engine and no template behind it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built over the challenge weekend. I used an AI coding assistant to help write it, then designed the prompt, reviewed the code and tested the whole thing end to end against real dog photos with live Gemini calls before shipping.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>The reasoning tokens Sentry's Vercel AI traces dropped for Gemini</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:06:17 +0000</pubDate>
      <link>https://dev.to/zkasuran/the-reasoning-tokens-sentrys-vercel-ai-traces-dropped-for-gemini-3ib2</link>
      <guid>https://dev.to/zkasuran/the-reasoning-tokens-sentrys-vercel-ai-traces-dropped-for-gemini-3ib2</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Sentry's JavaScript SDK auto-instruments the Vercel AI SDK, so every &lt;code&gt;generateText&lt;/code&gt; or &lt;code&gt;streamText&lt;/code&gt; call shows up as a &lt;code&gt;gen_ai&lt;/code&gt; span with token usage attached. When the model is a Gemini reasoning model, that usage was wrong: the span undercounted the output and the total because it silently dropped Gemini's reasoning tokens.&lt;/p&gt;

&lt;p&gt;Gemini reports its reasoning ("thoughts") tokens separately from the visible answer. The AI SDK's &lt;code&gt;outputTokens&lt;/code&gt; covers only the candidate answer and exposes the reasoning count through &lt;code&gt;providerMetadata.google.usageMetadata.thoughtsTokenCount&lt;/code&gt;. Sentry's &lt;code&gt;getProviderMetadataAttributes()&lt;/code&gt; handled OpenAI, Anthropic, Bedrock and DeepSeek metadata, but it never looked at the Google or Vertex block, so the reasoning tokens vanished from the span and the total was computed as input plus candidate-only output.&lt;/p&gt;

&lt;p&gt;On a real Gemini call this is not a rounding error. The model returned &lt;code&gt;usageMetadata { promptTokenCount: 14, candidatesTokenCount: 1, thoughtsTokenCount: 100, totalTokenCount: 115 }&lt;/code&gt;. Sentry recorded &lt;code&gt;output_tokens: 1&lt;/code&gt; and &lt;code&gt;total_tokens: 15&lt;/code&gt;, hiding 100 reasoning tokens. For a reasoning model the reasoning tokens are most of the cost, so the trace understated real usage by nearly 8x.&lt;/p&gt;

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

&lt;p&gt;The fix adds a Google and Vertex branch to &lt;code&gt;getProviderMetadataAttributes()&lt;/code&gt; in &lt;code&gt;packages/server-utils/src/ai/vercel-ai/index.ts&lt;/code&gt;. It reads &lt;code&gt;providerMetadata.{google|vertex}.usageMetadata&lt;/code&gt; and, when &lt;code&gt;thoughtsTokenCount&lt;/code&gt; is greater than zero, sets the output to &lt;code&gt;candidatesTokenCount + thoughtsTokenCount&lt;/code&gt;, sets the total from the real &lt;code&gt;totalTokenCount&lt;/code&gt; and records the reasoning breakdown under &lt;code&gt;gen_ai.usage.reasoning.output_tokens&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Deriving the output from the raw candidate and thoughts counts, rather than adding reasoning onto the SDK's existing value, is deliberate: it stays correct even if a future AI SDK version folds reasoning into &lt;code&gt;outputTokens&lt;/code&gt; itself, so it cannot double count. The branch is gated on &lt;code&gt;thoughtsTokenCount &amp;gt; 0&lt;/code&gt;, so a non-reasoning Gemini response is left exactly as it was. Both the OpenTelemetry span path and the &lt;code&gt;ai&lt;/code&gt; tracing-channel path go through this one helper, so both emit the corrected shape.&lt;/p&gt;

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

&lt;p&gt;I added a test file with four cases. A Gemini reasoning response now records &lt;code&gt;output_tokens: 101&lt;/code&gt;, &lt;code&gt;total_tokens: 115&lt;/code&gt; and &lt;code&gt;gen_ai.usage.reasoning.output_tokens: 100&lt;/code&gt;. The &lt;code&gt;vertex&lt;/code&gt; metadata variant is handled the same way. A non-reasoning response is unchanged. Run against the current source the three reasoning cases fail (&lt;code&gt;expected 1 to be 101&lt;/code&gt;) and the non-reasoning case passes, which pins the bug. With the fix all four pass.&lt;/p&gt;

&lt;p&gt;Green on the repo's own gates: the full &lt;code&gt;@sentry/server-utils&lt;/code&gt; vitest suite (377 passing across 40 files, my four added), &lt;code&gt;oxlint --type-aware&lt;/code&gt;, &lt;code&gt;oxfmt --check&lt;/code&gt; and &lt;code&gt;tsc&lt;/code&gt; on the source types all clean. I confirmed the fix against the AI SDK's own source: &lt;code&gt;convert-google-usage.ts&lt;/code&gt; maps output to candidates plus thoughts and reasoning to thoughts, which is exactly the accounting the fix uses.&lt;/p&gt;

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

&lt;p&gt;This is a fix to Sentry's own JavaScript SDK, in the Vercel AI instrumentation that feeds the AI monitoring product. Token usage on a &lt;code&gt;gen_ai&lt;/code&gt; span is what teams read to track model cost and behavior. For a reasoning model the reasoning tokens are the bulk of the spend, so an &lt;code&gt;output_tokens&lt;/code&gt; that omits them makes the trace understate cost in the direction that hurts most. After the fix the span carries the real output and total, so Sentry's usage numbers match what Gemini actually counted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;I verified this end to end with a real Google Gemini reasoning call, because the whole bug only appears when a model emits &lt;code&gt;thoughtsTokenCount&lt;/code&gt;. A real &lt;code&gt;gemini-3.6-flash&lt;/code&gt; request returned &lt;code&gt;{ promptTokenCount: 14, candidatesTokenCount: 1, thoughtsTokenCount: 100, totalTokenCount: 115 }&lt;/code&gt;. Fed through the actual Sentry Vercel AI processor, the emitted span attributes were &lt;code&gt;output_tokens: 1, total_tokens: 15&lt;/code&gt; before the fix and &lt;code&gt;output_tokens: 101, total_tokens: 115, reasoning.output_tokens: 100&lt;/code&gt; after. Google Gemini produced the reasoning tokens. Sentry is where they reappear once the SDK reads the Google usage metadata correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PR:&lt;/strong&gt; &lt;a href="https://github.com/getsentry/sentry-javascript/pull/23433" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#23433&lt;/a&gt;, from branch &lt;code&gt;fix/vercel-ai-gemini-reasoning-tokens&lt;/code&gt;. Found by probing the integration with real Gemini traffic, not from a filed issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by the author. Verified locally before submitting: the four new tests fail on the unpatched source and pass with the fix, the &lt;code&gt;@sentry/server-utils&lt;/code&gt; suite (377 passing), &lt;code&gt;oxlint --type-aware&lt;/code&gt;, &lt;code&gt;oxfmt --check&lt;/code&gt; and &lt;code&gt;tsc&lt;/code&gt; on the source types, plus a real Gemini reasoning run showing the span output and total tokens corrected.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>One tool call, counted twice: a Google GenAI streaming double-dip in Sentry's JS SDK</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 14 Aug 2026 09:44:00 +0000</pubDate>
      <link>https://dev.to/zkasuran/one-tool-call-counted-twice-a-google-genai-streaming-double-dip-in-sentrys-js-sdk-4l4p</link>
      <guid>https://dev.to/zkasuran/one-tool-call-counted-twice-a-google-genai-streaming-double-dip-in-sentrys-js-sdk-4l4p</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;When you call &lt;code&gt;@google/genai&lt;/code&gt; in streaming mode and the model asks to run a tool, Sentry's JavaScript SDK records that tool call to the span twice. One tool call in, two entries out.&lt;/p&gt;

&lt;p&gt;The attribute that carries them is &lt;code&gt;gen_ai.response.tool_calls&lt;/code&gt;. It should hold one object per call. For a single streamed &lt;code&gt;controlLight&lt;/code&gt; call it held two. Worse, the two did not even agree on their shape. Here is a real capture, which I come back to at the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_2079699"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"colorTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"warm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"controlLight"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_2079699"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"controlLight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"colorTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"warm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same id, same call, listed twice. One entry keys the parameters under &lt;code&gt;args&lt;/code&gt;, the other under &lt;code&gt;arguments&lt;/code&gt;. Anything reading this later sees two tool invocations where the model made one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the value
&lt;/h2&gt;

&lt;p&gt;The streaming instrumentation lives in &lt;code&gt;packages/server-utils/src/ai/google-genai/streaming.ts&lt;/code&gt;. Every chunk of the stream runs through &lt;code&gt;handleCandidateContent&lt;/code&gt;. That function wrote tool calls from two places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleCandidateContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recordOutputs&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// push #1&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&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="c1"&gt;// ...finish reasons...&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordOutputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseTexts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;                              &lt;span class="c1"&gt;// push #2&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Push #1 spreads &lt;code&gt;chunk.functionCalls&lt;/code&gt; into the accumulator. Push #2 walks &lt;code&gt;candidate.content.parts&lt;/code&gt; and pushes every &lt;code&gt;functionCall&lt;/code&gt; it finds. They look like two different sources. They are not.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chunk.functionCalls&lt;/code&gt; is a getter on the &lt;code&gt;@google/genai&lt;/code&gt; response object. Here is what it actually does inside the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;get&lt;/span&gt; &lt;span class="nf"&gt;functionCalls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;functionCalls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&lt;/span&gt;
    &lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCall&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fc&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;fc&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="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;functionCalls&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 reads the exact same &lt;code&gt;candidates[].content.parts&lt;/code&gt; that push #2 iterates, filters for &lt;code&gt;functionCall&lt;/code&gt; and hands them back. So both pushes are pulling the identical tool call out of the identical parts array. The first keeps the SDK-native shape &lt;code&gt;{ id, name, args }&lt;/code&gt;. The second rebuilds it as &lt;code&gt;{ type, id, name, arguments }&lt;/code&gt;. Two views of one thing, both written down.&lt;/p&gt;

&lt;p&gt;The non-streaming path never had this problem. Its &lt;code&gt;addResponseAttributes&lt;/code&gt; reads tool calls once, straight from &lt;code&gt;response.functionCalls&lt;/code&gt;, then emits a single entry per call. So streaming and non-streaming disagreed on both the count and the key name for the very same response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;There is a single source of truth here: the SDK accessor the non-streaming path already trusts. Take the tool calls only from &lt;code&gt;chunk.functionCalls&lt;/code&gt; and drop the second push:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleCandidateContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;recordOutputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// `chunk.functionCalls` is the SDK accessor over the candidate's function-call parts,&lt;/span&gt;
  &lt;span class="c1"&gt;// so it is the single source of truth for tool calls. Also reading `part.functionCall`&lt;/span&gt;
  &lt;span class="c1"&gt;// from those same parts would record every call twice, with two different shapes. This&lt;/span&gt;
  &lt;span class="c1"&gt;// mirrors the non-streaming path, which likewise takes tool calls from `response.functionCalls`.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCalls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;functionCalls&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="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidates&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="c1"&gt;// ...finish reasons...&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;parts&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recordOutputs&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;responseTexts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&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;I kept &lt;code&gt;chunk.functionCalls&lt;/code&gt; rather than the parts loop on purpose. Both code paths now share one source and emit the SDK-native shape, so streaming output matches non-streaming output byte for byte. That is also how the OpenAI and Anthropic integrations in this repo are built: their streaming paths reconstruct the exact tool-call shape their non-streaming paths produce. The deprecated &lt;code&gt;gen_ai.response.tool_calls&lt;/code&gt; example happens to show &lt;code&gt;{ name, arguments }&lt;/code&gt;, but no provider integration normalizes to that literally (Anthropic keeps &lt;code&gt;input&lt;/code&gt;, OpenAI nests under &lt;code&gt;function&lt;/code&gt;), so consistency within a provider was the property worth protecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it on real traffic
&lt;/h2&gt;

&lt;p&gt;I did not want to trust a mock for this. I made one real streaming call to &lt;code&gt;gemini-3.6-flash&lt;/code&gt; with a &lt;code&gt;controlLight&lt;/code&gt; tool, captured the actual chunks the model sent back (response id &lt;code&gt;IuF-auTiDpW2g8UPnNDKsAI&lt;/code&gt;), then replayed that identical response through the instrumentation twice: once against &lt;code&gt;develop&lt;/code&gt;, once with the fix.&lt;/p&gt;

&lt;p&gt;Before, &lt;code&gt;gen_ai.response.tool_calls&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_2079699"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"colorTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"warm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"controlLight"&lt;/span&gt;&lt;span class="p"&gt;},{&lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_2079699"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"controlLight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"colorTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"warm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;}}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"call_2079699"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:{&lt;/span&gt;&lt;span class="nl"&gt;"colorTemperature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"warm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"brightness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"controlLight"&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One real call, one entry.&lt;/p&gt;

&lt;p&gt;A unit test covers the single-call case, several calls arriving across chunks and the &lt;code&gt;recordOutputs: false&lt;/code&gt; case, then checks that the non-streaming path still records one entry in the same shape. It fails on &lt;code&gt;develop&lt;/code&gt; with two entries and passes with the fix. Locally the full &lt;code&gt;@sentry/server-utils&lt;/code&gt; unit suite is green (377 passing), with &lt;code&gt;oxlint&lt;/code&gt;, &lt;code&gt;oxfmt --check&lt;/code&gt; and the TypeScript build all clean.&lt;/p&gt;

&lt;p&gt;The change is open as &lt;a href="https://github.com/getsentry/sentry-javascript/pull/23432" rel="noopener noreferrer"&gt;getsentry/sentry-javascript#23432&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by me. I verified it locally before submitting: the new and existing &lt;code&gt;@sentry/server-utils&lt;/code&gt; unit tests, &lt;code&gt;oxlint&lt;/code&gt;, &lt;code&gt;oxfmt --check&lt;/code&gt; and the TypeScript build all pass, plus the real gemini-3.6-flash run captured before and after.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The hidden svg Biome flagged anyway: a noSvgWithoutTitle false positive</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Fri, 14 Aug 2026 08:13:58 +0000</pubDate>
      <link>https://dev.to/zkasuran/the-hidden-svg-biome-flagged-anyway-a-nosvgwithouttitle-false-positive-51jh</link>
      <guid>https://dev.to/zkasuran/the-hidden-svg-biome-flagged-anyway-a-nosvgwithouttitle-false-positive-51jh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;A Biome user filed &lt;a href="https://github.com/biomejs/biome/issues/11317" rel="noopener noreferrer"&gt;issue #11317&lt;/a&gt;: the &lt;code&gt;noSvgWithoutTitle&lt;/code&gt; lint rule flagged &lt;code&gt;&amp;lt;svg aria-hidden&amp;gt;&amp;lt;rect /&amp;gt;&amp;lt;/svg&amp;gt;&lt;/code&gt;, even though that svg is hidden from the accessibility tree and does not need a title. Write it as &lt;code&gt;&amp;lt;svg aria-hidden="true"&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;svg aria-hidden={true}&amp;gt;&lt;/code&gt; and the rule stays quiet. Write it with the JSX boolean shorthand &lt;code&gt;&amp;lt;svg aria-hidden&amp;gt;&lt;/code&gt; and the rule reports "title element cannot be empty". Same meaning, different result.&lt;/p&gt;

&lt;p&gt;The shorthand is not a niche style. In JSX, &lt;code&gt;aria-hidden&lt;/code&gt; with no value is exactly &lt;code&gt;aria-hidden={true}&lt;/code&gt;, the same way &lt;code&gt;disabled&lt;/code&gt; is &lt;code&gt;disabled={true}&lt;/code&gt; on an input. React users write it constantly, so the rule was punishing correct, accessible code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the value
&lt;/h2&gt;

&lt;p&gt;The rule lives in &lt;code&gt;crates/biome_js_analyze/src/lint/a11y/no_svg_without_title.rs&lt;/code&gt;. The guard that is supposed to let a hidden svg through looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aria_hidden_attr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="nf"&gt;.find_attribute_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aria-hidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr_static_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aria_hidden_attr&lt;/span&gt;&lt;span class="nf"&gt;.as_static_value&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="n"&gt;attr_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr_static_val&lt;/span&gt;&lt;span class="nf"&gt;.text&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;attr_text&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&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;Read it top down. Find the &lt;code&gt;aria-hidden&lt;/code&gt; attribute. Read its static value. If that value is the string &lt;code&gt;"true"&lt;/code&gt;, return &lt;code&gt;None&lt;/code&gt;, which means no diagnostic. Reasonable for &lt;code&gt;aria-hidden="true"&lt;/code&gt; and &lt;code&gt;aria-hidden={true}&lt;/code&gt;. The trouble is the second line, &lt;code&gt;as_static_value()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is what that helper does, in &lt;code&gt;crates/biome_js_syntax/src/jsx_ext.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;as_static_value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;StaticValue&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;self&lt;/span&gt;&lt;span class="nf"&gt;.initializer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.as_static_value&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;The first thing it touches is &lt;code&gt;self.initializer()?&lt;/code&gt;. An initializer is the &lt;code&gt;= something&lt;/code&gt; part of an attribute. &lt;code&gt;aria-hidden="true"&lt;/code&gt; has one. &lt;code&gt;aria-hidden={true}&lt;/code&gt; has one. The bare shorthand &lt;code&gt;aria-hidden&lt;/code&gt; has none. For the shorthand, &lt;code&gt;initializer()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, the &lt;code&gt;?&lt;/code&gt; short circuits and &lt;code&gt;as_static_value()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;. The &lt;code&gt;&amp;amp;&amp;amp; let Some(..)&lt;/code&gt; in the guard fails, the whole block is skipped and the svg falls through to the title checks and gets flagged.&lt;/p&gt;

&lt;p&gt;So the rule never actually decided the shorthand was not hidden. It just never got far enough to ask. The value was gone one line before the comparison that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The shorthand carries its truth in its presence, not in a value. The moment we know the attribute exists but has no initializer, we can treat it as &lt;code&gt;true&lt;/code&gt; and stop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aria_hidden_attr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="nf"&gt;.find_attribute_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"aria-hidden"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// In JSX the boolean shorthand `aria-hidden` (an attribute with no&lt;/span&gt;
    &lt;span class="c1"&gt;// initializer) is equivalent to `aria-hidden={true}`, so it hides&lt;/span&gt;
    &lt;span class="c1"&gt;// the svg from the accessibility tree and no title is required.&lt;/span&gt;
    &lt;span class="n"&gt;aria_hidden_attr&lt;/span&gt;&lt;span class="nf"&gt;.initializer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr_static_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aria_hidden_attr&lt;/span&gt;&lt;span class="nf"&gt;.as_static_value&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attr_static_val&lt;/span&gt;&lt;span class="nf"&gt;.text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;aria_hidden_attr.initializer()?&lt;/code&gt; is the whole fix. If there is no initializer we are looking at the shorthand, so &lt;code&gt;?&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; from the rule and no diagnostic is raised. If there is an initializer, the existing &lt;code&gt;"true"&lt;/code&gt; check runs exactly as before, so &lt;code&gt;aria-hidden="true"&lt;/code&gt;, &lt;code&gt;aria-hidden={true}&lt;/code&gt; and &lt;code&gt;aria-hidden={false}&lt;/code&gt; all keep the behavior they had. Clippy insists on the &lt;code&gt;?&lt;/code&gt; form here over an explicit &lt;code&gt;if ... .is_none() { return None; }&lt;/code&gt;. Biome's CI denies warnings, so the terse version is the one that compiles clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch it fail, then pass
&lt;/h2&gt;

&lt;p&gt;I added two valid cases to the rule's spec at &lt;code&gt;crates/biome_js_analyze/tests/specs/a11y/noSvgWithoutTitle/valid.jsx&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;rect&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt; &lt;span class="na"&gt;aria-hidden&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;rect&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the source reverted to the old guard but these cases in place, the spec test fails on exactly the shorthand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;× Alternative text title element cannot be empty
  &amp;gt; 43 │  &amp;lt;svg aria-hidden&amp;gt;&amp;lt;rect /&amp;gt;&amp;lt;/svg&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Line 44, &lt;code&gt;&amp;lt;svg aria-hidden={true}&amp;gt;&lt;/code&gt;, does not fail, which matches the report: the value forms always worked, only the shorthand was broken. Put the one-line fix back and both cases pass while the &lt;code&gt;invalid.jsx&lt;/code&gt; spec still flags every svg that genuinely has no title.&lt;/p&gt;

&lt;p&gt;The rest of the gates are green too. &lt;code&gt;cargo run -p rules_check&lt;/code&gt; passes, so the new valid example I added to the rule's documentation is checked the same way. &lt;code&gt;cargo fmt --all --check&lt;/code&gt; is clean. &lt;code&gt;cargo clippy -p biome_js_analyze --all-features --all-targets -- --deny warnings&lt;/code&gt; is clean. The snapshot regenerates with &lt;code&gt;INSTA_UPDATE=always&lt;/code&gt; and produces no changes beyond the new input lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change
&lt;/h2&gt;

&lt;p&gt;This is a fix inside Biome itself, in the linter. Issue &lt;a href="https://github.com/biomejs/biome/issues/11317" rel="noopener noreferrer"&gt;#11317&lt;/a&gt;, pull request &lt;a href="https://github.com/biomejs/biome/pull/11334" rel="noopener noreferrer"&gt;biomejs/biome#11334&lt;/a&gt;, branch &lt;a href="https://github.com/zkasuran/biome/tree/fix/no-svg-without-title-shorthand-aria-hidden" rel="noopener noreferrer"&gt;&lt;code&gt;fix/no-svg-without-title-shorthand-aria-hidden&lt;/code&gt;&lt;/a&gt; at commit &lt;code&gt;5d92567&lt;/code&gt;. One statement of source in &lt;code&gt;no_svg_without_title.rs&lt;/code&gt;, two spec cases, a documentation example and a changeset. A dead read of a value that was never there, fixed by asking the question one line earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used to trace the root cause, write the fix and the test cases, then run the checks. I own the change, reviewed it and verified it locally before submitting. Verified: the rule spec test passes with the fix and fails on the shorthand case without it; &lt;code&gt;cargo run -p rules_check&lt;/code&gt;, &lt;code&gt;cargo fmt --all --check&lt;/code&gt; and &lt;code&gt;cargo clippy -p biome_js_analyze --all-features --all-targets -- --deny warnings&lt;/code&gt; are clean; the snapshot regenerates with no changes.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The fallback that never fired: empty SDK name and version in Sentry .NET console apps</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Thu, 13 Aug 2026 01:33:31 +0000</pubDate>
      <link>https://dev.to/zkasuran/the-fallback-that-never-fired-empty-sdk-name-and-version-in-sentry-net-console-apps-5c4c</link>
      <guid>https://dev.to/zkasuran/the-fallback-that-never-fired-empty-sdk-name-and-version-in-sentry-net-console-apps-5c4c</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that only showed up in console apps
&lt;/h2&gt;

&lt;p&gt;A Sentry .NET user filed &lt;a href="https://github.com/getsentry/sentry-dotnet/issues/5352" rel="noopener noreferrer"&gt;issue #5352&lt;/a&gt;: running the &lt;code&gt;Sentry.Samples.Console.Basic&lt;/code&gt; sample, every structured log went out with an empty &lt;code&gt;sentry.sdk.name&lt;/code&gt; and an empty &lt;code&gt;sentry.sdk.version&lt;/code&gt;. They also suspected trace metrics had the same problem. They were right.&lt;/p&gt;

&lt;p&gt;Those two attributes are not decoration. They tell Sentry which SDK produced a log or a metric. Lose them and a whole class of apps ships telemetry that cannot be attributed back to the .NET SDK. The strange part: the exact same logging code inside an ASP.NET Core app was fine. Only console apps dropped the fields. That split is the whole story, so this post walks the trace that explains it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the value
&lt;/h2&gt;

&lt;p&gt;Logs and metrics both end up in one method, &lt;code&gt;SetDefaultAttributes&lt;/code&gt; in &lt;code&gt;src/Sentry/Protocol/SentryAttributes.cs&lt;/code&gt;. Before the fix it read the SDK fields straight off the object it was handed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&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="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;SetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.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="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;SetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&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;Nothing wrong on its face. If &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;Version&lt;/code&gt; are set they get written. If they are null the attributes are quietly skipped. So the real question is what &lt;code&gt;sdk&lt;/code&gt; is on the console path.&lt;/p&gt;

&lt;p&gt;Two call sites feed this method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/Sentry/SentryLog.cs:153&lt;/span&gt;
&lt;span class="n"&gt;sdk&lt;/span&gt; &lt;span class="p"&gt;??=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Sdk&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// src/Sentry/SentryMetric.Factory.cs:23&lt;/span&gt;
&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetDefaultAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Sdk&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both look defensive. Both say "use the scope's Sdk, otherwise fall back to &lt;code&gt;SdkVersion.Instance&lt;/code&gt;". &lt;code&gt;SdkVersion.Instance&lt;/code&gt; is the populated object, with &lt;code&gt;Name = "sentry.dotnet"&lt;/code&gt; and the assembly version. So why does the fallback never help?&lt;/p&gt;

&lt;p&gt;Here is the line that decides it, in &lt;code&gt;src/Sentry/Scope.cs:277&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;SdkVersion&lt;/span&gt; &lt;span class="n"&gt;Sdk&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&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;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;   &lt;span class="c1"&gt;// Name = null, Version = null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Scope.Sdk&lt;/code&gt; is auto-initialized to &lt;code&gt;new SdkVersion()&lt;/code&gt;. That object exists, its &lt;code&gt;Name&lt;/code&gt; and &lt;code&gt;Version&lt;/code&gt; are just null. Because the property is never null, &lt;code&gt;scope?.Sdk ?? SdkVersion.Instance&lt;/code&gt; always resolves to &lt;code&gt;scope.Sdk&lt;/code&gt; and never reaches &lt;code&gt;SdkVersion.Instance&lt;/code&gt;. The &lt;code&gt;??&lt;/code&gt; only fires when &lt;code&gt;scope&lt;/code&gt; itself is null. The fallback everyone wrote was dead code.&lt;/p&gt;

&lt;p&gt;That empty object flows into &lt;code&gt;SetDefaultAttributes&lt;/code&gt;, both guards see null, so both attributes drop out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ASP.NET Core escaped
&lt;/h2&gt;

&lt;p&gt;The framework integrations fill the scope's Sdk. ASP.NET Core does it in its middleware (&lt;code&gt;src/Sentry.AspNetCore/SentryMiddleware.cs:257-258&lt;/code&gt;), setting &lt;code&gt;scope.Sdk.Name&lt;/code&gt; and &lt;code&gt;scope.Sdk.Version&lt;/code&gt;. Once those are non-null the guards pass and the attributes appear. The core &lt;code&gt;Enricher&lt;/code&gt; populates the Sdk on events and transactions, never on the scope and never on the logs or metrics path. So a plain console app, with no integration touching &lt;code&gt;scope.Sdk&lt;/code&gt;, leaves it empty. That is the difference between the two app types, all the way down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One change, at the single point both logs and metrics share. Fall back per field to the populated instance when the incoming field is null:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Fall back to the populated SDK instance when the scope's Sdk was not filled in by a framework&lt;/span&gt;
&lt;span class="c1"&gt;// integration, e.g. in console apps, so logs and metrics still carry the SDK name and version (see #5352).&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;sdk&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="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&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="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&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="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;SetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.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="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;SetAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&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;A few things make this the right shape. It fixes logs and metrics in one edit because both funnel through here, so the two call sites cannot drift apart. It does not override anyone: when ASP.NET Core has set &lt;code&gt;scope.Sdk.Name&lt;/code&gt;, that value is non-null so the &lt;code&gt;??&lt;/code&gt; short circuits and the integration still wins. &lt;code&gt;SdkVersion.Instance&lt;/code&gt; is the same object the envelope header already uses, so logs and metrics now agree with the envelope instead of contradicting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test first, watch it fail, then pass
&lt;/h2&gt;

&lt;p&gt;Two new tests reproduce the console path. The log one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Fact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SetDefaultAttributes_EmptyScopeSdk_UsesSdkInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SentryOptions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SentryLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TraceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SentryLogLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// A console app does not populate scope.Sdk, so its Name and Version stay null.&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetDefaultAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Should&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;NotBeNullOrWhiteSpace&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sentry.dotnet"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attributes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ShouldContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sentry.sdk.version"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SdkVersion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&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;The metric one mirrors it with &lt;code&gt;new SdkVersion()&lt;/code&gt;, which is what the metric path receives in a console app. Two existing serialization tests were pinning the buggy payload (no SDK attributes), so they are corrected to include the name and version.&lt;/p&gt;

&lt;p&gt;Revert only the source file and keep the tests: the suite reports &lt;code&gt;Failed: 4, Passed: 2529&lt;/code&gt;. The two new tests fail on the missing &lt;code&gt;sentry.sdk.name&lt;/code&gt;, plus the two corrected serialization tests. Put the one-line fix back and it is &lt;code&gt;Failed: 0, Passed: 2533, Skipped: 5, Total: 2538&lt;/code&gt; on net10.0. &lt;code&gt;dotnet format --verify-no-changes&lt;/code&gt; is clean on the changed files. The pre-existing &lt;code&gt;Protocol_Default_VerifyAttributes&lt;/code&gt; tests never caught the bug because they pre-populate the Sdk, so they never touch the empty case.&lt;/p&gt;

&lt;h2&gt;
  
  
  The change
&lt;/h2&gt;

&lt;p&gt;This is a fix inside the Sentry .NET SDK itself, not an app that uses it. Issue &lt;a href="https://github.com/getsentry/sentry-dotnet/issues/5352" rel="noopener noreferrer"&gt;#5352&lt;/a&gt;, pull request &lt;a href="https://github.com/getsentry/sentry-dotnet/pull/5483" rel="noopener noreferrer"&gt;getsentry/sentry-dotnet#5483&lt;/a&gt;, branch &lt;a href="https://github.com/zkasuran/sentry-dotnet/tree/fix/sdkversion-empty-for-console" rel="noopener noreferrer"&gt;&lt;code&gt;fix/sdkversion-empty-for-console&lt;/code&gt;&lt;/a&gt; at commit &lt;code&gt;36a0d63&lt;/code&gt;. One line of source in &lt;code&gt;SentryAttributes.cs&lt;/code&gt; plus the regression tests. A dead &lt;code&gt;??&lt;/code&gt; fallback that everyone assumed was covering the empty case, sitting one property away from the fix.&lt;/p&gt;

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

&lt;p&gt;This entry is in two Bug Smash categories. Clear the Lineup covers the fix itself. Best Use of Sentry fits because the fix lives inside Sentry's own SDK. &lt;code&gt;sentry.sdk.name&lt;/code&gt; and &lt;code&gt;sentry.sdk.version&lt;/code&gt; are the attribution layer of the telemetry pipeline. They are how the platform knows a log or a metric came from &lt;code&gt;sentry.dotnet&lt;/code&gt; and which version produced it, which is what lets Sentry group data by SDK, spot version-specific regressions and route an issue to the right maintainers. When a console app drops them, its logs and metrics arrive unattributed. The ASP.NET Core path silently disagreeing with it made the gap easy to miss. Restoring the fields at the one method both paths share means every log and every metric now carries the same SDK identity the envelope header already sends, so the data a team relies on to debug production lines up with itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used to trace the root cause, write the fix and the tests, then run the test suite. I own the change, reviewed it and verified it locally before submitting. Verified: the full &lt;code&gt;Sentry.Tests&lt;/code&gt; suite on net10.0 (2533 passing, 0 failing); the bug reproduced by reverting only the source file (4 failing); &lt;code&gt;dotnet format --verify-no-changes&lt;/code&gt; clean on the changed files.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>dotnet</category>
      <category>opensource</category>
    </item>
    <item>
      <title>The .git substring that broke but pr new for GitHub Pages repos</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Thu, 13 Aug 2026 01:32:50 +0000</pubDate>
      <link>https://dev.to/zkasuran/the-git-substring-that-broke-but-pr-new-for-github-pages-repos-4gbd</link>
      <guid>https://dev.to/zkasuran/the-git-substring-that-broke-but-pr-new-for-github-pages-repos-4gbd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that only bites &lt;code&gt;.github.io&lt;/code&gt; repos
&lt;/h2&gt;

&lt;p&gt;Someone opened issue &lt;a href="https://github.com/gitbutlerapp/gitbutler/issues/15302" rel="noopener noreferrer"&gt;#15302&lt;/a&gt; on GitButler with a clean report: running &lt;code&gt;but pr new&lt;/code&gt; in their repo failed with HTTP 404 and the message "Failed to list open pull requests". Their remote was a GitHub Pages repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git@github.com:bburns-ds/bburns-test.github.io.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a completely ordinary clone URL. Nothing exotic. So why would GitButler be unable to list pull requests for a real repository the user clones every day?&lt;/p&gt;

&lt;h2&gt;
  
  
  Following the 404
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;but pr new&lt;/code&gt; has to turn a remote URL into an &lt;code&gt;(owner, repo)&lt;/code&gt; pair before it can call the GitHub API. I traced the path through the crates:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;but pr new&lt;/code&gt; calls &lt;code&gt;but_forge::derive_forge_repo_info(&amp;amp;remote_url)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_forge_reviews&lt;/code&gt; pulls &lt;code&gt;owner&lt;/code&gt; and &lt;code&gt;repo&lt;/code&gt; out of that and calls &lt;code&gt;but_github&lt;/code&gt;'s pull request list.&lt;/li&gt;
&lt;li&gt;That ends at &lt;code&gt;list_open_pulls&lt;/code&gt;, which issues &lt;code&gt;GET /repos/{owner}/{repo}/pulls&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the 404 is GitHub telling us the request went to a repository that does not exist. The interesting question is what &lt;code&gt;derive_forge_repo_info&lt;/code&gt; produced for a &lt;code&gt;.github.io&lt;/code&gt; URL. I parsed the reporter's URL and got back &lt;code&gt;repo = "org"&lt;/code&gt;. Not &lt;code&gt;org.github.io&lt;/code&gt;. Just &lt;code&gt;org&lt;/code&gt;. The API call had become &lt;code&gt;GET /repos/org/org/pulls&lt;/code&gt;, which of course 404s.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;.git&lt;/code&gt; substring trap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;derive_forge_repo_info&lt;/code&gt; parses the URL with the &lt;code&gt;git-url-parse&lt;/code&gt; crate. Here is the exact code that splits the path, from &lt;code&gt;git-url-parse&lt;/code&gt; 0.6.0 (&lt;code&gt;src/types/provider/generic.rs&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="nf"&gt;.ends_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".git"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;separated_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;take_until&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".git"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;separated_pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;is_not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;is_not&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the &lt;code&gt;.ends_with(".git")&lt;/code&gt; branch closely. It reads the repo name with &lt;code&gt;take_until(".git")&lt;/code&gt;. &lt;code&gt;take_until&lt;/code&gt; stops at the &lt;strong&gt;first&lt;/strong&gt; &lt;code&gt;.git&lt;/code&gt; substring, not the terminal suffix. Most repo names never contain &lt;code&gt;.git&lt;/code&gt;, so this is invisible almost all the time. But a GitHub Pages repo is named &lt;code&gt;something.github.io&lt;/code&gt;, where the &lt;code&gt;.github&lt;/code&gt; piece contains &lt;code&gt;.git&lt;/code&gt;. So the path &lt;code&gt;org/org.github.io.git&lt;/code&gt; gets cut at the &lt;code&gt;.git&lt;/code&gt; inside &lt;code&gt;.github&lt;/code&gt; and the repo name collapses to &lt;code&gt;org&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The trap only springs when two things line up: the URL ends in the &lt;code&gt;.git&lt;/code&gt; clone suffix AND the repo name contains &lt;code&gt;.git&lt;/code&gt; earlier. Canonical GitHub clone URLs always append &lt;code&gt;.git&lt;/code&gt;, so every Pages repo hits it. I confirmed the full parse matrix in a scratch crate against &lt;code&gt;git-url-parse&lt;/code&gt; 0.6.0:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;remote URL&lt;/th&gt;
&lt;th&gt;parsed repo&lt;/th&gt;
&lt;th&gt;should be&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git@github.com:org/repo.git&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;repo&lt;/td&gt;
&lt;td&gt;repo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git@github.com:org/org.github.io&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;org.github.io&lt;/td&gt;
&lt;td&gt;org.github.io&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;git@github.com:org/org.github.io.git&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;org&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;org.github.io&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://github.com/org/org.github.io.git&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;org&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;org.github.io&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Only the rows that carry both the suffix and the substring are wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The buggy line lives inside a published dependency, so I cannot edit it. But I can stop feeding it the input that trips it. Look at the &lt;code&gt;else&lt;/code&gt; branch above: for a URL that does NOT end in &lt;code&gt;.git&lt;/code&gt;, the parser uses &lt;code&gt;is_not("/")&lt;/code&gt;, which reads the whole repo name up to the next slash and keeps &lt;code&gt;org.github.io&lt;/code&gt; intact.&lt;/p&gt;

&lt;p&gt;So the fix is to strip a single trailing &lt;code&gt;.git&lt;/code&gt; at our boundary, before parsing, in &lt;code&gt;crates/but-forge/src/lib.rs&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;derive_forge_repo_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ForgeRepoInfo&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;let&lt;/span&gt; &lt;span class="n"&gt;git_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GitUrl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;derive_forge_repo_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ForgeRepoInfo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// git-url-parse 0.6.0's GenericProvider strips the git-suffix with&lt;/span&gt;
    &lt;span class="c1"&gt;// `take_until(".git")`, which stops at the FIRST ".git" substring rather&lt;/span&gt;
    &lt;span class="c1"&gt;// than the terminal suffix. So `org/org.github.io.git` yields repo="org"&lt;/span&gt;
    &lt;span class="c1"&gt;// and every URL built from it points at the wrong repository and 404s.&lt;/span&gt;
    &lt;span class="c1"&gt;// Strip a single trailing ".git" ourselves so the parser takes its correct&lt;/span&gt;
    &lt;span class="c1"&gt;// `is_not("/")` branch. Safe because a real repo name can't end in ".git".&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="nf"&gt;.strip_suffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".git"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;git_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GitUrl&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;strip_suffix&lt;/code&gt; matters here. It removes exactly one trailing occurrence, which is the single &lt;code&gt;.git&lt;/code&gt; clone suffix GitHub appends. Using &lt;code&gt;replace(".git", "")&lt;/code&gt; would delete the &lt;code&gt;.git&lt;/code&gt; inside &lt;code&gt;.github&lt;/code&gt; and recreate the bug. It is safe to strip because GitHub does not allow a repo name to end in &lt;code&gt;.git&lt;/code&gt;, so a trailing &lt;code&gt;.git&lt;/code&gt; is always the clone suffix. SSH and HTTPS URLs are handled the same way because the strip runs on the raw string. The same &lt;code&gt;(owner, repo)&lt;/code&gt; also feeds the web base URL and the commit, PR and compare links, so those are fixed too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test, failing then passing
&lt;/h2&gt;

&lt;p&gt;I added a table-driven regression test, &lt;code&gt;repo_name_containing_dotgit_is_parsed_correctly&lt;/code&gt;, that asserts forge, owner and repo for nine URL cases: SSH and HTTPS, each plain and with &lt;code&gt;.git&lt;/code&gt;, plus the &lt;code&gt;*.github.io&lt;/code&gt; family with and without the suffix.&lt;/p&gt;

&lt;p&gt;With the fix reverted and the test kept, the suite fails on exactly the reported case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;---- tests::repo_name_containing_dotgit_is_parsed_correctly stdout ----
assertion `left == right` failed: repo for git@github.com:org/org.github.io.git
  left: "org"
 right: "org.github.io"

&lt;/span&gt;&lt;span class="gp"&gt;test result: FAILED. 103 passed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;1 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the one-line fix in place, everything is green:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;test tests::repo_name_containing_dotgit_is_parsed_correctly ... ok
&lt;/span&gt;&lt;span class="gp"&gt;test result: ok. 104 passed;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;0 failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That before/after pair is the proof: the test catches the real defect and the single line is what resolves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cargo test -p but-forge&lt;/code&gt; with the fix: 104 passed, 0 failed.&lt;/li&gt;
&lt;li&gt;Same run with the fix removed: 103 passed, 1 failed on the target case.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cargo clippy -p but-forge --all-targets&lt;/code&gt;: 0 warnings.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cargo fmt -- --check&lt;/code&gt;: clean.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pull request: &lt;a href="https://github.com/gitbutlerapp/gitbutler/pull/15311" rel="noopener noreferrer"&gt;gitbutlerapp/gitbutler#15311&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue: &lt;a href="https://github.com/gitbutlerapp/gitbutler/issues/15302" rel="noopener noreferrer"&gt;gitbutlerapp/gitbutler#15302&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Branch &lt;a href="https://github.com/zkasuran/gitbutler/tree/fix/pr-repo-name-containing-dotgit" rel="noopener noreferrer"&gt;&lt;code&gt;fix/pr-repo-name-containing-dotgit&lt;/code&gt;&lt;/a&gt; on my fork, commit &lt;a href="https://github.com/zkasuran/gitbutler/commit/c34cf75b3ec00181d7dd0623db74b500e6c0fb77" rel="noopener noreferrer"&gt;&lt;code&gt;c34cf75&lt;/code&gt;&lt;/a&gt;. One line changed in &lt;code&gt;crates/but-forge/src/lib.rs&lt;/code&gt; plus the regression test.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A one-line fix, but only after the parser's first &lt;code&gt;.git&lt;/code&gt; cut gave up its secret. The best bugs hide inside a string almost nobody thinks to name with a dot-git in the middle.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used while working on this fix. I did the diagnosis, the design, the review and the verification. I own the change. What was verified locally before submitting: &lt;code&gt;cargo test -p but-forge&lt;/code&gt; (104 passed, plus the new test fails 1 of 104 with the fix reverted), &lt;code&gt;cargo clippy -p but-forge --all-targets&lt;/code&gt; (0 warnings) and &lt;code&gt;cargo fmt -- --check&lt;/code&gt; (clean).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>rust</category>
      <category>opensource</category>
    </item>
    <item>
      <title>A single failed flush was silently killing telemetry in the Sentry Python SDK</title>
      <dc:creator>Asuran</dc:creator>
      <pubDate>Wed, 12 Aug 2026 08:45:27 +0000</pubDate>
      <link>https://dev.to/zkasuran/a-single-failed-flush-was-silently-killing-telemetry-in-the-sentry-python-sdk-4bgo</link>
      <guid>https://dev.to/zkasuran/a-single-failed-flush-was-silently-killing-telemetry-in-the-sentry-python-sdk-4bgo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;This entry is a real, tested bug fix in the &lt;a href="https://github.com/getsentry/sentry-python" rel="noopener noreferrer"&gt;Sentry Python SDK&lt;/a&gt; for issue &lt;a href="https://github.com/getsentry/sentry-python/issues/7138" rel="noopener noreferrer"&gt;#7138&lt;/a&gt;. The SDK batches logs, metrics and spans and ships them from a background daemon thread. If one flush ever raised, that thread died. From then on the process kept buffering telemetry that was never sent and eventually dropped. No error reached the application. The fix makes the flush loop tolerate a failed batch and keep running.&lt;/p&gt;

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

&lt;p&gt;Sentry's SDK does not send every log or span the moment you create it. It buffers them in a &lt;code&gt;Batcher&lt;/code&gt; and a background daemon thread drains the buffer on a timer. That thread runs a simple loop: wait, then flush.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_flush_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_flush_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLUSH_WAIT_TIME&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_flush_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# nothing catches an exception here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_flush()&lt;/code&gt; serializes the buffered items and hands them to the transport. If any part of that raised, the exception propagated straight out of &lt;code&gt;_flush_loop&lt;/code&gt;. Because this is the thread's entry point, the thread ended. Python does not restart it.&lt;/p&gt;

&lt;p&gt;The quiet part is what happens next. &lt;code&gt;add()&lt;/code&gt; decides whether to start the flusher by checking &lt;code&gt;_flusher_pid == os.getpid()&lt;/code&gt;. After the thread has died that check still passes, because the pid never changed, so &lt;code&gt;add()&lt;/code&gt; happily keeps appending to a buffer that nothing drains. Items pile up until the buffer hits its hard cap (&lt;code&gt;MAX_BEFORE_DROP&lt;/code&gt;, 1000 for logs). Every item after that is dropped through &lt;code&gt;_record_lost&lt;/code&gt;. So a single transient error inside one flush turns into permanent, silent telemetry loss for the rest of the process lifetime. On a long-lived web worker the whole point of running the SDK is gone, with nothing in the logs to say why.&lt;/p&gt;

&lt;p&gt;The issue was filed with a concrete trigger: a &lt;code&gt;RuntimeError: _core::PyFeatureContext is unsendable ...&lt;/code&gt; raised while serializing a value during a flush. But the class of the bug is broader than any one trigger. The batcher sits on the SDK's own hot path and must not let one bad item take down the sender.&lt;/p&gt;

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

&lt;p&gt;The fix follows the issue title exactly: tolerate exceptions in the flush loop. Each loop wraps its flush in &lt;code&gt;capture_internal_exceptions()&lt;/code&gt;, the SDK's own context manager for errors that are the SDK's own fault and should be logged rather than thrown at the user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;sentry_sdk.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;capture_internal_exceptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_timestamp&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_flush_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_flush_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FLUSH_WAIT_TIME&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_flush_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;capture_internal_exceptions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_flush&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same guard goes on &lt;code&gt;SpanBatcher._flush_loop&lt;/code&gt;, which has two flush calls (the pending-buckets flush and the periodic full flush), so all three batchers are covered: logs and metrics through the base &lt;code&gt;Batcher&lt;/code&gt;, spans through the &lt;code&gt;SpanBatcher&lt;/code&gt; override. A failed batch is now swallowed and logged. The loop stays alive and the next flush delivers everything that queued up in the meantime.&lt;/p&gt;

&lt;p&gt;PR: &lt;a href="https://github.com/getsentry/sentry-python/pull/7186" rel="noopener noreferrer"&gt;getsentry/sentry-python#7186&lt;/a&gt;, from branch &lt;a href="https://github.com/zkasuran/sentry-python/tree/fix/tolerate-exceptions-in-flush-loop" rel="noopener noreferrer"&gt;&lt;code&gt;fix/tolerate-exceptions-in-flush-loop&lt;/code&gt;&lt;/a&gt; on my fork. sentry-python's bot auto-closes PRs opened before a maintainer agrees on the issue, so the change was staked on the issue thread first. Once a maintainer confirmed the direction there, the branch was rebased onto current master and the PR opened.&lt;/p&gt;

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

&lt;p&gt;The diff is small and on point: four files, +94 / -15, with the real source change being the two &lt;code&gt;with capture_internal_exceptions():&lt;/code&gt; guards. The rest is comments and tests.&lt;/p&gt;

&lt;p&gt;I added one regression test per loop, &lt;code&gt;test_flush_loop_swallows_flush_exception&lt;/code&gt;, in &lt;code&gt;tests/test_logs.py&lt;/code&gt; (base &lt;code&gt;Batcher&lt;/code&gt;) and &lt;code&gt;tests/tracing/test_span_batcher.py&lt;/code&gt; (&lt;code&gt;SpanBatcher&lt;/code&gt;). Each builds a batcher whose &lt;code&gt;_flush&lt;/code&gt; raises once and then stops the loop, calls &lt;code&gt;_flush_loop()&lt;/code&gt; directly on the main thread and asserts it returns instead of raising. Run against the unfixed source both fail with &lt;code&gt;RuntimeError: boom in flush&lt;/code&gt;, which is exactly the propagation that killed the real thread. With the fix both pass. They carry the repo's own &lt;code&gt;@pytest.mark.tests_internal_exceptions&lt;/code&gt; marker, the suite's opt-in for tests that exercise &lt;code&gt;capture_internal_exceptions&lt;/code&gt;, which is a nice confirmation that the fix routes through the SDK's standard internal-error handling rather than a bespoke &lt;code&gt;try/except&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Verification, all green: 73 passed across the logs, metrics and span-batcher suites, &lt;code&gt;ruff check&lt;/code&gt; and &lt;code&gt;ruff format --check&lt;/code&gt; clean on the changed files, &lt;code&gt;mypy sentry_sdk&lt;/code&gt; shows no new errors versus the base (its 28 pre-existing errors are all in an unrelated integration, none in the batcher files).&lt;/p&gt;

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

&lt;p&gt;This is a fix to Sentry's own delivery pipeline, the part every log, metric and span passes through on its way out. The failure mode it removes is the worst kind for an observability tool: the SDK stops reporting and never says so, so the first you know is a gap in your dashboards during an incident. Making the flush loop survive a single bad batch means the SDK degrades to "log the internal error and keep going" instead of "go silent forever," which is the behavior you want from the thing you rely on to tell you when everything else is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI disclosure
&lt;/h2&gt;

&lt;p&gt;AI assistance (Claude, Anthropic) was used in developing this change. The design, review and verification were done by the author. Verified locally before submitting: the two new regression tests fail on the unpatched source and pass with the fix, the logs, metrics and span-batcher suites pass (73 tests), &lt;code&gt;ruff check&lt;/code&gt; and &lt;code&gt;ruff format --check&lt;/code&gt; are clean on the changed files, &lt;code&gt;mypy sentry_sdk&lt;/code&gt; shows no new errors versus the base.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
