RE2 vs PCRE: Every Regex Construct That Silently Breaks in GA4 (and the Fix)
If your regex works in JavaScript but returns zero rows in GA4, RE2 is almost certainly the reason. GA4 (and BigQuery, Go, Rust) runs Google's RE2 engine, which is linear-time but rejects several PCRE families entirely.
If your regex works in JavaScript but returns zero rows in GA4, RE2 is almost certainly the reason. GA4 (and BigQuery, Go, Rust) runs Google's RE2 engine, which is linear-time but rejects several PCRE families entirely.
Here's the complete list and the fix for each.
What RE2 rejects
| PCRE construct | Example | RE2 verdict | Replacement |
|---|---|---|---|
| Lookahead | foo(?=bar) |
rejected | Capture the suffix: (foo)bar
|
| Negative lookahead | (?!admin) |
rejected | Negated class or exclude filter |
| Lookbehind | (?<=/)(\w+) |
rejected | Capture group: /(\w+)
|
| Backreference | `(foo\ | bar)\1` | rejected |
| Atomic group | (?>a+)b |
rejected | Plain group (a+)b
|
| Possessive quantifier | a++ |
rejected | Greedy a+ (RE2 is linear anyway) |
| Recursion | (?R) |
rejected | Unroll the recursion |
| Named groups (some) | (?P<name>...) |
partial | Use numbered groups (...)
|
\w with Unicode |
\w+ (Unicode) |
ASCII only |
[A-Za-z0-9_] or use Unicode classes where supported |
The three that bite most
1. Lookahead in GA4 segments
You want "page paths containing /blog/ but not /blog/admin/". In PCRE: .*/blog/(?!admin).*. In RE2 this is rejected silently — GA4 returns zero rows, no error.
Fix: Use GA4's exclude filter, or restructure: .*/blog/[^a].* (rough, but RE2-safe). For exact exclusions, use two filters: include .*/blog/.* AND exclude .*/blog/admin/.*.
2. Backreferences in event names
You want repeated words: (\w+)\s+\1. RE2 rejects this.
Fix: Expand: (\w+)\s+\w+ won't match the same word. If you need the same word twice, list them: hello hello|world world.
3. Non-greedy .*?
.*? works in RE2 (it's supported), but in GA4's full-match semantics it's usually pointless — GA4 matches the entire value anyway, so greedy .* is what you want.
The RE2 mindset
-
Full match by default. GA4 matches the entire value. Wrap substring patterns in
.*on both sides. - No lookahead/lookbehind. Restructure with capture groups or use exclude filters.
- No backreferences. Expand or use multiple patterns.
- Test in RE2, not JS. JavaScript RegExp accepts constructs RE2 rejects. Always verify in an RE2 engine (Go's regexp, Rust's regex, or a tester that flags RE2 compatibility).
Free tooling
I built RegexLab — a free, browser-only regex tester that:
- Shows match/no-match per line
- Flags RE2-incompatible constructs
- Has a pattern library with 52 tested patterns (dates, URLs, UUIDs, IPs, UTM parameters, log levels) — each with an RE2-compatibility note
- Has a dedicated RE2 vs PCRE page with the full construct list and replacements
Nothing leaves your browser. The whole thing runs on client-side JavaScript RegExp.
TL;DR
- GA4 = RE2 engine. No lookahead, no backreferences, no atomic groups.
- Restructure with capture groups, exclude filters, or expanded alternation.
- Test in RE2 before shipping. JavaScript-valid ≠ GA4-valid.
What's the RE2 incompatibility that cost you the most debugging time? Mine was a lookahead that matched everything in preview and nothing in production.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.