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.
Top comments (0)