Error event metadata: user_id, release, request id and the PII line

Which fields belong on an error event, which ones to hash or drop before it leaves your process, and how Infrai's capture and group routes store them.

Attach four things to every error event and you can answer almost any incident question: an environment, a release, a stable fingerprint, and an opaque reference to the affected user. Everything else is a tag. Infrai takes the first three as top-level fields on POST /v1/errors/capture, so the aggregated group can later tell you which release started the bleeding and whether one customer is hitting it or all of them.

The expensive mistake isn’t attaching too little. It’s attaching the whole request object “just in case” — headers, body, session — and finding out eleven months later that a bearer token and a customer’s email address have been sitting in an error store that half the company can read. Article 5 of the GDPR calls that a data minimisation problem, and an erasure request won’t care that it was accidental.

Decide the field list once, put it behind one function, and the rest is mechanical.

The field list, with the PII column filled in

FieldWhat it buys youPII riskWhere it goes
environmentKeeps staging noise out of the production group countsnonetop-level field
releaseAnswers “did the fix ship?” and “is this a regression?“nonetop-level field
fingerprintControls grouping — turns 400 events into 3 problemslow, unless you put an address in ittop-level field
user referenceSeparates “one unlucky tenant” from “everyone”high if it’s an email or a nameuser_id, hashed
request / trace idJoins the error back to the request lognone when you generate it yourselfa tag
route and methodTells you which endpoint is brokennonea tag
raw headers, cookies, bodyOccasionally useful, permanently radioactivesevereleave it out

The user reference is the only genuinely hard call in that table. You want it, because user_count on a group is the difference between a P1 and a Tuesday, and you don’t want the raw identifier, because then your error store becomes a system of record for personal data. A keyed hash settles both: HMAC the internal user id with a pepper your error pipeline holds, truncate it, and send that. Support can still map a complaint to a hash by running the same function; an attacker with a database dump can’t enumerate users from a rainbow table because the pepper isn’t in the dump.

What a capture call carries

export INFRAI_API_KEY="your_infrai_api_key"

curl -sS -X POST "https://api.infrai.cc/v1/errors/capture" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "TypeError: Cannot read properties of undefined (reading \"id\")\n    at applyDiscount (/app/src/checkout.js:42:17)",
    "exception": "TypeError",
    "fingerprint": "checkout:applyDiscount:TypeError",
    "environment": "production",
    "release": "2026.07.4"
  }'

The response is the part worth designing around:

{
  "ok": true,
  "data": {
    "event_id": "evt_err_n9yibA8CLwU90UPX18yKZJwe",
    "fingerprint": "a3ce50dffa080384f612f21c2c75496dc817c4aceaac873df80d5f375a5b2838",
    "error_group_id": "errgrp_ytofEmy7HQhSEwOvvGsld220",
    "is_new_group": true,
    "dashboard_url": "https://infrai.cc/projects/proj_local/errors/evt_err_n9yibA8CLwU90UPX18yKZJwe"
  },
  "metadata": { "request_id": "req_afae89d470fc4e8886578579", "latency_ms": 25 }
}

Two details there earn their keep. The fingerprint that comes back is a SHA-256 digest of the string you sent, not the string itself — so grouping keys stay stable without being readable. And is_new_group is a boolean regression alarm: true on a release you shipped an hour ago is the signal you actually want paging someone, as opposed to the four hundredth occurrence of a known problem.

Every response also carries metadata.request_id. Log it next to your own request id and the two systems are joinable without a correlation service.

Sending the tags and the user reference

curl -sS -X POST "https://api.infrai.cc/v1/errors/message" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "checkout: discount lookup returned null for an active promo",
    "level": "warning",
    "environment": "production",
    "release": "2026.07.4",
    "user_id": "u_9f13c2a4b8e15d07",
    "tags": {"route": "POST /checkout", "request_id": "req_9f2c1", "region": "eu-central"}
  }'

POST /v1/errors/message is the message-level sibling of capture — free, rate-limited, and it takes the same metadata envelope (tags, user_id, level, environment, release) that the capture route documents. Use it for the near-misses you want counted but not paged on.

Tag values need to be bounded. The error index publishes an ERROR_TAGS_HIGH_CARDINALITY code, which is the API’s way of saying that a tag whose value is a full URL with an order id in it is a cardinality bomb, not a tag. Put the id in the message; put route: "POST /checkout" in the tag.

Redact in the process, not in the dashboard

import { createHmac } from "node:crypto";

const API = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
const PEPPER = process.env.ERROR_USER_PEPPER;
if (!KEY || !PEPPER) throw new Error("INFRAI_API_KEY and ERROR_USER_PEPPER must be set");

