<?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: Session Replay</title>
    <description>The latest articles on DEV Community by Session Replay (session_replay).</description>
    <link>https://dev.to/session_replay</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%2Forganization%2Fprofile_image%2F14592%2F1d0ec0c8-ca04-4954-aefc-2cded833cb47.png</url>
      <title>DEV Community: Session Replay</title>
      <link>https://dev.to/session_replay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/session_replay"/>
    <language>en</language>
    <item>
      <title>Your package.json Says What You Asked For. Only the Lockfile Says What You Got.</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:58:45 +0000</pubDate>
      <link>https://dev.to/session_replay/your-packagejson-says-what-you-asked-for-only-the-lockfile-says-what-you-got-3728</link>
      <guid>https://dev.to/session_replay/your-packagejson-says-what-you-asked-for-only-the-lockfile-says-what-you-got-3728</guid>
      <description>&lt;p&gt;Ninety-seven malware advisories were published to the GitHub Advisory Database yesterday: 94 npm packages and 3 PyPI. I pulled them with &lt;code&gt;type=malware&lt;/code&gt; and read the names, and they split into two very different campaigns.&lt;/p&gt;

&lt;p&gt;Most were forks of one popular library republished under scopes nobody has heard of. Ten were something else: names shaped like somebody's internal packages rather than like public ones. &lt;code&gt;@tesla-insurance/vinless-quote&lt;/code&gt;. &lt;code&gt;@airbnb-extended/typescript-config&lt;/code&gt;. &lt;code&gt;app-sca-info-banking&lt;/code&gt;. &lt;code&gt;unified-platform&lt;/code&gt;. &lt;code&gt;risk-detection&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That second group is dependency confusion, and the usual explanation of it stops one step before the part that decides whether you are exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The explanation everybody repeats
&lt;/h2&gt;

&lt;p&gt;An attacker publishes a package to the public registry using the name of one of your private, internal packages. Your install picks up theirs instead of yours.&lt;/p&gt;

&lt;p&gt;True, and it leaves out the mechanism, which is the only part you can act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually decides it
&lt;/h2&gt;

&lt;p&gt;npm's own documentation is plain about this. A scope can be bound to a registry, in &lt;code&gt;.npmrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;@myco:&lt;/span&gt;&lt;span class="py"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://reg.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then, in their words, "once a scope is associated with a registry, any &lt;code&gt;npm install&lt;/code&gt; for a package with that scope will request packages from that registry instead." They also note that "scopes have a many-to-one relationship with registries: one registry can host multiple scopes, but a scope only ever points to one registry."&lt;/p&gt;

&lt;p&gt;Without that line, there is no association to honour, and the request goes to the default registry like any other.&lt;/p&gt;

&lt;p&gt;So exposure here is a &lt;strong&gt;configuration&lt;/strong&gt; property, not a property of your dependencies. It is decided by a file most people have not opened this year, and it is the same answer for every package in that scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which is why the reflex fix does not work
&lt;/h2&gt;

&lt;p&gt;Ask a room what stops dependency confusion and somebody will say pin your versions. Exact versions, &lt;code&gt;save-exact&lt;/code&gt;, no carets anywhere.&lt;/p&gt;

&lt;p&gt;Pinning constrains &lt;strong&gt;which version&lt;/strong&gt;. It says nothing about &lt;strong&gt;which registry answered&lt;/strong&gt;. If the request is going to the public registry and an attacker has published your package name there, &lt;code&gt;1.2.3&lt;/code&gt; matches their &lt;code&gt;1.2.3&lt;/code&gt; exactly as happily as it would have matched yours. A pin is a defence against an upgrade you did not want, which is a different attack.&lt;/p&gt;

&lt;p&gt;The lockfile is genuinely better, and still not the control you want. npm says it exists so that "subsequent installs are able to generate identical trees, regardless of intermediate dependency updates". Identical to what was resolved before. It protects what is already in it. A dependency added this afternoon resolves fresh, against whatever registry is configured now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field that answers the question
&lt;/h2&gt;

&lt;p&gt;There is exactly one place in a normal project that records where a package came from, and it is not &lt;code&gt;package.json&lt;/code&gt;. It is the &lt;code&gt;resolved&lt;/code&gt; field in the lockfile, which npm documents as "the place where the package was actually resolved from. In the case of packages fetched from the registry, this will be a url to a tarball."&lt;/p&gt;

&lt;p&gt;Which makes the audit a one-liner. Count the hosts your tree actually came from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.. | .resolved? // empty'&lt;/span&gt; package-lock.json &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;/ &lt;span class="s1"&gt;'{print $3}'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;uniq&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a healthy private-registry project that prints your internal host and &lt;code&gt;registry.npmjs.org&lt;/code&gt;, and nothing else. Anything third is worth ten minutes. And if a scope you believe is internal shows up under the public host, you have found the thing before somebody else did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that generalises past npm
&lt;/h2&gt;

&lt;p&gt;Look at what was true the whole time this was going wrong. The manifest was correct: the package name in it is the one you meant. The version was correct. Code review passed, because the diff showed a dependency you recognised. Every artefact a person would naturally read says the right thing.&lt;/p&gt;

&lt;p&gt;The only record of what actually happened lives in a generated file that nobody opens, reviews, or reads in a pull request, and that most teams have at some point added to &lt;code&gt;.gitignore&lt;/code&gt; by accident.&lt;/p&gt;

&lt;p&gt;That shape is worth recognising, because it is not rare. When somebody tells you their build started failing or behaving strangely after an install, the question is never what was requested. It is what was fetched, at that moment, on that machine. Those are different claims, and only one of them is written down.&lt;/p&gt;

</description>
      <category>npm</category>
      <category>security</category>
      <category>node</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The Browser Closed Your Paragraph Early, and Your CSS Stopped Applying</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Mon, 12 Oct 2026 13:00:43 +0000</pubDate>
      <link>https://dev.to/session_replay/the-browser-closed-your-paragraph-early-and-your-css-stopped-applying-1phi</link>
      <guid>https://dev.to/session_replay/the-browser-closed-your-paragraph-early-and-your-css-stopped-applying-1phi</guid>
      <description>&lt;p&gt;Write this, then read back what the browser actually built:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;before &lt;span class="nt"&gt;&amp;lt;figure&amp;gt;&lt;/span&gt;FIG&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt; after&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I parsed it in Chrome this morning. What comes back is not that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;before &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&amp;lt;figure&amp;gt;&lt;/span&gt;FIG&lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt; after&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four things happened that nobody asked for. The paragraph closed before the figure. The figure became a sibling rather than a child. The word &lt;code&gt;after&lt;/code&gt; is now a bare text node hanging directly off &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, in no paragraph at all. And the &lt;code&gt;&amp;lt;/p&amp;gt;&lt;/code&gt; you wrote arrived too late to close anything, so it opened and closed a new empty one.&lt;/p&gt;

&lt;p&gt;Here is the measurement, which you can run in any console:&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;parse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;html&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;doc&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;DOMParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;parseFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;!doctype html&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/html&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="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paragraphs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;before &amp;lt;figure&amp;gt;FIG&amp;lt;/figure&amp;gt; after&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { html: '&amp;lt;p&amp;gt;before &amp;lt;/p&amp;gt;&amp;lt;figure&amp;gt;FIG&amp;lt;/figure&amp;gt; after&amp;lt;p&amp;gt;&amp;lt;/p&amp;gt;', paragraphs: 2 }&lt;/span&gt;
&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;before &amp;lt;span&amp;gt;SPAN&amp;lt;/span&amp;gt; after&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// { html: '&amp;lt;p&amp;gt;before &amp;lt;span&amp;gt;SPAN&amp;lt;/span&amp;gt; after&amp;lt;/p&amp;gt;', paragraphs: 1 }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;div&lt;/code&gt; and &lt;code&gt;ul&lt;/code&gt; behave exactly like &lt;code&gt;figure&lt;/code&gt;. &lt;code&gt;span&lt;/code&gt; does not, and the difference is the whole mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is specified behaviour, not a quirk
&lt;/h2&gt;

