<?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: nathanielbrooks0360</title>
    <description>The latest articles on DEV Community by nathanielbrooks0360 (@nathanielbrooks0360).</description>
    <link>https://dev.to/nathanielbrooks0360</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%2F4061625%2F39438523-c662-44b0-aa96-19755f633aaf.png</url>
      <title>DEV Community: nathanielbrooks0360</title>
      <link>https://dev.to/nathanielbrooks0360</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nathanielbrooks0360"/>
    <language>en</language>
    <item>
      <title>Error event metadata: release, environment, request ID — and staying PII-safe under GDPR</title>
      <dc:creator>nathanielbrooks0360</dc:creator>
      <pubDate>Tue, 04 Aug 2026 18:22:06 +0000</pubDate>
      <link>https://dev.to/nathanielbrooks0360/error-event-metadata-release-environment-request-id-and-staying-pii-safe-under-gdpr-5bdm</link>
      <guid>https://dev.to/nathanielbrooks0360/error-event-metadata-release-environment-request-id-and-staying-pii-safe-under-gdpr-5bdm</guid>
      <description>&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; attach release, environment, service, route and a request ID to every error event, keep the user identity down to an opaque internal reference, and do the PII work at the producer — the plan where you go back later and delete user-specific rows out of a log store is the part that usually doesn't hold up.&lt;/p&gt;

&lt;p&gt;The metadata you never attached is the metadata nobody can ask you to erase.&lt;/p&gt;

&lt;p&gt;I own the platform roadmap for a payments-adjacent team, so every error tracking decision gets weighed three ways: what it costs to run, what it does to the on-call rotation, and how expensive it would be to leave. Error event metadata sits right in the middle of that argument. Attach too little and you're reading stack traces with no idea which release or which region produced them, attach too much and you've quietly built a second copy of your user table inside somebody else's search index, in a jurisdiction you didn't pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  The incident where the field I assumed was there wasn't
&lt;/h2&gt;

&lt;p&gt;Our checkout service captures errors from two places: the HTTP path, where middleware stuffs a request ID into the context, and a background retry worker that drains a queue. I'd been on the team long enough to believe every captured event carried that request ID, because I'd seen it in every event I'd ever looked at — and every event I'd ever looked at came from the HTTP path.&lt;/p&gt;

&lt;p&gt;Then a bad deploy went out on a Friday.&lt;/p&gt;

&lt;p&gt;For 40 minutes the retry worker produced roughly twelve thousand events that all landed in a single group, with an empty request ID, an empty route, and a payload whose entire useful content was &lt;code&gt;error: invalid input&lt;/code&gt;. No correlation ID meant no way to join those events against the access log, so I couldn't tell whether one customer was retrying a poisoned message ten thousand times or whether ten thousand customers each hit it once. Those are wildly different incidents — the first is a bad row, the second is an error budget event that eats a quarter's worth of burn. I spent about 90 minutes writing a throwaway script to correlate by timestamp before I noticed the worker builds its own &lt;code&gt;context.Context&lt;/code&gt; and never went through the middleware that populates the field. My assumption, my bug, and the error message told me nothing about it.&lt;/p&gt;

&lt;p&gt;The invariant I took away: metadata quality is a property of the code path, not of the tool. Any field that gets injected by one framework layer will be missing on every path that skips that layer, and you will find out during the incident rather than before it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What metadata should you attach to an error event without shipping user data you can't delete?
&lt;/h2&gt;

&lt;p&gt;Six fields carry most of the debugging weight, and I'd argue for them in this order: release (the exact build or commit, not a semver you bumped by hand), environment, service name, route or job name, request ID, and — if you run distributed tracing — trace and span IDs so the event can be joined against traces you already store elsewhere.&lt;/p&gt;

&lt;p&gt;That's the whole list I'd defend in a design review.&lt;/p&gt;

&lt;p&gt;Everything past that is a cardinality decision, and cardinality is a capacity question in disguise. Every field you attach is a dimension somebody will eventually group by, and a tag with 50k distinct values behaves very differently from one with 12. Region, tenant ID, feature flag state and client version are usually worth their cost. Full URLs with query strings are not, because query strings collect tokens; raw request bodies are not, because they collect everything; headers are not, unless you allowlist them, since &lt;code&gt;Authorization&lt;/code&gt; and &lt;code&gt;Cookie&lt;/code&gt; ride along with the rest. Stack-local variables are the sharpest edge here — plenty of error SDKs will serialize locals for you, which is exactly how an email address or a password reset token ends up in a stack frame that gets indexed and retained for 90 days.&lt;/p&gt;

