<?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: donerightlabs</title>
    <description>The latest articles on DEV Community by donerightlabs (@donerightlabs).</description>
    <link>https://dev.to/donerightlabs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112098%2F973c714e-2988-484c-a92d-d355072ac6f4.png</url>
      <title>DEV Community: donerightlabs</title>
      <link>https://dev.to/donerightlabs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/donerightlabs"/>
    <language>en</language>
    <item>
      <title>An AI Assistant Handed Me 6 'Real' Invoice Samples. 5 Were Completely Made Up</title>
      <dc:creator>donerightlabs</dc:creator>
      <pubDate>Thu, 10 Sep 2026 11:05:09 +0000</pubDate>
      <link>https://dev.to/donerightlabs/an-ai-assistant-handed-me-6-real-invoice-samples-5-were-completely-made-up-3ddo</link>
      <guid>https://dev.to/donerightlabs/an-ai-assistant-handed-me-6-real-invoice-samples-5-were-completely-made-up-3ddo</guid>
      <description>&lt;p&gt;I was building a parser for Vietnamese electronic invoices — the XML files tax authorities require for every VAT invoice issued in Vietnam. The goal: read a file from any provider, whoever issued it, and normalize it into one clean JSON shape.&lt;/p&gt;

&lt;p&gt;To sanity-check it against real-world variation, I asked another AI assistant to help me gather sample invoice XML files from a handful of different well-known Vietnamese e-invoice providers, plus one generic "tax authority standard" file — six in total.&lt;/p&gt;

&lt;p&gt;I ran my parser against all six. It rejected five of them outright.&lt;/p&gt;

&lt;p&gt;My first reaction was: great, my code is broken on day one, five different providers, five different failures. My second reaction, a few minutes later, was that this was actually the correct outcome — and figuring out why took me somewhere I wasn't expecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch
&lt;/h2&gt;

&lt;p&gt;Before assuming my code was wrong, I did something boring: I read the README the other AI had written for the sample files. Buried in it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"the data inside (company names, tax codes, amounts) is simulated test data, not actual issued invoices... sourced from an open-source repo."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Simulated. Not "close to real." Simulated by a developer who'd never seen an actual Vietnamese invoice, writing plausible-looking test fixtures for their own unrelated project on GitHub — repurposed by an AI assistant as "sample invoices" because they were shaped like the right kind of thing.&lt;/p&gt;

&lt;p&gt;So instead of trusting my own tests, I went and checked the actual tag names against the legal source of truth.&lt;/p&gt;

&lt;p&gt;Here's the thing about Vietnamese e-invoices: since 2022, every provider is legally required to use the &lt;em&gt;exact same&lt;/em&gt; XML tag structure, defined by the tax authority. Not "similar." Identical. &lt;code&gt;TTChung&lt;/code&gt;, &lt;code&gt;NBan&lt;/code&gt;, &lt;code&gt;NMua&lt;/code&gt;, &lt;code&gt;MST&lt;/code&gt;, &lt;code&gt;DChi&lt;/code&gt; — Vietnamese abbreviations, mandated by decree, because the tax authority's own systems have to parse every invoice regardless of which vendor issued it.&lt;/p&gt;

&lt;p&gt;Five of my six "sample" files used tags like &lt;code&gt;&amp;lt;SellerInfo&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;BuyerTaxCode&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;InvoiceNumber&amp;gt;&lt;/code&gt;. Clean, readable, very sensible-looking English tag names.&lt;/p&gt;

&lt;p&gt;Tag names that, as far as I can tell, no real Vietnamese e-invoice has ever used. Nobody in Vietnam is legally allowed to invent their own — that's the entire point of the mandate. My parser hadn't failed. It had correctly identified five pieces of confident, internally-consistent fiction and said so. Only the sixth file used the real government-mandated structure, and that's the only one it accepted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;I could have made the parser "smarter" — taught it to also recognize the invented English tag names, so it would accept all six and I'd have a tidier-looking demo. I didn't. Teaching a parser to accept a format that doesn't exist in reality isn't robustness. It's just a more elaborate way of being wrong, with extra confidence — and the first real invoice that came in later would have had to fight through that noise instead of matching cleanly.&lt;/p&gt;

&lt;p&gt;So I built strictly to the real legal spec, kept the one sample that actually matched it, and waited for real data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then real invoices started arriving
&lt;/h2&gt;

&lt;p&gt;Over the next few days, the person I was building this for started digging up actual XML files from their own email and provider portals — a utility bill, a couple of online purchase receipts, a clinic invoice, an old 2021 logistics invoice, eventually a genuine business-to-business invoice.&lt;/p&gt;