&lt;p&gt;The HTML Standard says a &lt;code&gt;p&lt;/code&gt; element's end tag can be omitted&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;if the &lt;code&gt;p&lt;/code&gt; element is immediately followed by an &lt;code&gt;address&lt;/code&gt;, &lt;code&gt;article&lt;/code&gt;, &lt;code&gt;aside&lt;/code&gt;, &lt;code&gt;blockquote&lt;/code&gt;, &lt;code&gt;details&lt;/code&gt;, &lt;code&gt;dialog&lt;/code&gt;, &lt;code&gt;div&lt;/code&gt;, &lt;code&gt;dl&lt;/code&gt;, &lt;code&gt;fieldset&lt;/code&gt;, &lt;code&gt;figcaption&lt;/code&gt;, &lt;code&gt;figure&lt;/code&gt;, &lt;code&gt;footer&lt;/code&gt;, &lt;code&gt;form&lt;/code&gt;, &lt;code&gt;h1&lt;/code&gt;, &lt;code&gt;h2&lt;/code&gt;, &lt;code&gt;h3&lt;/code&gt;, &lt;code&gt;h4&lt;/code&gt;, &lt;code&gt;h5&lt;/code&gt;, &lt;code&gt;h6&lt;/code&gt;, &lt;code&gt;header&lt;/code&gt;, &lt;code&gt;hgroup&lt;/code&gt;, &lt;code&gt;hr&lt;/code&gt;, &lt;code&gt;main&lt;/code&gt;, &lt;code&gt;menu&lt;/code&gt;, &lt;code&gt;nav&lt;/code&gt;, &lt;code&gt;ol&lt;/code&gt;, &lt;code&gt;p&lt;/code&gt;, &lt;code&gt;pre&lt;/code&gt;, &lt;code&gt;search&lt;/code&gt;, &lt;code&gt;section&lt;/code&gt;, &lt;code&gt;table&lt;/code&gt;, or &lt;code&gt;ul&lt;/code&gt; element&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Can be omitted" is the author-facing half of that sentence, and it reads like a convenience. The parser-facing half is that the tag &lt;strong&gt;is&lt;/strong&gt; omitted whether you wrote it or not. Because the standard requires it, this is not a rendering difference you can test your way around in another browser. I measured Chrome; any conformant parser is obliged to build the same tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs
&lt;/h2&gt;

&lt;p&gt;The orphaned text is the part that bites. Every rule you wrote for &lt;code&gt;p&lt;/code&gt; stops applying to it: &lt;code&gt;line-height&lt;/code&gt;, &lt;code&gt;max-width&lt;/code&gt;, &lt;code&gt;margin&lt;/code&gt;, &lt;code&gt;font-size&lt;/code&gt;, the lot. Half your sentence inherits from &lt;code&gt;body&lt;/code&gt; and the half above it does not, so the spacing changes partway through what the reader sees as one paragraph. The empty &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; adds a margin on top of that, and it is not in your source to find.&lt;/p&gt;

&lt;p&gt;That is a strange defect to receive. It is visible, it is clearly a layout fault, and it is confined to one page and one place on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it comes from, since nobody types this
&lt;/h2&gt;

&lt;p&gt;Three routes, and the author is not wrong in any of them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A template or component wrapping a body in &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;, where that body can contain a figure, a table or a code block.&lt;/li&gt;
&lt;li&gt;A markdown or CMS pipeline that renders an image, an embed or an attachment as a &lt;code&gt;&amp;lt;figure&amp;gt;&lt;/code&gt; and lets it land mid-paragraph. I hit this last week in a publishing pipeline, which refused the file rather than render it, and that refusal is the only reason I looked.&lt;/li&gt;
&lt;li&gt;Rich text pasted into a field whose output gets wrapped in &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; on the way out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part you cannot see from the source
&lt;/h2&gt;

&lt;p&gt;Here is the uncomfortable bit, and the reason this is worth knowing rather than just fixing. The defect does not exist in your template, your markdown, or your CMS record. All three are exactly what you intended, and re-reading them proves nothing. It exists only in the tree the parser built.&lt;/p&gt;

&lt;p&gt;So the report arrives as "the spacing looks wrong on that page", from somebody who cannot tell you which half of which paragraph, and you cannot reproduce it by reading the file that produced it. Opening the page yourself may not help either, because the condition is in the content: it needs the paragraph that happened to contain a figure, which is one article out of two hundred.&lt;/p&gt;

&lt;p&gt;The only artefact that shows the fault is the DOM as it was built at the time, on the page the reader had open. That is never what gets sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  A cheap assertion
&lt;/h2&gt;

&lt;p&gt;Both of these should be zero on a content page, and neither is on a page with this defect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Point this at whatever wraps your rendered content, not at body:&lt;/span&gt;
&lt;span class="c1"&gt;// on most pages the prose lives inside &amp;lt;article&amp;gt; or &amp;lt;main&amp;gt;.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;root&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;article, main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;body&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;orphans&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;childNodes&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;n&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;nodeType&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEXT_NODE&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;empties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;root&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="s1"&gt;p&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;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orphans&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;empties&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cheap enough to assert in whatever test already renders your templates, which turns a layout report nobody can reproduce into a failing build.&lt;/p&gt;

&lt;p&gt;If you take one thing from this: a correct source file and a correct DOM are two different claims. The parser sits between them, and it is allowed to disagree with you.&lt;/p&gt;

</description>
      <category>html</category>
      <category>css</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>I Tested the "unload Blocks bfcache" Rule and My Page Was Restored Anyway</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Fri, 09 Oct 2026 12:59:30 +0000</pubDate>
      <link>https://dev.to/session_replay/i-tested-the-unload-blocks-bfcache-rule-and-my-page-was-restored-anyway-1572</link>
      <guid>https://dev.to/session_replay/i-tested-the-unload-blocks-bfcache-rule-and-my-page-was-restored-anyway-1572</guid>
      <description>&lt;p&gt;Microsoft's Edge release notes on 8 October carried an announcement about the &lt;code&gt;unload&lt;/code&gt; event: as of Edge 155 the rollout reaches 100% of page loads, and handlers no longer run unless a site opts in through Permissions Policy. The note adds the standard reasoning, that &lt;code&gt;unload&lt;/code&gt; "can prevent pages from using the back/forward cache".&lt;/p&gt;

&lt;p&gt;Which is the version of this everybody repeats, usually without the "can": register an &lt;code&gt;unload&lt;/code&gt; handler and your page is ineligible for bfcache.&lt;/p&gt;

&lt;p&gt;I tested it. The page was restored from bfcache anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;On a page in Chrome 154, register the handler and a marker, navigate away, navigate back:&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__log&lt;/span&gt; &lt;span class="o"&gt;=&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;pageshow&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="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__log&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="s2"&gt;`persisted=&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;persisted&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="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;unload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{});&lt;/span&gt;
&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1zw4n1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After going forward to another page and then back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"marker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1zw4n1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"pageshowLog"&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="s2"&gt;"pageshow persisted=true"&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;&lt;code&gt;persisted=true&lt;/code&gt; is the browser telling you directly that this was a back/forward cache restore, and the surviving marker says the same thing a second way: the JavaScript context was never torn down.&lt;/p&gt;

&lt;p&gt;So on that browser, on that page, an &lt;code&gt;unload&lt;/code&gt; listener did not prevent the restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am not claiming
&lt;/h2&gt;

&lt;p&gt;One measurement, one browser version, one page, with the handler attached at runtime rather than present in the original source. That is not a new rule, and if you take "unload no longer blocks bfcache" away from this you have swapped one piece of folklore for another.&lt;/p&gt;

&lt;p&gt;The eligibility rules differ by engine, by platform, by whether the handler was there at parse time, and now by whether the deprecation rollout has reached that page at all. That last one is new and it is the interesting part: if the handler is never going to fire, there is less reason for it to disqualify anything.&lt;/p&gt;