&lt;p&gt;For user identity, attach a stable pseudonymous reference — &lt;code&gt;u_9f2c41&lt;/code&gt;, generated by you, meaningful only against a mapping table you control. It gives you everything the debugging session needs (is this one user or ten thousand?) and it keeps the natural identifiers out of the payload. It also means the affected-user count in your incident review is a real number instead of a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  PII, GDPR, and the erasure request you can't serve from your log store
&lt;/h2&gt;

&lt;p&gt;GDPR Article 17 gives people a right to erasure, and observability payloads are where that right goes to get complicated, because logs and error events are the one data store nobody models. The EU framing is the strict one, the US picture is a patchwork with California's CPRA closest to it in spirit, and in both cases the operational question is identical: when a deletion request arrives, can you actually find and remove that person's data, everywhere?&lt;/p&gt;

&lt;p&gt;Most error tracking backends don't answer yes. What they give you is time-based retention plus scrubbing, which is a different control with a different shape: Sentry does server-side scrubbing with configurable retention, Datadog sells a sensitive data scanner that redacts on ingest, and a self-hosted Grafana Loki setup gives you deletion at the chunk level rather than at the person level. Infrai's observability surface takes the retention-and-redaction approach too and doesn't offer per-user log deletion or a bulk export interface, which is a real constraint to price in if your erasure workflow has to reach into stored telemetry.&lt;/p&gt;

&lt;p&gt;So redact at the producer. It's the only control you fully own, it survives a vendor migration, and it doesn't depend on anyone's roadmap.&lt;/p&gt;

&lt;p&gt;The pattern I've settled on is a pseudonymous reference plus a mapping table in a database I can run a &lt;code&gt;DELETE&lt;/code&gt; against, which turns an erasure request into one row in one place. Whether that fully satisfies a regulator depends on how re-identifiable the remaining metadata is, and I'm not a lawyer — check with yours, because your mileage genuinely may vary here. What I'm confident about is the engineering half: you cannot redact your way out of a payload you already shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buy versus build: what each option actually hands you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;How you integrate&lt;/th&gt;
&lt;th&gt;PII controls&lt;/th&gt;
&lt;th&gt;Per-user erasure&lt;/th&gt;
&lt;th&gt;Ops load&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sentry&lt;/td&gt;
&lt;td&gt;Language SDKs&lt;/td&gt;
&lt;td&gt;Server-side scrubbing, retention settings&lt;/td&gt;
&lt;td&gt;Partial, via account data controls&lt;/td&gt;
&lt;td&gt;Low (SaaS)&lt;/td&gt;
&lt;td&gt;Frontend and mobile teams that need source maps and replay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Datadog&lt;/td&gt;
&lt;td&gt;Agent plus SDKs&lt;/td&gt;
&lt;td&gt;Sensitive data scanner on ingest&lt;/td&gt;
&lt;td&gt;No direct per-user delete&lt;/td&gt;
&lt;td&gt;Low, but agent fleet to manage&lt;/td&gt;
&lt;td&gt;Shops already paying for the wider platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grafana Loki, self-hosted&lt;/td&gt;
&lt;td&gt;OpenTelemetry or Promtail&lt;/td&gt;
&lt;td&gt;Whatever you build in the pipeline&lt;/td&gt;
&lt;td&gt;Chunk-level deletion, coarse&lt;/td&gt;
&lt;td&gt;High — you own storage and retention&lt;/td&gt;
&lt;td&gt;Teams with strict data residency and spare SRE capacity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honeycomb&lt;/td&gt;
&lt;td&gt;OpenTelemetry native&lt;/td&gt;
&lt;td&gt;Redact in the collector&lt;/td&gt;
&lt;td&gt;Not a per-user operation&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Teams whose real question is high-cardinality tracing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrai&lt;/td&gt;
&lt;td&gt;One REST endpoint, no SDK to install&lt;/td&gt;
&lt;td&gt;Redact before you send&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Services that want error capture under the same key as their other backend calls&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;What earns that last row a place in the comparison is the integration column rather than the feature column. The API describes itself: a public discovery endpoint hands back the request schema, the response schema and runnable examples for each capability without a key, so adding error capture to a service is reading one endpoint definition instead of adopting another SDK and its transitive dependencies. For a platform team that already tracks dependency count as an on-call risk, that matters more than most feature checkboxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Go path that keeps the bad fields from ever leaving
&lt;/h2&gt;

&lt;p&gt;Build the event from an explicit struct. Never marshal whatever your context happens to hold — an allowlist is a design you can review, and a dump is a promise you can't keep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"bytes"&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"crypto/sha256"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/hex"&lt;/span&gt;
    &lt;span class="s"&gt;"encoding/json"&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"io"&lt;/span&gt;
    &lt;span class="s"&gt;"net/http"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strconv"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Only the fields we decided are safe to leave the process. UserRef is our own&lt;/span&gt;
