LW IT Solutions
« Blog Overview /Data Privacy/Tutorials / Tutorial: Finding Personal Data in GA4 Page...

Tutorial: Finding Personal Data in GA4 Page Paths and Removing It

Tutorial: Finding Personal Data in GA4 Page Paths and Removing It
Contents
  1. What Counts as Personal Data in a Page Path
  2. Pulling the Page Paths Out of the Property
  3. The Patterns Worth Scanning For
  4. Redacting at the Stream Rather Than in the Tag
  5. The Data That Is Already Stored
  6. What the Scan Cannot See
  7. Sources

Personal data does not arrive in an analytics property through a decision. It arrives because a form submits over GET instead of POST, because a password reset link carries an address, or because a search box writes what was typed into the URL.

Nothing in the interface reports it. The page path is just a dimension, and a path containing an email address looks like any other path in a list of four thousand. Finding it takes one query and six patterns; getting rid of it takes two separate measures, because the leak and the stored data are different problems.

A URL containing an email address travelling from the browser through the tag and the data stream into the property and the report, with the two points at which it can be redacted marked, beside a table of scan findings
Two of the five stations can still remove the value. Past the fourth it is stored, and only a deletion request changes that.

What Counts as Personal Data in a Page Path

The obvious cases are email addresses and phone numbers. The less obvious ones cause more trouble, because they are not recognised as personal at all.

An order number is personal data as soon as it can be resolved to a person in another system, and it usually can. A password reset token is not personal data but is a secret, and a secret in an analytics report is available to everyone with read access to the property. A search term is personal data whenever people type their own name or address into the search box, which on a shop with an order lookup happens daily.

What all of them have in common is that they arrive through the URL, and the URL reaches GA4 in full. Everything after the question mark becomes part of the page dimension, and the fragment after the hash is the only part that never leaves the browser.

Pulling the Page Paths Out of the Property

The Data API returns the same dimensions the reports use, without the row limits of the interface. One request over a long window brings back everything that was ever recorded.

curl -X POST \
  "https://analyticsdata.googleapis.com/v1beta/properties/123456789:runReport" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "dateRanges":  [{ "startDate": "395daysAgo", "endDate": "today" }],
    "dimensions":  [{ "name": "pagePathPlusQueryString" }],
    "metrics":     [{ "name": "screenPageViews" }],
    "limit": 100000,
    "orderBys": [{ "metric": { "metricName": "screenPageViews" }, "desc": true }]
  }'

Three details decide whether the result is complete. The dimension has to be the one that includes the query string – pagePath alone strips exactly the part where the data usually sits. The date range should reach back as far as the property retains data, because a leak that was fixed six months ago is still in the stored rows. And a property with many paths needs paging: the response carries a row count, and everything beyond the limit needs a second request with an offset.

A note on quota. The API counts tokens per property and per day, and a query over four hundred days with a hundred thousand rows is not free. Running it once a quarter is sensible; running it in a loop while developing exhausts the daily allowance in an afternoon.

The Patterns Worth Scanning For

Six patterns cover almost every finding, and they are worth applying to both the raw and the decoded form of the path, because an address arrives as %40 as often as it does as @.

import re, urllib.parse

MUSTER = {
    "mail":       re.compile(r"[\w.+-]+@[\w-]+\.[\w.]{2,}"),
    "mail_kod":   re.compile(r"[\w.+-]+%40[\w-]+"),
    "telefon":    re.compile(r"(?:\+|00)\d{7,15}|\b0\d{8,13}\b"),
    "schluessel": re.compile(r"[?&](?:token|hash|auth|key|sig|pwd|password)=", re.I),
    "namensfeld": re.compile(r"[?&](?:name|vorname|nachname|first|last|user)=", re.I),
    "suche":      re.compile(r"[?&](?:q|s|search|query)=", re.I),
}

def pruefe(pfad):
    formen = {pfad, urllib.parse.unquote(pfad)}
    return {name for name, m in MUSTER.items() if any(m.search(f) for f in formen)}

The last two need a second look before anything is reported. A parameter called name often carries a product name rather than a person, and a search parameter is only a problem if people put personal data into it – which is settled by reading twenty of the actual values, not by the parameter name.

The phone pattern produces the most false positives of all: order numbers, article numbers and dates of the form 20260920 all match a long run of digits. Narrowing it to the parameters it appears in, rather than to the whole path, removes most of that.