&lt;p&gt;The conclusion is not a rule. It is that this is a question you should stop answering from memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Because the browser will just tell you
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PerformanceNavigationTiming&lt;/code&gt; has a &lt;code&gt;notRestoredReasons&lt;/code&gt; property, and it is present in Chrome 154:&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;nav&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;navigation&lt;/span&gt;&lt;span class="dl"&gt;"&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notRestoredReasons&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 &lt;code&gt;null&lt;/code&gt; on a fresh navigation and on a successful restore, and it is populated when the browser tried to restore from bfcache and could not, with the reasons and the frame they belong to. So instead of reasoning about which of your handlers might be disqualifying the page, you ask the page.&lt;/p&gt;

&lt;p&gt;Log it once and look at real navigations:&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;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;pageshow&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nav&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;navigation&lt;/span&gt;&lt;span class="dl"&gt;"&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="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;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;persisted&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;notRestoredReasons&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/metrics/bfcache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nav&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;notRestoredReasons&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;Where it is missing you learn nothing, which is honest, and better than a confident wrong answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that costs you something
&lt;/h2&gt;

&lt;p&gt;Cache eligibility is the small half. The large half is that the same uncertainty governs &lt;strong&gt;whether your exit-time reporting runs at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If your analytics or your error reporter batches events and flushes them in an &lt;code&gt;unload&lt;/code&gt; handler, then on Edge that code has been running for a shrinking share of page loads since 152 and, from 155, for none of them. No error. No console warning. No failed request. The handler simply is not called.&lt;/p&gt;

&lt;p&gt;And the events you lose are a specific set: the last error before somebody left, the queued batch, the "abandoned at this step" signal. The ones about leaving.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;pagehide&lt;/code&gt; and &lt;code&gt;visibilitychange&lt;/code&gt;, and &lt;code&gt;sendBeacon&lt;/code&gt; rather than &lt;code&gt;fetch&lt;/code&gt;, because a beacon is specified to outlive the page. But the migration is not the lesson. The lesson is that &lt;strong&gt;you cannot tell from the client whether it worked.&lt;/strong&gt; A handler that never runs looks exactly like a handler that ran and had nothing to send, and from inside the page both are silence.&lt;/p&gt;

&lt;p&gt;So verify it where the evidence survives: count arrivals on the server, grouped by browser major version. If one browser's exit beacons per session fall while its page views do not, you have found it. That number is the only thing in this whole area that cannot be argued with, and it is the one almost nobody plots.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;A staged deprecation does not announce itself in your logs. It shows up as a slope in a number you are probably not watching, and the thing it takes away is evidence, which is the one category of loss that hides its own size.&lt;/p&gt;

&lt;p&gt;Ask the browser what it did. Count what arrived. Do not recite what used to be true.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Your Offline Banner Fires on a Guess, and a VPN Can Flip It</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Thu, 08 Oct 2026 12:59:50 +0000</pubDate>
      <link>https://dev.to/session_replay/your-offline-banner-fires-on-a-guess-and-a-vpn-can-flip-it-5aaj</link>
      <guid>https://dev.to/session_replay/your-offline-banner-fires-on-a-guess-and-a-vpn-can-flip-it-5aaj</guid>
      <description>&lt;p&gt;Somewhere in most front ends there is a line like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="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="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onLine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;showOfflineBanner&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MDN's page for that property contains a sentence that should probably be in a larger font:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Therefore, this property is inherently unreliable, and you should not disable features based on the online status, only provide hints when the user may seem offline.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Not "be careful with". &lt;strong&gt;Do not disable features based on it.&lt;/strong&gt; The reason is what it actually measures, which is not reachability.&lt;/p&gt;

&lt;h2&gt;
  
  
  It fails in both directions, for documented reasons
&lt;/h2&gt;

&lt;p&gt;MDN names the mechanisms, and they are worth reading as two separate bugs rather than one caveat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It says &lt;code&gt;true&lt;/code&gt; when nothing works.&lt;/strong&gt; In MDN's words, "connection to LAN is considered online, even though the LAN may not have Internet access", and "the computer may be running a virtualization software that has virtual ethernet adapters that are always 'connected'". So a developer with Docker running, or anybody on a hotel wifi that has not been paid for yet, is online as far as the property is concerned.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It says &lt;code&gt;false&lt;/code&gt; when everything works.&lt;/strong&gt; This is the one that will reach your inbox. Again from MDN: "On Windows, the online status is determined by whether it can reach a Microsoft home server, which may be blocked by firewalls or VPNs, even if the computer has Internet access."&lt;/p&gt;

&lt;p&gt;Read that again with your user base in mind. The browser decides whether &lt;em&gt;your&lt;/em&gt; app is online by whether it can reach &lt;em&gt;Microsoft&lt;/em&gt;. On a corporate network, behind a VPN that routes split-tunnel or a firewall that drops unrecognised destinations, that probe fails and the flag goes false while your API is perfectly reachable.&lt;/p&gt;

&lt;p&gt;If you disabled the save button on that flag, you disabled it for exactly the users who are on a managed laptop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second thing people reach for is also a hint
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;onLine&lt;/code&gt; disappoints, the next stop is usually &lt;code&gt;navigator.connection&lt;/code&gt;. It is worth knowing what that returns before building on it. Read from the machine I am writing this on, a desktop on wifi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"onLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effectiveType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"4g"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rtt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"downlink"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&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;&lt;code&gt;effectiveType&lt;/code&gt; is &lt;code&gt;"4g"&lt;/code&gt; on a wired-speed connection with no radio involved, because the label is a bucket for "fast", not a statement about the network type. The &lt;code&gt;rtt&lt;/code&gt; and &lt;code&gt;downlink&lt;/code&gt; figures are rounded into coarse steps on purpose, to make them less useful for fingerprinting. They are a hint about conditions, and nothing in them tells you whether your own API answers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only honest check is a request you control
&lt;/h2&gt;

&lt;p&gt;There is exactly one way to know whether your app can reach your server, which is to ask your server:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;reachable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctrl&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;AbortController&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;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&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;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&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;started&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;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HEAD&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;no-store&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&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;catch &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="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;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&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;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&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;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;started&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;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&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;cache: "no-store"&lt;/code&gt; is not decoration: without it a cached 200 will tell you the network is fine while it is down. The timeout matters because the interesting failure is not refusal, it is silence.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;navigator.onLine&lt;/code&gt; as MDN suggests, to decide whether it is worth asking at all, and never as the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  And why this ends up in a ticket you cannot close
&lt;/h2&gt;

&lt;p&gt;Here is the shape of the report. "Your app kept telling me I had no internet. My internet was fine."&lt;/p&gt;

&lt;p&gt;That is unfalsifiable, and the reason is that the flag leaves no trace. It is a boolean with no provenance: nothing records which heuristic produced it, what the browser probed, whether it was a virtual adapter or a blocked reachability check. You cannot reproduce it either, because you are not behind their VPN and your machine is not running their endpoint protection.&lt;/p&gt;

&lt;p&gt;The fix for the ticket is the same as the fix for the code. If something is going to tell a user they are offline, record the evidence it decided on: the status and timing of a request you control, not the boolean. &lt;code&gt;{"ok": false, "status": "AbortError", "ms": 3000}&lt;/code&gt; is a bug report. &lt;code&gt;navigator.onLine === false&lt;/code&gt; is an opinion.&lt;/p&gt;

&lt;p&gt;Everything above is one instance of a thing worth generalising: when a browser API gives you a single boolean for a condition that has ten causes, the boolean is not evidence about any of them.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
      <category>browsers</category>
    </item>
    <item>
      <title>Your Console Screenshot Shows Values That Never Coexisted</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:59:12 +0000</pubDate>
      <link>https://dev.to/session_replay/your-console-screenshot-shows-values-that-never-coexisted-4pk9</link>
      <guid>https://dev.to/session_replay/your-console-screenshot-shows-values-that-never-coexisted-4pk9</guid>
      <description>&lt;p&gt;Three lines, from MDN's own documentation for &lt;code&gt;console&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That logs &lt;code&gt;{}&lt;/code&gt;. Expand the object in the console afterwards and you see &lt;code&gt;prop: 123&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;MDN states the mechanism plainly: "Information about an object is lazily retrieved. This means that the log message shows the content of an object at the time when it's first viewed, not when it was logged."&lt;/p&gt;