const EMAIL = /[\w.+-]+@[\w-]+\.[\w.]+/g;
const BEARER = /\bBearer\s+[A-Za-z0-9._-]+/g;
const CARD = /\b\d{13,19}\b/g;

export function scrub(text) {
  return String(text).replace(EMAIL, "[email]").replace(BEARER, "Bearer [redacted]").replace(CARD, "[number]");
}

export function pseudonym(userId) {
  return "u_" + createHmac("sha256", PEPPER).update(String(userId)).digest("hex").slice(0, 16);
}

export async function report(err, { route, release, environment = "production" }) {
  const res = await fetch(`${API}/v1/errors/capture`, {
    method: "POST",
    headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      message: scrub(err.stack ?? err.message),
      exception: err.name ?? "Error",
      fingerprint: `${route}:${err.name}`,
      environment,
      release,
    }),
  });
  if (!res.ok) {
    console.error("error capture failed", res.status, await res.text());
    return null;
  }
  const { data } = await res.json();
  if (data.is_new_group) console.warn("new error group on this release:", data.dashboard_url);
  return data;
}

Scrubbing belongs here, on the way out, because it’s the only place that still knows what the values mean. A regex over err.stack is crude, and it’s also the difference between an incident and a notifiable breach — in our testing the three patterns above catch the overwhelming majority of what leaks into a Node stack trace, since the usual culprit is an interpolated email or token inside an error message somebody wrote by hand.

The pepper lives in your environment, never in the payload.

What the group view hands back

curl -sS "https://api.infrai.cc/v1/errors/group_detail/errgrp_ytofEmy7HQhSEwOvvGsld220" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "error_group_id": "errgrp_ytofEmy7HQhSEwOvvGsld220",
  "title": "TypeError: Cannot read properties of undefined (reading 'id')",
  "count": 2,
  "user_count": 1,
  "first_seen_at": "2026-07-25T15:51:03.867078Z",
  "last_seen_at": "2026-07-25T15:53:57.588640Z",
  "environments": ["staging", "production"],
  "releases": ["2026.07.4", "2026.07.5"],
  "environment_distribution": {"staging": 1, "production": 1},
  "release_distribution": {"2026.07.4": 1, "2026.07.5": 1}
}

That’s the payoff for the discipline. release_distribution tells you the version the problem entered on, environment_distribution tells you whether it survives outside staging, user_count sizes the blast radius, and none of it required storing a name. A group whose releases array contains exactly one version is usually a fresh regression; a group spread evenly across five is an old friend.

Check what actually got stored — the field you never verified is the field that ships a customer’s email to a vendor:

curl -sS "https://api.infrai.cc/v1/errors/get/evt_err_n9yibA8CLwU90UPX18yKZJwe" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}" | python3 -m json.tool

Retention, residency and erasure

Now the limitations, because a page that only sells doesn’t help you pass a review.

The errors namespace has no delete or redact route — it’s capture, message, list, search, get, groups, group_detail, events and resolve. If your data protection process has to reach individual events on request, that’s a real gap, and the workable design is the pseudonym above: rotate the pepper and the historical hashes stop resolving to anyone. Capture is served from both western and China regions, so where the data lands follows the key you use; if you have a contractual EU-only residency clause, confirm that before you route production traffic.

If what you need is the vendor doing the redaction for you, stick with a platform built around it. Sentry’s server-side scrubbing runs by default and takes configurable rules, and Datadog’s sensitive data scanner works across its logs and error products. Rollbar sits in the same category. Those are better answers than “we regex it ourselves” when an auditor is asking who guarantees the redaction.

What the metadata costs

Captured events are billable at $0.00005 per call — verified 2026-07-25 — and every read route (list, search, get, groups, group_detail, events) is free and rate-limited. New accounts start with $2 free credit, which is roughly 39,999 captures before you’ve paid anything. Get today’s number rather than trusting this paragraph:

curl -sS "https://api.infrai.cc/v1/discovery" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

Rates here move down, not up, and discount campaigns run — so what you read may well be cheaper than what’s printed above. The structural fact is the durable one: writes are metered per event, reads aren’t, and adding tags doesn’t change the price of a capture. Nothing about your metadata policy is a cost decision.

RequirementSentryDatadogInfrai errors
Vendor-side scrubbing rulesBuilt in, on by defaultSensitive data scannerYou scrub before sending
Stack frames rendered with source mapsYesYesNot today — the message field holds the text
Grouping key you controlYesYesYes, via fingerprint
Per-event deletion APIYesYesNo
Same key also does logs, queue, cron, emailNoPartlyYes

That last row is the honest reason to be here. If error tracking with deep frame-level tooling is the only thing you’re buying, Sentry is the better product and you should buy it. If the error store is one surface among the queue, the cron job and the transactional email that the same feature needs, then one credential and one bill start to matter more than the dashboard.

References

Browse more errors developer guides