&lt;p&gt;Three bugs surfaced. None of them showed up on a single one of the AI-generated samples, because you can only find these by touching the real thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 1 — the privacy leak.&lt;/strong&gt; My parser had a "raw fallback" mode: alongside the clean normalized output, it also dumped every tag/value pair it found, so nobody would lose data to a field I hadn't mapped yet. I'd also built a privacy feature — if a buyer had no tax code (a strong signal they're a private individual, not a business), their name and address got redacted from the output. Redacted from the &lt;em&gt;clean&lt;/em&gt; output, that is. Nobody told the raw fallback dump about that rule. The first real invoice I tested — a utility bill — leaked the buyer's actual name, home address, and email straight through the "debug" field I'd added for convenience. The privacy feature and the debug feature had never been told about each other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 2 — silently dropping the buyer's name.&lt;/strong&gt; The real spec has two different tags for "who bought this": one for a company name, a different one for an individual's name. I only knew about the company-name tag — none of the fake samples had ever used the other one, because whoever wrote them didn't know it existed either. Real invoices from individual buyers started coming in using that second tag. My parser read them, found nothing under the tag it was looking for, and happily reported "fully processed, no issues." It wasn't lying exactly. It just didn't know what it didn't know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug 3 — the one that actually worried me.&lt;/strong&gt; A 2021 invoice came in using an older schema version than everything I'd tested. Same overall shape, but the invoice number and issue date — the two fields you'd use to identify or deduplicate an invoice — lived under different tag names entirely. My parser again reported total success while silently returning &lt;code&gt;null&lt;/code&gt; for both.&lt;/p&gt;

&lt;p&gt;That's the one that made me change how the whole system reports errors. It's not enough to check "did I find the big structural block." I had to start checking "did I actually get a value for the fields that matter," and say so explicitly when I didn't — instead of a green checkmark hiding a null.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep thinking about
&lt;/h2&gt;

&lt;p&gt;The fake samples weren't malicious, and the AI that made them was upfront that they weren't real — I just could have easily not checked. They were also &lt;em&gt;useless as an adversary&lt;/em&gt;. A genuinely broken test file breaks loudly. A plausible-but-fictional one passes quietly and teaches you nothing, because it was generated by something optimizing for "looks like a Vietnamese invoice" instead of "is one."&lt;/p&gt;

&lt;p&gt;Real data doesn't have that problem. It doesn't care if it's plausible. It just is what it is, including two tag names I'd never heard of and a schema version I didn't know still existed in the wild.&lt;/p&gt;

&lt;p&gt;I don't think the lesson here is "don't use AI-generated test data." I used AI for plenty of this build, including help gathering that first batch of samples. The lesson is narrower: when you're validating against an external, legally-defined standard you don't fully control, "generated a file that passes my own tests" and "matches reality" are two different claims, and only one of them is checkable — by going back to the source of truth instead of trusting the fixture.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few questions I'd ask if I were reading this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Wait, so is the AI-generated test data "bad"?&lt;/strong&gt; Not really — it was honestly labeled as simulated, and it was still useful as a starting shape. The mistake would've been mine, not the tool's, if I'd stopped checking there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What does the parser do now when it hits a tag name it's never seen?&lt;/strong&gt; It doesn't guess. It reports exactly what it found, flags which expected fields came back empty, and includes the raw data it read so a human can see what actually happened instead of a silent null.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Couldn't you just support every tag name variant you find?&lt;/strong&gt; For invented, non-standard ones, no — that would mean trusting fiction as if it were law. For real variants tied to an older but still legally valid schema version, yes, and that's exactly what happened with bug 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this a Vietnam-specific problem?&lt;/strong&gt; The specific tags are. The pattern — an AI-shaped test fixture that's plausible enough to pass your own tests without matching the actual external standard you're building against — isn't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The parser is live on Apify Store as &lt;a href="https://apify.com/donerightlabs/vn-einvoice-xml-normalizer" rel="noopener noreferrer"&gt;Vietnam E-Invoice XML Normalizer&lt;/a&gt; — callable directly by AI agents over MCP if you're building bookkeeping tools that need to ingest Vietnamese invoices.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Three Playwright/Apify bugs that took me way too long to find (and the fixes)</title>
      <dc:creator>donerightlabs</dc:creator>
      <pubDate>Sun, 06 Sep 2026 10:04:46 +0000</pubDate>
      <link>https://dev.to/donerightlabs/three-playwrightapify-bugs-that-took-me-way-too-long-to-find-and-the-fixes-15ko</link>
      <guid>https://dev.to/donerightlabs/three-playwrightapify-bugs-that-took-me-way-too-long-to-find-and-the-fixes-15ko</guid>
      <description>&lt;h1&gt;
  
  
  Three Playwright/Apify bugs that took me way too long to find (and the fixes)
&lt;/h1&gt;

&lt;p&gt;I recently shipped &lt;a href="https://apify.com/donerightlabs/tender-intelligence" rel="noopener noreferrer"&gt;Vietnam Tender Intelligence&lt;/a&gt; — a keyword-based monitor for Vietnam's national e-procurement portal, built with Playwright + &lt;a href="https://github.com/daijro/camoufox" rel="noopener noreferrer"&gt;Camoufox&lt;/a&gt; (a hardened Firefox build for stealth automation). It works, and it's live: it turned out to be a much better debugging exercise than I expected. Three bugs in particular cost me way more time than they should have, so I'm writing them down for whoever hits the same wall next.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A silent version mismatch between Playwright and Camoufox
&lt;/h2&gt;

&lt;p&gt;Right after wiring up the browser launch, every run failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Found property "&amp;lt;root&amp;gt;.viewport.isMobile" - false which is not described in this scheme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in my code touched &lt;code&gt;isMobile&lt;/code&gt;. Turned out Playwright 1.61 added that field to the internal &lt;code&gt;Browser.setDefaultViewport&lt;/code&gt; protocol call, and Camoufox's Juggler layer (the protocol it uses to drive Firefox) didn't recognize it yet. It's a real upstream incompatibility, tracked in &lt;a href="https://github.com/daijro/camoufox/issues/653" rel="noopener noreferrer"&gt;daijro/camoufox#653&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The counter-intuitive part: the official Apify base image is tagged &lt;code&gt;apify-python-playwright-camoufox:3.14-1.61.0&lt;/code&gt; — so the natural instinct is to match your &lt;code&gt;playwright&lt;/code&gt; pin to &lt;code&gt;1.61.0&lt;/code&gt;. Don't. That's the &lt;em&gt;broken&lt;/em&gt; version for this combo. Pinning &lt;code&gt;playwright==1.60.0&lt;/code&gt; fixed it immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; when a base image tag looks like a version recommendation, verify it against the actual compatibility matrix of the libraries involved — it can just be a taxonomy label, not a peer-dependency promise.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;.click()&lt;/code&gt; on a text input can hang forever; &lt;code&gt;.fill()&lt;/code&gt; doesn't
&lt;/h2&gt;

&lt;p&gt;This one took the longest to nail down because it was intermittent. Some runs would sail through in 40 seconds; others would sit at &lt;code&gt;Locator.click()&lt;/code&gt; until a timeout fired, with no error, no popup, nothing actionable in the trace.&lt;/p&gt;

&lt;p&gt;After enough repeated runs, I noticed the timeouts always stalled at the exact same call: clicking a text input immediately before typing into it — a completely standard "click to focus, then type" pattern. Switching to &lt;code&gt;locator.fill(value)&lt;/code&gt; — which focuses the element as part of its own actionability checks, without dispatching a raw mouse click — made the hang disappear across dozens of subsequent runs.&lt;/p&gt;

&lt;p&gt;I never found a fully satisfying root cause (my best guess: something in the click-dispatch path occasionally not resolving cleanly under a residential-proxy connection with irregular latency, which &lt;code&gt;fill()&lt;/code&gt;'s internal path avoids). But the fix generalizes: &lt;strong&gt;if you don't need the literal mouse-click side effect, &lt;code&gt;.fill()&lt;/code&gt; is strictly more robust for text inputs&lt;/strong&gt; — one less network round-trip and no dependency on precise coordinate/z-index resolution to succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Apify's automated Actor tests use your &lt;em&gt;default input&lt;/em&gt; — and an empty dataset counts as a failure
&lt;/h2&gt;

&lt;p&gt;Apify runs every published Actor daily with its default/prefilled input and expects: success, completion within 5 minutes, and a &lt;strong&gt;non-empty&lt;/strong&gt; dataset. Miss any of those on 2 of the last 3 daily runs and the Actor gets silently flagged "Under maintenance" (hidden from Store search) — you find out by email.&lt;/p&gt;

&lt;p&gt;Two non-obvious ways to trip this that aren't really "bugs" in the traditional sense:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your default input is a narrow query that occasionally has zero real-world matches, a perfectly correct run with zero results still counts as a failure. Pick a default keyword/filter broad enough to (almost) always return something.&lt;/li&gt;
&lt;li&gt;If you've added retry logic to make individual runs more resilient (good for real users, who don't mind an extra 2 minutes), make sure the worst case — including retries — still fits inside 5 minutes. A retry loop tuned for "eventually succeed" and a hard 5-minute CI-style SLA pull in different directions; you have to explicitly budget for both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Also: pick a genuinely persistent store, not the default one
&lt;/h2&gt;

&lt;p&gt;Small one, but it cost a day of "why isn't my session persisting" confusion: &lt;code&gt;Actor.getValue()&lt;/code&gt; / &lt;code&gt;Actor.setValue()&lt;/code&gt; operate on the &lt;em&gt;current run's&lt;/em&gt; default key-value store, which is fresh every run. To actually persist state (a saved cookie jar, in my case) across separate Actor executions, open a &lt;strong&gt;named&lt;/strong&gt; store instead — &lt;code&gt;Actor.openKeyValueStore(name=...)&lt;/code&gt; in Python — and read/write through that. Obvious in hindsight; not obvious from the method names.&lt;/p&gt;




&lt;p&gt;None of these are exotic. They're the kind of thing you find in 20 minutes with a good search — once you know the exact term to search for. Hopefully this saves the next person that 20 minutes, or the day I lost on the key-value store one.&lt;/p&gt;

&lt;p&gt;If you're building something similar (Playwright + Camoufox + Apify), happy to compare notes in the comments. And if you're curious what it looks like running end to end, the Actor itself is here: &lt;a href="https://apify.com/donerightlabs/tender-intelligence" rel="noopener noreferrer"&gt;Vietnam Tender Intelligence&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>debugging</category>
      <category>programming</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