&lt;p&gt;Not when it was logged. That is the whole article, and it has a consequence most people have never had to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common explanation is wrong
&lt;/h2&gt;

&lt;p&gt;The usual account of this is that &lt;code&gt;console.log&lt;/code&gt; is asynchronous, and that you are seeing a race. That is not what happens. The call is synchronous and the message is recorded at the moment you make it, in order, with the right timestamp.&lt;/p&gt;

&lt;p&gt;What is deferred is reading the object's contents. The console message holds a &lt;strong&gt;reference&lt;/strong&gt;, not a copy, and the contents are serialised when something asks to see them. If nothing ever expands that line, nothing ever reads it. If you expand it four minutes later, you get the object as it is four minutes later, under a line stamped four minutes ago.&lt;/p&gt;

&lt;p&gt;Primitives do not behave this way. &lt;code&gt;console.log(count)&lt;/code&gt; captures the number. It is objects, arrays, Maps and anything else held by reference where the line and its payload can come from different moments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which breaks the single most requested artefact in bug reporting
&lt;/h2&gt;

&lt;p&gt;"Send me the console" is the most common follow-up question in software, and for good reason: a red line in the console is often the whole answer.&lt;/p&gt;

&lt;p&gt;Now think about how that screenshot gets made. Something goes wrong. The user, or a colleague, opens developer tools &lt;strong&gt;after the fact&lt;/strong&gt;, finds the interesting line, clicks the arrow to expand the object, and screenshots what appears.&lt;/p&gt;

&lt;p&gt;Every expanded object in that image was read at screenshot time. The log line next to it was written earlier. If your code mutates the object it logged, and most code that logs state does, then the screenshot shows a pairing of time and value that never existed.&lt;/p&gt;

&lt;p&gt;You then debug against it. You read &lt;code&gt;status: "settled"&lt;/code&gt; against a line that fired during &lt;code&gt;pending&lt;/code&gt;, and you look for a bug that explains how a settled order got into the pending branch. There is no such bug. There is a viewer that read the object late.&lt;/p&gt;

&lt;h2&gt;
  
  
  And two reporters can hand you contradictory evidence, both honest
&lt;/h2&gt;

&lt;p&gt;The split that makes this genuinely nasty: whether the value is stale depends on whether somebody clicked the triangle.&lt;/p&gt;

&lt;p&gt;A collapsed line often shows a preview rendered at log time. Expanded, it shows the current contents. So one reporter who screenshots without expanding and one who expands first can send you two images of the same console on the same bug, disagreeing about what the object held, and neither of them has done anything wrong or touched anything.&lt;/p&gt;

&lt;p&gt;If you have ever had two tickets for the same fault that contradicted each other on a detail and closed one as unreproducible, this is a candidate for why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix in your own logging
&lt;/h2&gt;

&lt;p&gt;If the object is going to be mutated and the moment matters, clone it at the point of logging. MDN's suggestion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and it notes that &lt;code&gt;structuredClone()&lt;/code&gt; is more effective for cloning different types of object, which it is: &lt;code&gt;JSON&lt;/code&gt; round-tripping silently drops functions and &lt;code&gt;undefined&lt;/code&gt;, turns &lt;code&gt;Date&lt;/code&gt; into a string, and throws on anything circular.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;order before dispatch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;structuredClone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cost is real and worth stating. Cloning is work at log time rather than at view time, which is the wrong trade on a hot path, and it loses the things a reference gives you: a live object you can keep expanding, DOM nodes, class instances with their prototypes. So do it where the object is mutable and the timing is the point, and leave the reference where you are logging something you only ever read once.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule underneath it
&lt;/h2&gt;

&lt;p&gt;A log line's timestamp tells you when the call happened. It tells you nothing about when its payload was read.&lt;/p&gt;

&lt;p&gt;Anything that reads later is reading later, and that includes the viewer in your browser. A collector that serialises at the moment of the call gets a different answer from a viewer that serialises on expand, and the difference is not a bug in either of them. It is the difference between a snapshot and a reference, and only one of the two is evidence about a moment.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>debugging</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Your Bug Report Says Intel Mac on Catalina. The Machine Is Neither.</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:00:28 +0000</pubDate>
      <link>https://dev.to/session_replay/your-bug-report-says-intel-mac-on-catalina-the-machine-is-neither-2a6e</link>
      <guid>https://dev.to/session_replay/your-bug-report-says-intel-mac-on-catalina-the-machine-is-neither-2a6e</guid>
      <description>&lt;p&gt;Here is the User-Agent string Chrome sent from the Mac I am writing this on, read out of the browser a few minutes ago:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three of those facts are false. The machine is not Intel, it is ARM. The OS is not 10.15.7, which is Catalina and shipped in 2020; it is 26.6.2. And Chrome is not 154.0.0.0, it is 154.0.8037.93.&lt;/p&gt;

&lt;p&gt;None of that is a bug. It is the documented design. Chromium's own &lt;a href="https://www.chromium.org/updates/ua-reduction/" rel="noopener noreferrer"&gt;User-Agent reduction&lt;/a&gt; page says the macOS platform version is frozen at &lt;code&gt;10_15_7&lt;/code&gt; and the browser version is reduced to &lt;code&gt;0.0.0&lt;/code&gt;, with the reduction fully in place from Chrome 113. The macOS freeze exists because when browsers started reporting &lt;code&gt;Mac OS X 11_*&lt;/code&gt;, a long tail of sites broke on it.&lt;/p&gt;

&lt;p&gt;So the string is behaving correctly. The problem is what we do with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A measured field can be confidently wrong
&lt;/h2&gt;

&lt;p&gt;Most advice about environment fields in bug reports is about getting them measured instead of typed, because a person recalling their OS version is guessing and a script reading it is not. That advice is right and it is not enough.&lt;/p&gt;

&lt;p&gt;If your collector reads &lt;code&gt;navigator.userAgent&lt;/code&gt; and parses it, your tickets now carry an OS field that is measured, consistent, and wrong on every Mac you will ever receive a report from. It is worse than a typed field in one specific way: nobody audits it. A typed &lt;code&gt;macOS, I think the latest one&lt;/code&gt; invites a follow-up question. &lt;code&gt;Intel Mac OS X 10.15.7&lt;/code&gt;, sitting in the field a script filled in, reads as fact.&lt;/p&gt;

&lt;p&gt;Then somebody tries to reproduce. The ticket says Intel, Catalina. If you are diligent, you go and find an Intel machine, or spin up a VM with an OS from 2020, and you fail to reproduce a rendering bug that only happens on Apple silicon. You write &lt;strong&gt;cannot reproduce&lt;/strong&gt; and close it. The reporter, who is on an ARM Mac on macOS 26, files it again next week.&lt;/p&gt;

&lt;p&gt;That is the cost, and it is paid by whoever is least able to argue with the field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the real values
&lt;/h2&gt;

&lt;p&gt;Everything the string used to carry is still available, through client hints. The catch is the shape of the API.&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;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userAgentData&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;hints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;getHighEntropyValues&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;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHighEntropyValues&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;platformVersion&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;architecture&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;bitness&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;uaFullVersion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="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;On the machine above that returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"macOS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"platformVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"26.6.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"architecture"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bitness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"uaFullVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"154.0.8037.93"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mobile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&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;Which is the truth, and not a value you can get any other way.&lt;/p&gt;