Redacting at the Stream Rather Than in the Tag

With the findings in hand, the leak is closed at the earliest point that is under control. GA4 offers that at the data stream itself, and it applies to everything the stream receives regardless of which tag sent it.

The setting sits in the administration under the web data stream, in the tag settings, as Redact data. It has two parts: an automatic removal of anything shaped like an email address, and a list of query parameter names whose values are replaced before the hit is stored.

token, hash, auth, key, sig, pwd, password,
email, mail, e-mail, user, username,
phone, tel, mobile,
name, firstname, lastname

This list belongs in the same place as the scan results, and it should be re-checked whenever the scan finds a new parameter. Redaction is exact: a parameter that is spelled differently on one page – e_mail rather than email – passes through untouched.

Two remarks on where this sits relative to the other options. Overwriting page_location in the tag manager works too and catches the value earlier, but only for the hits that particular tag sends; a second tag, or a hard-coded gtag call somewhere in the theme, bypasses it. Scrubbing in a server-side container is the strongest option and the only one that also protects the onward transfer to advertising platforms, and it is a separate piece of work with its own article. The stream setting is the one that takes ten minutes and covers everything.

The Data That Is Already Stored

Redaction applies to hits from now on. It changes nothing about the rows already in the property, and those are the ones that matter for a data protection question.

What removes them is a data deletion request, in the administration under the property. The relevant type deletes selected parameters across all events, and the page location is one of those parameters.

Three properties of that mechanism are worth knowing before it is triggered. It has a pending window of several days during which it can be withdrawn, which is deliberate and useful. Once running, it takes up to about two months to be applied throughout. And it is irreversible – the page dimension for that period is gone afterwards, including for the paths that were fine, if the request was scoped to the whole parameter.

The one thing the request does not touch is the BigQuery export. Data exported before the deletion sits in a dataset the property no longer controls, and it has to be removed there separately – which is a DELETE or a rebuilt table, and it needs to happen for the same period.

UPDATE `projekt.analytics_123456789.events_*`
SET event_params = ARRAY(
      SELECT AS STRUCT
        p.key,
        IF(p.key = 'page_location',
           STRUCT(REGEXP_REPLACE(p.value.string_value,
                  r'([?&](?:email|token|phone)=)[^&]*', r'\1[entfernt]')
                  AS string_value, p.value.int_value,
                  p.value.float_value, p.value.double_value),
           p.value) AS value
      FROM UNNEST(event_params) AS p)
WHERE _TABLE_SUFFIX BETWEEN '20260101' AND '20260920';

What the Scan Cannot See

Three places hold the same kind of data and do not appear in this report.

The first is event parameters other than the page location. A form tracking setup that sends the entered value as a parameter puts it into a custom dimension, and the scan above never looks there. The same Data API query with the custom dimension in place of the page path finds it, and that query is worth running for every registered dimension once.

The second is the user ID field. A property configured to receive a user ID and given an email address instead is a textbook violation, and it is invisible in reports because the field is not shown in most of them. The check is the same as everywhere: look at the actual values, not at the field name.

The third is everything that has already left. If the hits also went to an advertising platform, then redacting in GA4 changes nothing about the copy that arrived there, and that platform has its own deletion path. The list of destinations is worth writing down before the first deletion request, because the request itself does not produce it.

Lukas Wojcik

Lukas Wojcik

Systems architect and technology enthusiast specializing in scalable tracking solutions, GMP Stack (GA4 & GTM), and robust backend architectures. Advocate for clean code and privacy-first design.

Get in Touch

Briefly describe your project or inquiry for a tailored response. This site is protected by reCAPTCHA.

Write a comment

The email address is not published. Required fields are marked with an asterisk.

ALL ARTICLES & CATEGORIES

CCTV

Follow this category by RSS

Cloud & AI

Follow this category by RSS

Data Privacy

All 13 articles in this category Follow this category by RSS

Digital Analytics

All 50 articles in this category Follow this category by RSS

Digital Marketing

All 30 articles in this category Follow this category by RSS

IT & Networks

All 16 articles in this category Follow this category by RSS

Music Production

Follow this category by RSS

Raspberry PI

Follow this category by RSS

Smart Home

All 18 articles in this category Follow this category by RSS

Web Development

Follow this category by RSS

WordPress Plugins & Tricks

Follow this category by RSS