&lt;span class="c"&gt;// pseudonymous id, resolvable against a table we control. Check the discovery&lt;/span&gt;
&lt;span class="c"&gt;// entry for the capture capability for the full field set before you extend it.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;errorEvent&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Message&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"message"`&lt;/span&gt;
    &lt;span class="n"&gt;Level&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"level"`&lt;/span&gt;
    &lt;span class="n"&gt;Environment&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"environment"`&lt;/span&gt;
    &lt;span class="n"&gt;Release&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"release"`&lt;/span&gt;
    &lt;span class="n"&gt;Service&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"service"`&lt;/span&gt;
    &lt;span class="n"&gt;RequestID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;            &lt;span class="s"&gt;`json:"request_id"`&lt;/span&gt;
    &lt;span class="n"&gt;Tags&lt;/span&gt;        &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`json:"tags,omitempty"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="n"&gt;errorEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Marshal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// Same event, same key: a retry after a network blip never double-counts.&lt;/span&gt;
    &lt;span class="n"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sum256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;idemKey&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EncodeToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewRequestWithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"https://api.infrai.cc/v1/errors/capture"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Bearer "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INFRAI_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idemKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&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="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;429&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
            &lt;span class="c"&gt;// A 4xx body carries the reason. Surface it, don't swallow it.&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"capture rejected: %d %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&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="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="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"capture: still rate limited after 4 attempts"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryAfter&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strconv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Atoi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryAfter&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;secs&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errorEvent&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;"checkout: payment provider declined confirmation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Level&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"production"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;"checkout-api@a41f9c2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="s"&gt;"checkout-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;RequestID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"01J9F3M2QK7A0R8ZC4V6N1TD5S"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Tags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"route"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;"POST /checkout/confirm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"region"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="s"&gt;"eu-west-1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"user_ref"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"u_9f2c41"&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;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"capture:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&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;Two habits are doing the work there. The idempotency key means a retried capture is the same capture, so a flapping network doesn't inflate the counts your alerting thresholds are tuned against, and the 429 branch honours &lt;code&gt;Retry-After&lt;/code&gt; instead of hammering the endpoint during exactly the minute you can least afford it. Wire the same builder into every entry point — HTTP handler, queue consumer, cron job — and you close the gap that cost me a Friday evening.&lt;/p&gt;

&lt;p&gt;Now the part that doesn't fit on a slide. This whole approach assumes error events are the primary artifact, and for plenty of teams they aren't. If your debugging question is "where did the latency go across nine services", you want a span waterfall, and a store with only trace ID and span ID fields to correlate on isn't the right tool — go to Honeycomb or Tempo. If you need paging with escalation policies, keep PagerDuty or Grafana alerting in the picture, because a query API with no notification routes means writing your own poller and then owning the reliability of that poller. Minified JavaScript stack traces without source map resolution are close to useless, so frontend-heavy teams should stick with Sentry. And if the silent failure you're most afraid of is a job that should have run and didn't, no error tracker covers it — that's a Healthchecks.io-shaped hole, whatever else you buy.&lt;/p&gt;

&lt;p&gt;Pick the metadata first, though. It outlives whichever backend you're on this year.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GDPR Article 17, right to erasure: &lt;a href="https://gdpr-info.eu/art-17-gdpr/" rel="noopener noreferrer"&gt;https://gdpr-info.eu/art-17-gdpr/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sentry data scrubbing and retention: &lt;a href="https://docs.sentry.io/product/data-management-settings/scrubbing/" rel="noopener noreferrer"&gt;https://docs.sentry.io/product/data-management-settings/scrubbing/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Datadog Sensitive Data Scanner: &lt;a href="https://docs.datadoghq.com/sensitive_data_scanner/" rel="noopener noreferrer"&gt;https://docs.datadoghq.com/sensitive_data_scanner/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenTelemetry semantic conventions: &lt;a href="https://opentelemetry.io/docs/specs/semconv/" rel="noopener noreferrer"&gt;https://opentelemetry.io/docs/specs/semconv/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Grafana Loki documentation: &lt;a href="https://grafana.com/docs/loki/latest/" rel="noopener noreferrer"&gt;https://grafana.com/docs/loki/latest/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Infrai discovery endpoint (public, no key required): &lt;a href="https://api.infrai.cc/v1/discovery" rel="noopener noreferrer"&gt;https://api.infrai.cc/v1/discovery&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>observability</category>
      <category>errortracking</category>
      <category>gdpr</category>
      <category>go</category>
    </item>
  </channel>
</rss>