&lt;p&gt;Two things about that snippet are load-bearing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is a promise.&lt;/strong&gt; &lt;code&gt;getHighEntropyValues&lt;/code&gt; is asynchronous, and a collector that builds its payload synchronously when somebody clicks a button will post before it resolves. The failure mode is not an error: the field is simply absent or empty, on every report, and nobody notices for months because an empty environment field looks like a configuration choice. If your capture runs in a click handler, the await has to be inside it and the submit has to wait.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is not everywhere.&lt;/strong&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData" rel="noopener noreferrer"&gt;MDN&lt;/a&gt; marks &lt;code&gt;navigator.userAgentData&lt;/code&gt; as experimental, with "limited availability" because it "does not work in some of the most widely-used browsers". So the optional chaining is not decoration. Where the API is missing you are back to the frozen string, and the honest thing is to record that you could not measure the OS version rather than to parse &lt;code&gt;10_15_7&lt;/code&gt; and present it as one.&lt;/p&gt;

&lt;p&gt;That second branch is the one worth building deliberately. A field that says &lt;code&gt;unknown&lt;/code&gt; is a field somebody will ask about. A field that says Catalina sends them to the wrong machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;The useful habit is not "measure, don't ask". It is: &lt;strong&gt;know what each field is measuring, and what it is capable of measuring.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The UA string is measuring what the browser has decided to disclose by default, which since 2023 is deliberately less than the truth. Client hints measure the truth, if you ask for it, asynchronously, where the API exists. Those are two different instruments with the same name on the form, and only one of them can answer the question your reproduction attempt is about to depend on.&lt;/p&gt;

&lt;p&gt;This one came out of a comment from &lt;a class="mentioned-user" href="https://dev.to/phongdesigns"&gt;@phongdesigns&lt;/a&gt; on an earlier piece here, who pointed out that &lt;code&gt;measured&lt;/code&gt; is not the end of the question and &lt;code&gt;measured by what&lt;/code&gt; is. That is the sharper version, and it is the one I would start from now.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>javascript</category>
      <category>browsers</category>
    </item>
    <item>
      <title>Your Template Set the Title. Something Else Decided What Shipped.</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Mon, 05 Oct 2026 12:58:17 +0000</pubDate>
      <link>https://dev.to/session_replay/your-template-set-the-title-something-else-decided-what-shipped-4ple</link>
      <guid>https://dev.to/session_replay/your-template-set-the-title-something-else-decided-what-shipped-4ple</guid>
      <description>&lt;p&gt;A page of ours returns 200. The HTML validates. Every test is green. The &lt;code&gt;h1&lt;/code&gt; reads exactly what we wrote. And for several weeks it was shown to about 1,580 people a month in search results and clicked once.&lt;/p&gt;

&lt;p&gt;One click. At an average position of 8.7, where the usual click-through is somewhere between one and two per cent, so the expected number was twenty or thirty.&lt;/p&gt;

&lt;p&gt;Nothing was broken in any sense a test or a log would recognise. What was broken was the &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; element, and it was broken in a way that nothing inside the application could see.&lt;/p&gt;

&lt;h2&gt;
  
  
  One variable, two outputs
&lt;/h2&gt;

&lt;p&gt;The controller builds a single string and hands it to both places that need it: the page title and the Open Graph title. One assignment, two consumers, same value.&lt;/p&gt;

&lt;p&gt;What came out the other end was not the same value twice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;h1       (68): TypeError: Failed to Fetch - What It Means and How to Find the Cause
og:title (68): TypeError: Failed to Fetch - What It Means and How to Find the Cause
&amp;lt;title&amp;gt;  (58): TypeError: Failed to Fetch - What It Means and How to Find
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A view helper fits the title element to a character budget, cutting on a word boundary. That is a reasonable thing for a site to do and it was added deliberately. The point is not that the truncation is wrong. The point is that &lt;strong&gt;nothing at the call site says it happens&lt;/strong&gt;, the other two consumers of the same string are untouched, and the result is a sentence that stops before its own subject.&lt;/p&gt;

&lt;p&gt;That is the page with the 1,580 impressions. The half it loses, "the Cause", is the half somebody searching for the meaning of an error is actually looking for. Four of our highest-traffic pages were shipping a title that ended mid-phrase like this; another stops at "and the Keyboard Key", dropping the thing the article is about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why no test caught it
&lt;/h2&gt;

&lt;p&gt;Because every test asserted on the value we set, not the bytes that left.&lt;/p&gt;

&lt;p&gt;The model holds the full string. The controller holds the full string. The template is handed the full string. You can write a perfectly good test at any of those layers, watch it pass, and learn nothing, because the transformation happens below the lowest layer anybody thought to assert on.&lt;/p&gt;

&lt;p&gt;This is a general shape and it is worth naming: &lt;strong&gt;for any output whose consumer is outside your process, the thing to assert on is the rendered bytes.&lt;/strong&gt; Search results, email clients, social cards, RSS, webhook payloads, PDF exports. Every one of those goes through a layer that can reformat, truncate, escape or re-encode what you set, and every one of those consumers is somewhere your test suite is not.&lt;/p&gt;

&lt;p&gt;The test that would have caught it is boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"the title element ships the whole headline"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="n"&gt;article_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Nokogiri&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTML&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;at_css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;
  &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="vi"&gt;@article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It asserts on &lt;code&gt;response.body&lt;/code&gt;, which is the only artefact that is actually true. Everything above it is an intention.&lt;/p&gt;

&lt;h2&gt;
  
  
  The expensive property
&lt;/h2&gt;

&lt;p&gt;What makes this class of fault costly is not severity. It is that &lt;strong&gt;it produces no event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is no exception, no 4xx, no stack trace, no log line, no alert. The page is correct by every check that runs inside the system that produced it. Monitoring is green because monitoring measures whether the page was served, and it was.&lt;/p&gt;

&lt;p&gt;The only evidence exists outside your application, in a number that somebody has to go and look at, in a tool they open occasionally, weeks after the change that caused it. We found ours because a weekly job finally ran after being unavailable for nineteen days, and the number was too strange to ignore.&lt;/p&gt;

&lt;p&gt;Until then, every signal we had said the page was fine. Every signal was telling the truth about the thing it measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape worth taking away
&lt;/h2&gt;

&lt;p&gt;A failure that your system cannot observe is not a smaller problem than one that throws. It is a longer one, because the clock starts when somebody outside notices and decides to tell you, and most of them do not.&lt;/p&gt;

&lt;p&gt;That is true of a truncated title, which at least leaves a number behind. It is far more true of the things that leave nothing: a customer whose page half-loaded and who closed the tab, a user who hit an error and shrugged, a visitor who saw a broken image and assumed the site was like that.&lt;/p&gt;

&lt;p&gt;Those are the same category. The system behaves correctly from the inside, the only witness is outside it, and whether you ever find out is entirely up to somebody who owes you nothing.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>testing</category>
      <category>seo</category>
    </item>
    <item>
      <title>A Hang Is Not an Error, and Every Timeout You Add Turns It Into the Wrong One</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Fri, 02 Oct 2026 13:00:23 +0000</pubDate>
      <link>https://dev.to/session_replay/a-hang-is-not-an-error-and-every-timeout-you-add-turns-it-into-the-wrong-one-2b46</link>
      <guid>https://dev.to/session_replay/a-hang-is-not-an-error-and-every-timeout-you-add-turns-it-into-the-wrong-one-2b46</guid>
      <description>&lt;p&gt;A command returns nothing. No message, no exit code, no log line, no stack. You wait, then you press ctrl-C, and the only thing you have afterwards is your own memory that it took a while.&lt;/p&gt;

