Filters & scrubbing
For agents:
get_project(org="acme", project="web")showsstoreIpandfilters;update_project(org="acme", project="web", filters={dropCrawlers:true, messageDenyList:["ResizeObserver loop"]})changes them (project:write). Filtered events appear inget_usageasfilteredoutcomes with a reason, never as issues.
Inbound filters
Filters drop events cheaply and record a filtered usage outcome with a reason. They never count against your quota and never create issues. Configure them per project via PATCH /v1/orgs/{org}/projects/{project} {"filters": {…}} (merged with the existing settings):
| Setting | Default | Stage | Drops when |
|---|---|---|---|
dropCrawlers |
on | request | User-Agent matches known bots, headless browsers, uptime checkers, SEO crawlers (reason crawler) |
dropLegacyBrowsers |
off | request | IE ≤ 11 or the old Android stock browser (legacy-browser) |
allowedDomains |
[] |
request | The Origin/Referer host is not one of the listed domains (*.example.com allowed). Requests without either header — server SDKs — always pass (disallowed-domain) |
dropLocalhost |
off | event | request.url host or server_name is localhost, 127.x, or ::1 (localhost) |
dropBrowserExtensions |
on | event | The throwing frame's URL is a chrome-extension://, moz-extension://, safari-extension://… script (browser-extension) |
messageDenyList |
[] |
event | Any of up to 50 case-insensitive patterns (RE2 syntax) matches the message, log entry, or an exception value (message-deny-list). Matched against the first 4096 characters |
Request-stage filters run in the ingest Worker before the payload is parsed; event-stage filters run in the processor. Both read their settings from the edge config cache, so a change takes up to about a minute to apply everywhere — and a change that enables a filter applies sooner than that, because an absent config is re-checked every few seconds rather than every minute.
If that cache cannot be read at all, filters fail open: the event is stored rather than dropped. By the time the processor sees an event its body is already durable and the event is already counted, so the alternative would be discarding a real error because a settings lookup came back empty.
PII scrubbing
Scrubbing runs in the processor before an event is written to the record you read, on every error event and transaction, and cannot be turned off. The request as it arrived is held first in a 7-day replay buffer (raw/), so that a failure in processing never loses an event. It is read only to process or reprocess an event; the dashboard and API never return its contents. Scrub in your SDK as well (beforeSend) for anything that must never be stored at all.
Scrubbing is key-based plus pattern-based:
Sensitive keys (case-insensitive, anywhere in request, extra, tags, contexts, breadcrumbs, user): password, passwd, pwd, secret, token, api_key, apikey, access_token, auth, authorization, credentials, cookie, set-cookie, card* (prefix), ssn, stripetoken, mysql_pwd. Values are replaced with [Filtered].
Matching is per key part, so compound names resolve the way you would expect: x-auth-token, refresh_token, client_secret, accessToken and x-api-key are all filtered, as are plurals (cookies, tokens) and camelCase. It is deliberately not substring matching on the whole key — that would take author with it, and this product uses author for the suspect commit in fix context.
Two consequences worth knowing:
request.cookies, the dedicated field in the Sentry request interface, is filtered as a whole — not only theCookieheader beside it.- The
card*wildcard over-matches (cardinalityis filtered along withcardholder). The trade is one-sided and deliberate: masking a metric costs a number nobody reads twice, and leaking a card number costs a customer.
Deeply nested data stops being walked past 32 levels and is replaced with [Filtered: nesting too deep] rather than passed through. Nothing that deep is readable in the UI, and a scrubber that quietly gives up is worse than none at all.
Query strings are scrubbed by parameter name in both request.query_string and request.url, since the sensitive name is data there, not an object key.
Patterns (applied to every string, including messages, exception values, and frames): 13–19 digit sequences that pass the Luhn check (card numbers; epoch-millisecond timestamps do not) and -----BEGIN … PRIVATE KEY----- blocks become [Filtered]. Messages and stack traces are otherwise left intact — masking whole traces would destroy the product.
Client IP: user.ip_address is deleted unless the project has storeIp: true. The flag is off by default and is the only opt-in in the scrubber. Only that field is treated as an address: an IP your SDK sends elsewhere, such as an X-Forwarded-For request header or a tag, is kept.
SDK-side scrubbing (beforeSend, send_default_pii=False) still applies and runs first; Bugwatch's pass is a backstop.
No PII in analytics
The analytics index (Analytics Engine) receives only ids, low-cardinality dimensions (level, environment, release, platform, country), and user_hash — the first 16 hex characters of sha256("<project_id>:<user id, email, or username>"). Raw identities, emails, messages, and bodies never enter it, which matters because that store is append-only with no per-row deletion. Event bodies live in R2 under the project's prefix and expire with the project's retention tier (Security & privacy).
Deny-list patterns use RE2 syntax
Deny-list patterns are regular expressions run on a linear-time engine (RE2 semantics): matching takes time proportional to the length of the message, whatever the pattern. No pattern can stall event processing, so none is refused for its shape. ^(\w+\s?)+$, which takes seconds on a JavaScript engine against a 26-character message that fails to match, takes about a millisecond here.
The syntax is the familiar one — literals, ., classes like [a-z] and \d\w\s, anchors ^ $ \b, groups, alternation |, and repeats * + ? {n,m} — with two features left out because they need backtracking: lookaround ((?=…), (?!…), (?<=…), (?<!…)) and backreferences (\1). A pattern that uses them is refused when you save it, with an error saying why. Matching is case-insensitive; (?-i) inside a pattern turns that off for the rest of that pattern only.
Patterns are matched against the first 4096 characters of each candidate string, which is far more than any error message needs.