&lt;p&gt;This is a different category from a failure, and the difference is not severity. An error hands you a string, a code, a file and a line, and usually a timestamp. A hang hands you nothing at all, and everything built on top of it inherits the nothing: your shell waits, your editor waits, your CI job waits until somebody's limit kills it, and whatever it prints then is about the limit rather than the cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The system's own word for it is "slow"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;man 2 read&lt;/code&gt; on any Unix is unusually blunt about where this comes from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;[EINTR]&lt;/strong&gt; A read from a slow device was interrupted before any data arrived by the delivery of a signal.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"Slow device" is the whole category in two words. A local SSD is not a slow device. A network filesystem is. So is a FUSE mount, a stalled VPN route, and any cloud file provider that has to go and fetch the bytes before it can give them to you. Every one of those can turn an ordinary &lt;code&gt;open()&lt;/code&gt; or &lt;code&gt;read()&lt;/code&gt; into a call with no upper bound on how long it takes, and &lt;code&gt;man 2 open&lt;/code&gt; says the same thing about opening the file in the first place.&lt;/p&gt;

&lt;p&gt;Nothing in your program is wrong. It asked for a file. The answer has not come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  The error you eventually see names the wrong layer
&lt;/h2&gt;

&lt;p&gt;Here is the part that costs the afternoon. When a stalled read does surface an error, the message describes &lt;strong&gt;what the process was doing&lt;/strong&gt;, not what was wrong.&lt;/p&gt;

&lt;p&gt;A runtime that cannot finish loading a script reports that it could not load that script, and names it. So you go and look at the script. It is present. It is readable. You read it yourself in another window and it comes back instantly. You check permissions, you check the interpreter, you check the path, and all of it is fine, because none of it was ever the problem. Three layers down, one file the process touched before it got to yours lives on something that was not answering.&lt;/p&gt;

&lt;p&gt;The message is accurate and it points at the wrong thing, which is worse than being wrong, because you can spend hours confirming that an innocent file is innocent.&lt;/p&gt;

&lt;h2&gt;
  
  
  And every timeout relabels it again
&lt;/h2&gt;

&lt;p&gt;The obvious defence makes the diagnosis harder.&lt;/p&gt;

&lt;p&gt;Wrap the call in &lt;code&gt;timeout 30&lt;/code&gt; and you get exit 124, which reads as "this tool is slow". Put that in CI and the job fails with something generic, and the team writes down "flaky". Add a retry and it passes sometimes, which upgrades "flaky" to "known flaky" and buys the real cause another month of life. Each layer swaps the symptom for a label about itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do instead of debugging the thing that hung
&lt;/h2&gt;

&lt;p&gt;Do not investigate the command. Investigate the syscall it is sitting in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Is it blocked, or just busy? U means uninterruptible wait.&lt;/span&gt;
ps &lt;span class="nt"&gt;-o&lt;/span&gt; pid,stat,etime,command &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# What has it got open? The culprit is usually the last entry.&lt;/span&gt;
lsof &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;

&lt;span class="c"&gt;# macOS: a stack for the blocked thread, which names the syscall.&lt;/span&gt;
sample &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 3 &lt;span class="nt"&gt;-file&lt;/span&gt; /tmp/hang.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m5&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'read\|open\|stat'&lt;/span&gt; /tmp/hang.txt

&lt;span class="c"&gt;# Linux: the same answer, cheaper.&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/stack&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /proc/&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;/wchan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;man ps&lt;/code&gt; defines the one that matters: &lt;code&gt;U&lt;/code&gt; marks "a process in uninterruptible wait". A process in that state is not slow, it is waiting on something that has not answered, and no amount of reading its source will tell you what.&lt;/p&gt;

&lt;p&gt;Then the control experiment, which is the step people skip: run the same operation with one input removed. Point the tool at a different config file, a different working directory, a different mount. If it returns instantly, the input you removed is the slow device, and you have the answer in one try instead of twenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this one is worth writing down
&lt;/h2&gt;

&lt;p&gt;Because a hang destroys its own evidence. Kill the process and there is no stack, no log line, no exit code, no timestamp: nothing to attach to a ticket and nothing for anybody else to read later. The next person gets "it froze sometimes" and starts from zero.&lt;/p&gt;

&lt;p&gt;That is also why "it hung" and "it was slow" are the least actionable things a user can tell you. They are reports about an event that left no artefact, and you cannot go back and collect one, because the only moment the state existed was while somebody was staring at a spinner and deciding whether to wait.&lt;/p&gt;

&lt;p&gt;Whatever you want to know about a stall, you have to take it while the stall is still happening.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Your Status Page Is Written in the Present Tense. Bug Reports Are Not.</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:59:39 +0000</pubDate>
      <link>https://dev.to/session_replay/your-status-page-is-written-in-the-present-tense-bug-reports-are-not-256m</link>
      <guid>https://dev.to/session_replay/your-status-page-is-written-in-the-present-tense-bug-reports-are-not-256m</guid>
      <description>&lt;p&gt;For eight days in late September the network between Tokyo and Singapore was congested because subsea cables had been cut. Cloudflare's status page carried it from 23 September and closed it on the 29th. The cause, in their own words: "multiple subsea cable outages have caused congestion between Tokyo and Singapore datacenters", dating back to 21 September at 02:20 UTC.&lt;/p&gt;

&lt;p&gt;No deploy caused it. It threw no errors. And it was more than enough to make a site feel slow across a large part of the world.&lt;/p&gt;

&lt;p&gt;What that produces on your side is the worst report you can receive: "the site was slow yesterday". No error, no stack trace, no status code to search for. One region, some users, intermittently. Nothing in your own metrics either, because your monitoring almost certainly runs somewhere the cables were fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The asymmetry nobody designs around
&lt;/h2&gt;

&lt;p&gt;A status page is written in the present tense. The front page answers exactly one question: is this broken right now.&lt;/p&gt;

&lt;p&gt;A bug report is written in the past tense. It asks a different one: was this broken then.&lt;/p&gt;

&lt;p&gt;So by the time somebody mentions the site was slow on Tuesday, the incident has closed and the dashboard says all systems operational. That is true, and useless, and it quietly reads as confirmation that the user imagined it. You close the ticket as not reproducible, and you are both slightly wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The history is an API, and you already depend on several of them
&lt;/h2&gt;

&lt;p&gt;The useful part is that the incident archive is machine readable, on a path that is the same almost everywhere, because most of these pages are the same hosted product rather than something each company built.&lt;/p&gt;

&lt;p&gt;I checked five this morning. Every one answered with JSON on &lt;code&gt;/api/v2/incidents.json&lt;/code&gt;: &lt;code&gt;www.cloudflarestatus.com&lt;/code&gt;, &lt;code&gt;www.githubstatus.com&lt;/code&gt;, &lt;code&gt;status.openai.com&lt;/code&gt;, &lt;code&gt;www.redditstatus.com&lt;/code&gt; and &lt;code&gt;status.datadoghq.com&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each incident record carries &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;impact&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;started_at&lt;/code&gt;, &lt;code&gt;resolved_at&lt;/code&gt;, the affected &lt;code&gt;components&lt;/code&gt;, and the full &lt;code&gt;incident_updates&lt;/code&gt; history with a timestamp on every update. Which means "was anything on fire around 14:30 yesterday" stops being a question you ask in Slack and becomes one you answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"json"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"net/http"&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"time"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;incidents_around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;window: &lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/v2/incidents.json"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

  &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"incidents"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"started_at"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"resolved_at"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;incident&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compact&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="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Time&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;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;

    &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;incidents_around&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"www.cloudflarestatus.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Time&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="s2"&gt;"2026-09-25T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'impact'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  One thing that will bite you
&lt;/h2&gt;

&lt;p&gt;Do not assume &lt;code&gt;started_at&lt;/code&gt; comes before &lt;code&gt;resolved_at&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Incidents get filed retroactively, when somebody notices afterwards that something was broken and writes it up. The record then carries the time it was written as its start. On GitHub's status page as I write there is an incident titled "[Retroactive] Actions workflow run failures after deployment gate approvals" whose &lt;code&gt;started_at&lt;/code&gt; is 11:34 UTC today and whose &lt;code&gt;resolved_at&lt;/code&gt; is 02:00 UTC the same day, nine and a half hours earlier.&lt;/p&gt;

&lt;p&gt;A natural-looking &lt;code&gt;started_at &amp;lt;= t &amp;amp;&amp;amp; t &amp;lt;= resolved_at&lt;/code&gt; filter silently excludes exactly the incidents somebody cared enough about to go back and document. Take the minimum and maximum of whatever timestamps the record carries instead, which is what the snippet above does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The precondition is the whole problem
&lt;/h2&gt;

&lt;p&gt;All of this runs on two inputs: roughly when, and roughly where.&lt;/p&gt;

&lt;p&gt;Neither of them comes out of a free-text box. "It was slow yesterday" has neither, and the conversation to get them is the one everybody has had. Which page? I don't remember. About when? Afternoon? A week later the cables are repaired, the report is closed as not reproducible, and the only lesson anybody took is that reporting things here is a waste of time.&lt;/p&gt;

&lt;p&gt;So the cheap half of this is a script you write once and keep. The expensive half is arriving at a report that carries a timestamp and a location at all, which is a capture problem rather than an investigation one.&lt;/p&gt;

&lt;p&gt;Write the script anyway. It is twenty lines and it will settle an argument every few months. It just cannot do anything with a report that has nothing to join on.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>monitoring</category>
      <category>api</category>
    </item>
    <item>
      <title>The Error Your User Read Was Not in English, and You Have Been Searching for the Wrong String</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Wed, 30 Sep 2026 12:58:28 +0000</pubDate>
      <link>https://dev.to/session_replay/the-error-your-user-read-was-not-in-english-and-you-have-been-searching-for-the-wrong-string-2o09</link>
      <guid>https://dev.to/session_replay/the-error-your-user-read-was-not-in-english-and-you-have-been-searching-for-the-wrong-string-2o09</guid>
      <description>&lt;p&gt;A support ticket arrives with a screenshot of a browser error page. You read the message, paste a distinctive phrase from it into your codebase search, and get nothing. You search the web for it and get nothing useful either.&lt;/p&gt;

&lt;p&gt;Then you look again and notice the page is in German.&lt;/p&gt;

&lt;p&gt;This is a small thing that wastes a disproportionate amount of time, and it has a fix so cheap it is almost embarrassing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browsers localise the prose and keep the identifier
&lt;/h2&gt;

&lt;p&gt;Chromium's network errors have two parts that behave completely differently.&lt;/p&gt;

&lt;p&gt;There is a stable constant, like &lt;code&gt;ERR_NAME_NOT_RESOLVED&lt;/code&gt; or &lt;code&gt;ERR_CONNECTION_REFUSED&lt;/code&gt;, defined in &lt;code&gt;net_error_list.h&lt;/code&gt; with a fixed numeric value and a one-line English description for developers. That identifier does not change between locales, browser versions, or installs.&lt;/p&gt;

&lt;p&gt;And there is the text the user actually reads, which is translated into whatever language their browser is running in.&lt;/p&gt;

&lt;p&gt;So the same failure that tells you "This site can't be reached" tells somebody else "Diese Seite ist nicht erreichbar" or "No se puede acceder a este sitio web". One underlying condition, one stable code, and dozens of surface strings.&lt;/p&gt;

&lt;p&gt;Your instinct when a report arrives is to work from the sentence, because the sentence is the part that looks like information. It is the part that does not survive translation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which means the question you are asking is the wrong one
&lt;/h2&gt;

&lt;p&gt;"What did the error say?" is the natural thing to ask and it is subtly the worst version of the question, because it requests exactly the part that is locale-dependent.&lt;/p&gt;

&lt;p&gt;"Is there a code, usually in smaller text underneath?" requests the part that is not.&lt;/p&gt;

&lt;p&gt;The second question also happens to be easier for a non-technical person to answer, because they are not being asked to transcribe a paragraph or judge what is relevant. They are being asked to find a short string that looks like shouting.&lt;/p&gt;

&lt;p&gt;And the answer is searchable. &lt;code&gt;ERR_CERT_DATE_INVALID&lt;/code&gt; returns the same results whoever typed it, from wherever. The German sentence does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  This applies to your own error messages too
&lt;/h2&gt;

&lt;p&gt;Here is the part that costs something, and it is about code you control rather than code you do not.&lt;/p&gt;

&lt;p&gt;If your application shows localised error text and nothing else, you have built the same trap. Your Spanish-speaking user reports "Se produjo un error inesperado", you search your source for it, and depending on how your translations are stored you may not find the key, the call site, or the condition that produced it.&lt;/p&gt;

&lt;p&gt;The fix is to emit a stable identifier alongside the human text, visible to the user and constant across locales. Something like a short code in the corner of the error state, or in a detail line under it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Something went wrong. Please try again.
Reference: PAY-4021
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PAY-4021&lt;/code&gt; is not for the user. It is for you, and it survives the round trip through a screenshot, a translation, a paraphrase and a support agent's summary, all of which destroy the sentence above it.&lt;/p&gt;

&lt;p&gt;The rule of thumb: &lt;strong&gt;anything in your error UI that a translator can change is something a bug report cannot reliably carry.&lt;/strong&gt; If the only distinguishing content in a failure state is prose, you have made every report of it ambiguous by construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The wider shape
&lt;/h2&gt;

&lt;p&gt;Localisation is one instance of a general problem with bug reports, which is that the reporter can only send you what the interface showed them, and interfaces are designed for reading rather than for reporting.&lt;/p&gt;

&lt;p&gt;The user did not choose to send you an untranslatable sentence instead of a code. They sent you what was on screen. If the screen carried a stable reference, they would have sent that, because it is shorter and more obviously official-looking.&lt;/p&gt;

&lt;p&gt;So the next time a report is useless, it is worth asking whether the person failed to describe the problem or whether the screen failed to give them anything describable. Those have different fixes, and only one of them involves the reporter.&lt;/p&gt;

&lt;p&gt;Chromium's list of these error constants, with the English description of each, is &lt;a href="https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h" rel="noopener noreferrer"&gt;net_error_list.h&lt;/a&gt;. It is worth a bookmark for the next screenshot you cannot read.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>i18n</category>
      <category>localization</category>
    </item>
    <item>
      <title>A Sanitizer Is Two Parsers Agreeing, and Yours Only Controls One of Them</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Mon, 28 Sep 2026 12:57:51 +0000</pubDate>
      <link>https://dev.to/session_replay/a-sanitizer-is-two-parsers-agreeing-and-yours-only-controls-one-of-them-3643</link>
      <guid>https://dev.to/session_replay/a-sanitizer-is-two-parsers-agreeing-and-yours-only-controls-one-of-them-3643</guid>
      <description>&lt;p&gt;Last week a critical XSS landed in SunEditor, a rich-text editor a lot of applications embed. CVE-2026-59167, CVSS 10, fixed in 2.47.11. The advisory's own one-line summary is the interesting part: &lt;strong&gt;sanitizer bypass&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not "SunEditor had no sanitizer". It had one. The sanitizer ran, did its job as written, and the output still executed JavaScript.&lt;/p&gt;

&lt;p&gt;That distinction is worth more than the CVE, because the shape of it is not specific to SunEditor and will outlive this patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The assumption that breaks
&lt;/h2&gt;

&lt;p&gt;The mental model most of us carry is that sanitizing is filtering: you take a string of user HTML, remove the dangerous parts, and what remains is safe.&lt;/p&gt;

&lt;p&gt;That model quietly assumes there is one agreed answer to the question "what is in this string". There is not. &lt;strong&gt;Sanitizing is two parsers agreeing with each other, and you only control one of them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your sanitizer parses the input and decides what the elements and attributes are. Later, a browser parses the sanitized output and decides what the elements and attributes are. If those two ever disagree about the same bytes, the browser wins, because the browser is the one that renders it.&lt;/p&gt;

&lt;p&gt;Every sanitizer bypass in history is a place where they disagree.&lt;/p&gt;

&lt;h2&gt;
  
  
  What disagreed this time
&lt;/h2&gt;

&lt;p&gt;The SunEditor advisory names it precisely: the sanitization logic "does not fully remove executable event-handler attributes from certain custom/namespaced tags", and it points at namespaced elements, written like &lt;code&gt;&amp;lt;a:b&amp;gt;&lt;/code&gt;, as the construct involved.&lt;/p&gt;

&lt;p&gt;You can see the shape of the mistake without the payload. A filter that works from a list of known elements has to decide what to do with an element it does not recognise. &lt;code&gt;&amp;lt;a:b&amp;gt;&lt;/code&gt; is not in anyone's allowlist. It is not a real HTML element. A reasonable-looking implementation treats it as unknown, declines to apply the attribute rules it has for real elements, and leaves it alone.&lt;/p&gt;

&lt;p&gt;The browser is less fussy. It parses that into an element in the DOM like any other, and an &lt;code&gt;onclick&lt;/code&gt; attribute sitting on it is an event handler like any other. The sanitizer saw something it did not recognise; the browser saw something it was perfectly happy to run.&lt;/p&gt;

&lt;p&gt;Notice that the sanitizer was not bypassed by a cleverer string. It was bypassed by a construct that fell outside the set of things it had opinions about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule that generalises
&lt;/h2&gt;

&lt;p&gt;Read the advisory's own remediation list and every item is really one instruction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normalize DOM elements before sanitization&lt;/li&gt;
&lt;li&gt;Explicitly reject or unwrap unknown/custom/namespaced tags&lt;/li&gt;
&lt;li&gt;Strip all event-handler attributes from all elements, including unknown/custom elements&lt;/li&gt;
&lt;li&gt;Re-validate sanitized output after browser DOM parsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The instruction is: &lt;strong&gt;stop sanitizing the string and start sanitizing the tree.&lt;/strong&gt; Parse the input with the same engine that will eventually render it, walk the resulting nodes, and decide per node. Then your parser and the browser's parser cannot disagree, because there is only one of them.&lt;/p&gt;

&lt;p&gt;And the second half, which is the part people skip: an allowlist has to be a decision about everything, not a decision about the things you listed. "Remove &lt;code&gt;onclick&lt;/code&gt; from elements I know" leaves a hole shaped exactly like the elements you do not. "Remove every attribute beginning with &lt;code&gt;on&lt;/code&gt;, from every node, whatever it is" does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this class reaches you badly
&lt;/h2&gt;

&lt;p&gt;Stored XSS has an awkward property: &lt;strong&gt;the person who sees it is never the person who can fix it, and usually not even the person who caused it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An author saves content. A reader loads the page days later and the payload fires in &lt;em&gt;their&lt;/em&gt; browser. The developer is the third party in that transaction and observes none of it.&lt;/p&gt;

&lt;p&gt;Your server logs are no help, because nothing went wrong on the server. The save returned 200. The page render returned 200. Both are accurate. The entire event happened after the bytes left your machine, in a browser you have no access to, belonging to somebody who has no vocabulary for what they just saw.&lt;/p&gt;

&lt;p&gt;So it arrives as "a weird box appeared", or "the page flashed red", or most often as nothing at all, because the reader assumed their own machine was playing up and closed the tab.&lt;/p&gt;

&lt;p&gt;That is the uncomfortable takeaway. For a whole class of serious bug, your instrumentation is structurally in the wrong place, and the only witness is someone with no idea what they witnessed and no way to hand you the evidence.&lt;/p&gt;

&lt;p&gt;Upgrade SunEditor if you embed it. But the more durable action is to go and look at whatever sanitizes user HTML in your own stack, and find out whether it is reasoning about a string or about a tree.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>javascript</category>
      <category>html</category>
    </item>
    <item>
      <title>Your Own Browser Is the Worst Instrument You Own for Testing Your Own Site</title>
      <dc:creator>Vladimir Elchinov</dc:creator>
      <pubDate>Fri, 25 Sep 2026 12:57:59 +0000</pubDate>
      <link>https://dev.to/session_replay/your-own-browser-is-the-worst-instrument-you-own-for-testing-your-own-site-2hei</link>
      <guid>https://dev.to/session_replay/your-own-browser-is-the-worst-instrument-you-own-for-testing-your-own-site-2hei</guid>
      <description>&lt;p&gt;There is a category of bug that is invisible to the person best placed to fix it, and the reason is structural rather than careless.&lt;/p&gt;

&lt;p&gt;The clearest example is the login redirect loop. Your app finds no valid session and redirects to &lt;code&gt;/login&lt;/code&gt;. The login page believes it is signed in and redirects back. Round and round, until the browser gives up.&lt;/p&gt;

&lt;p&gt;It reproduces for every new visitor. It never reproduces for you, because your session cookie was set weeks ago and is working fine.&lt;/p&gt;

&lt;p&gt;You cannot see it, and no amount of care makes you able to. You are in a state the affected person is not in, and you cannot leave it by looking harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reflex, and what it actually covers
&lt;/h2&gt;

&lt;p&gt;The reflex is a private window, and it is a good reflex. It clears cookies, storage and cache, which is genuinely most of the client-side state that makes you unrepresentative.&lt;/p&gt;

&lt;p&gt;Here is what it does not touch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your account.&lt;/strong&gt; If the bug depends on a user with old data, an unusual permission set, or a record created before a migration, a private window gives you a signed-out browser and the same database. Sign in again and you are back in your own privileged, well-seeded, three-years-old account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your feature flags and experiment buckets.&lt;/strong&gt; If assignment is by user id rather than by cookie, a private window changes nothing. You are in the variant you were always in. If it is by cookie, you get a random new one, which is not the same as getting the one the reporter had.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your network position.&lt;/strong&gt; Same ISP, same resolver, same country, same corporate proxy, same VPN. A regional CDN problem, a DNS record that has not propagated everywhere, or a firewall that only affects one network are all untouched by which window you opened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your CDN edge.&lt;/strong&gt; Your nearest edge node has been warmed by your own traffic for months. A visitor hitting a cold edge somewhere else gets a different first-byte time and, if something is misconfigured, sometimes a different response entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your device.&lt;/strong&gt; Same OS, same browser build, same fonts, same locale, same timezone, same screen, same accessibility settings. Every layout bug that only happens at a particular width, and every date bug that only happens across a timezone boundary, is still invisible.&lt;/p&gt;

&lt;p&gt;So a private window resets the client, and a large share of what makes your experience unrepresentative was never in the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth knowing what you are carrying
&lt;/h2&gt;

&lt;p&gt;If you want to see how much local state you are actually holding on a site, this is quick, and the answer is usually more than people expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run in the console on your own site, in a normal window.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cookies:&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="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;; &lt;/span&gt;&lt;span class="dl"&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="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localStorage keys:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sessionStorage keys:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceWorker&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;getRegistrations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;service workers:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rs&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;r&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;scope&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="nx"&gt;caches&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="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ks&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cache storage:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A service worker is the one that catches people out. It can serve a cached shell to you indefinitely while every new visitor gets the current one, which means you and they are running different versions of the front end with no version number anywhere to say so.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for a bug report
&lt;/h2&gt;

&lt;p&gt;The practical consequence is not "test more carefully". It is that &lt;strong&gt;for a whole class of bugs, the reporter is holding evidence you cannot regenerate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not their opinion of what went wrong. The state: which build they had, what their cookies said, what the network actually returned, what was in the console at the moment it failed. Once they close the tab, most of it is gone, and the gap between "it works for me" and "it does not work for me" becomes an argument neither side can settle.&lt;/p&gt;

&lt;p&gt;Which is why "can you try again and tell me if it still happens" is such an expensive question. It asks the one person with the evidence to destroy it and produce a fresh copy, from a state that may not reproduce.&lt;/p&gt;

&lt;p&gt;The cheaper question is the one that captures what they had while they still have it. And the cheapest habit, when a report arrives that you cannot reproduce, is to stop treating your own failure to reproduce as evidence. It mostly tells you that you are a different user on a different network on a different machine, which you already knew.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>debugging</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
