Skip to content

Latest commit

 

History

7 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

google-goto-wrapper-study

English · Deutsch

Languages. This README and the German one are equivalent in substance — every number in both is produced by the same scripts. Everything else is single-language:

Component Language
README.md · README.de.md English · German, both fully maintained
Data in data/*.csv (field names and values) English
Figures in docs/img/ generated in both languages from the same data
Lab notebook data/raw/ergebnisse.md German only — primary source, unaltered
Measurement protocols in protocol/ German only
Code in analysis/ English identifiers, German comments
Evidence captures in docs/img/ German — screenshots of a German browser on German SERPs

If you do not read German: everything needed to understand or reuse the data is in this file. The CSV schema below defines every field and value; the Design and Reproduction sections describe the setup in full. The German protocols add hands-on steps, not definitions — nothing in them changes how the data should be read. The lab notebook is included for provenance, so that the CSVs can be traced back to what was written down during collection.

Controlled measurement of Google's google.com/goto link wrapping on German search result pages. 216 classified link positions across 12 runs, plus a depth check of 11 wrapped links against served HTML and rendered DOM.

This repository holds the raw data, the measurement protocols and the analysis scripts. Every figure quoted below is computed from the CSVs: the key figures by analysis/summarize.py, the p-values by analysis/inference.py. Nothing is entered by hand.


Key findings

# Finding Evidence
1 Wrapping is governed by login state, not by the debug port. Logged out: 108 of 108 links wrapped. Logged in: 0 of 108. observations.csv
2 The remote-debugging port has no discriminative power. 50 % wrapped with the port open, 50 % without — exactly the base rate. observations.csv
3 AI Overview citations are not governed separately. Across 36 SERPs, AIO sources and organic results always fell into the same state. Not one deviation. observations.csv
4 The destination URL stays on the page. In 11 of 11 wrapped links checked, the full URL including path was present in both the initial source and the DOM. dom_check.csv
5 The code is randomised per request: 90 observed goto codes, 90 of them unique. Not one repeated. observations.csv
6 It still leaks the length of the destination URL: payload = URL length + 56 bytes, in 10 of 10 resolvable pairs. dom_check.csv

Share of wrapped links by factor level


Zusammenfassung auf Deutsch

Kontrollierte Messung des google.com/goto-Link-Wrappings auf deutschen Google-Suchergebnisseiten, erhoben in einem 2×2×2-Plan aus Browser, Anmeldestatus und Debug-Port, ergänzt um vier Durchgänge zum Fenstertyp.

  • Der Anmeldestatus ist der einzige Faktor, der die Daten trennt. Abgemeldet: 108 von 108 Links gewrappt. Angemeldet: 0 von 108. Der Debug-Port — die in SEO-Kreisen kursierende Erklärung — hat keinerlei Trennschärfe (50 % gewrappt in beide Richtungen, also der Grundanteil).
  • AI-Overview-Quellen werden nicht gesondert behandelt. In 36 von 36 Suchergebnisseiten fielen AIO-Quellen und organische Treffer in denselben Zustand.
  • Das Wrapping entfernt die Ziel-URL nicht. In 11 von 11 geprüften gewrappten Links stand die vollständige Adresse samt Pfad sowohl im ausgelieferten HTML als auch im fertigen DOM.
  • Der Code ist pro Anfrage zufällig (90 von 90 eindeutig), verrät aber die Länge der Ziel-URL (Nutzlast = URL-Länge + 56 Byte, 10 von 10 Paaren).

Rohdaten, Protokoll und Auswertungsskripte liegen in diesem Repository. Daten unter CC BY 4.0, Code unter MIT. Vollständige deutsche Fassung: README.de.md.


Research question

Since summer 2026, SEO communities have reported that Google replaces destination addresses in search results with a redirect through google.com/goto?url=CAES…. The actual target is then no longer readable in the href attribute.

Two questions were open and are measured here:

  1. Under what conditions does the wrapping occur? Three explanations circulated in the threads: an open debug port acting as an automation signal, the browser, or the window type ("use incognito and you'll see it").
  2. Does it also affect the source links in the AI Overview, or only the organic results below? No public analysis of this existed at the time of measurement.

A third question came from a comment thread where someone asked — and was never answered — whether the real destination URLs are still present in the DOM. Part B answers it.


Design

Full 2×2×2 factorial, each cell occupied once:

Factor Levels
Browser Chrome 152.0.7977.75 · Firefox 153.0
Login state logged out · logged in
Debug port closed · open (--remote-debugging-port)

Plus four additional runs on window type (incognito or private window versus the real everyday profile), to test the circulating incognito claim in a paired fashion.

Each of the 12 runs used the same three search queries. Per query, six link positions were classified: the first three organic results and the first three AI Overview sources.

12 runs × 3 queries × 6 link positions = 216 observations

Controls. Every run in the main measurement used a fresh, empty browser profile. Cookie choice was "reject all" throughout. No VPN, verified via ip a. Same machine, same connection (Vodafone DSL, Bavaria), all measurements within a single day.

Classification. "Wrapped" means the copied link address contains a CAES… code instead of the destination URL. "Direct" means the destination URL is readable in plain text — including when it passes through the long-standing click tracker google.com/url?sa=t&…&url=https://…. This boundary is deliberately conservative and affects only the logged-in Firefox runs.


Results

1 · Login state separates the data completely

Browser Login Port organic AIO sources
Chrome logged out closed 18/18 18/18
Chrome logged out open 9/9 9/9
Chrome logged in closed 0/18 0/18
Chrome logged in open 0/9 0/9
Firefox logged out closed 18/18 18/18
Firefox logged out open 9/9 9/9
Firefox logged in closed 0/18 0/18
Firefox logged in open 0/9 0/9

Wrapped links out of possible. The separation is complete and identical in both browsers. The debug port changes nothing within any cell.

On the unit of analysis. The 216 observations are not independent: login state was set once per run, not once per link. The defensible test therefore operates at run level. analysis/inference.py shows what the choice of unit does:

Unit of analysis n p (Fisher, two-sided)
Run — defensible 12 2.2 × 10⁻³
Search result page 36 2.2 × 10⁻¹⁰
Individual link — over-optimistic 216 3.5 × 10⁻⁶⁴

61 orders of magnitude apart, same data. The valid value is p = 0.0022, and it is simultaneously the smallest value a 12-run design can reach at all: under complete separation it is determined solely by the number of runs. A smaller p would have to be bought with more runs, not with more links read per run.

The "108 of 108" phrasing in this README is therefore descriptive, not the sample size of a test.

Window type is ruled out as an explanation: the main measurement used no incognito windows at all, only fresh profiles — and reproduced the same effect. In the additional runs, window type coincided with login state (private = logged out, everyday profile = logged in) and cannot be separated there. The advice "use incognito" describes a real difference but names the wrong cause.

2 · AI Overview sources follow the organic results

Across all 36 evaluated SERPs there was not a single case in which AIO sources and organic results were treated differently. Both blocks are either wrapped together or direct together.

This suggests the wrapping is set at response level rather than per module. In practice: checking the state on the organic block also checks the AIO citations.

3 · The wrapper does not remove the destination URL

Part B examined 11 wrapped links and searched for the destination domain in the initial source (Ctrl+U, i.e. the HTML served before JavaScript runs) and in the finished DOM (developer tools, Elements tab). The wrapper was not resolved here — only what is present on the page itself was checked.

Count
wrapped, destination URL in initial source 11 / 11
wrapped, destination URL in DOM 11 / 11
wrapped, destination URL nowhere to be found 0 / 11

In every case the full address including path was present on the same page. The wrapper obscures exactly one location: the href attribute of the result link — that is, what the status bar, hover and "copy link address" display.

4 · The code rotates, the length does not

All 90 recorded goto codes are unique. At 13 link positions that showed the same payload length — and therefore the same destination URL length — across several runs, the code differed every time. No code-to-URL lookup table can be built from this.

The length, by contrast, is unprotected. The code is Base64 over a protobuf structure; the length of the payload field can be read without decryption and stands in a constant relation to the destination URL:

Payload against destination URL length

payload in bytes = length of destination URL in characters + 56

Holds in 10 of 10 pairs where code and resolved URL are both available. Cross-check against the main measurement: estimating URL length from the wrapped codes and comparing against URLs actually observed on the same query in the logged-in runs, 87 of 90 estimates match (97 %). The three deviations sit at positions where the ranking demonstrably shifted between runs.

5 · The token is not bound to the session

Added the same day: 18 wrapped codes from the logged-out runs (D2, D3, D4, Z1, Z3) were resolved against google.com/goto?url= — no cookies, no session, from a different client than the one they were issued to.

Count
HTTP 302 with Location header 18 / 18
URL-length prediction (payload − 56) correct 18 / 18

Three consequences. First, the token is portable and still valid hours after issuance — it is bound to neither cookie, session nor browser. Second, codes from the Firefox runs also resolve at the Chrome endpoint, so the format is demonstrably just an envelope and the payload is interchangeable across browsers. Third, the 56-byte rule is confirmed here against the server's own response, independently of Part B — bringing it to 28 confirmed cases.

Practical note for reimplementation: HEAD returns HTTP 200 with HTML at this endpoint; only GET returns the 302 with Location. Checking with curl -I will not show the redirect.

Collected with analysis/resolve_wrapper.py, recorded in data/redirect_check.csv. Deliberately run as a sample with a delay between requests.

6 · AI Overviews mostly cite outside the top 3

Evaluated across the 18 search result pages where destination URLs were available in plain text: of 54 AIO sources, 25 (46 %) were identical to one of the three recorded organic results, 29 (54 %) were not. In none of those 18 pages did all three AIO sources come from the top 3.

The caveat that belongs with the number: only the first three organic results were recorded. "Outside the top 3" therefore does not mean "outside page one". The narrower claim still stands: AIO source selection is not the result list in miniature.

7 · Evidence capture

Search result page while logged out, goto link in the status bar

Chrome incognito window, logged out (note the "Anmelden" sign-in button top right), query wie funktioniert eine wärmepumpe. The cursor rests on the first AI Overview source; the status bar shows google.com/goto?url=CAESpgEB6zswFeJ01…. That exact code is recorded in data/dom_check.csv as the AIO source of the first query, and it is the one for which the full destination URL was found in both source and DOM.

The bookmarks bar is redacted because it contained private folder names. Nothing else in the image is altered.

One token, three confirmations

The same figure collects three routes to one token's destination: status bar, HTML and server response. Note that the first two check the delivered page and are not independent of each other — the DOM is built from the HTML. The server response is the independent one. The token in the capture and the one in the curl call are character-identical and can be checked against each other. The capture and the labels in it are German: it shows a German browser on a German SERP, and it is reproduced unaltered rather than restaged in English.

8 · Secondary findings

Signing in with an open debug port is blocked in Firefox. In run D8, Google refused the login with "This browser or app may not be secure". Reproduced with a second, independent account. Chrome with an open port was unaffected. After signing in without the port, the session persisted even with the port subsequently opened — so the check applies at the login step, not continuously.

This yields an indirect chain rather than the assumed direct effect: the open port does not cause wrapping, it prevents login in Firefox — and the logged-out state then causes the wrapping.

CAPTCHAs appeared only in logged-out runs (D1, D2, D4), but not in all of them: Z1 and Z3 were also logged out and stayed clear. No CAPTCHA appeared in any logged-in run.

Firefox uses a different wrapping format than Chrome. The CAES payload is identical, the envelope is not:

State Format
Chrome, logged out google.com/goto?url=CAES…
Firefox, logged out, organic google.com/url?sa=t&…&url=CAES…
Firefox, logged out, AIO google.com/url?sa=i&…&url=/goto?url%3DCAES… (doubly nested)
Firefox, logged in google.com/url?sa=t&…&url=https://… (plain text)

This matters methodologically: a test that only searches for the string goto? returns a false negative for the logged-out organic block in Firefox. Part of the contradictory reporting in the threads likely traces back to this.

The goto? counter in the source is not an intensity measure. It is sharply discriminative as a yes/no indicator (0 versus >0) but scales with the number of links on the page, not with the strength of the wrapping: 318–347 hits on the heat-pump query versus 45–47 on the roofer query, each stable across all runs.


What these data do not show

Stated in full, so the reach of the claims is clear:

  • One location, one connection, one day. All measurements from Bavaria over Vodafone DSL within a single day. Regional, provider-side or temporal effects are not ruled out.
  • Three queries in the main measurement, three more in Part B. All German, all informational or commercial-investigation intent. Transactional, navigational and English-language queries are missing.
  • Each cell occupied once. No repeat measurement of the same cell, hence no statement about stability over time.
  • One Google account in the logged-in runs, a second only for the login blocking test. Account age, type and history were not varied.
  • The sequence test ran in the wrong direction. Ten consecutive queries stayed at 0 throughout while logged in. The genuinely interesting reverse case — does a logged-out state switch back to direct after prolonged use? — was not measured.
  • No causal account of the mechanism. The data show what the wrapping depends on, not why Google deploys it.
  • Manual collection. Classification by reading and copying by hand, recorded in a lab notebook. No automated crawling.

Reproduction

Standard library only, no installation step:

python3 analysis/parse_notebook.py   # notebook -> CSV
python3 analysis/summarize.py        # every figure in this README
python3 analysis/inference.py        # significance at three units of analysis
python3 analysis/make_figures.py     # the SVG figures, English and German

parse_notebook.py rewrites the CSVs from the notebook; the files in the repository are produced exactly this way. Two scripts have extra requirements: make_evidence_figure.py (evidence chain, needs Pillow) and make_content_figures.py (factor matrix and format comparison as standalone figures for reuse, not embedded in this README). The notebook under notebooks/ needs pandas, seaborn and matplotlib; the scripts above do not.

resolve_wrapper.py is deliberately not part of that sequence: it makes network requests to Google and overwrites data/redirect_check.csv. How long an issued token stays valid was not measured — only validity across a few hours on the day of measurement is documented.

To run your own collection: protocol/MESSUNG-ANLEITUNG.md contains the full protocol of the main measurement, protocol/ZUSATZMESSUNG.md that of the two additional parts. Both are in German.


Structure

Language markers: [en] English only, [de] German only, [en/de] both.

README.md · README.de.md          [en/de]
data/
  raw/ergebnisse.md    [de]       lab notebook, unaltered. Primary source of the
                       manual measurement; the three CSVs below are derived from it.
  observations.csv     [en]       216 rows, one per link position
  runs.csv             [en]       36 rows, one per run × query
  dom_check.csv        [en]       11 rows, Part B with the destination URLs found
  redirect_check.csv   [en]       18 rows, log of the server responses. The only
                       file outside the notebook, written by resolve_wrapper.py.
analysis/
  parse_notebook.py    notebook -> CSV
  summarize.py         key figures
  inference.py         Fisher exact test, three units of analysis
  resolve_wrapper.py   resolves goto tokens, checks status and length
  make_evidence_figure.py  evidence chain figure
  make_figures.py      SVG figures of both READMEs, English and German
  make_content_figures.py  factor matrix and format comparison
notebooks/
  01-exploration.ipynb  exploratory analysis with pandas and seaborn
protocol/              [de]       measurement protocols as used during collection
docs/img/
  fig1-factors.svg     [en]       generated figures of this README
  fig2-payload.svg     [en]
  fig1-faktoren.svg    [de]       same data, labels for README.de.md
  fig2-nutzlast.svg    [de]
  beleg-*.png          [de]       evidence captures, German browser and SERP
  factor_matrix.svg · chrome_vs_firefox_format.svg
                       [de]       standalone figures, not embedded in a README

Schema of observations.csv

Field Values
run_id D1D8 main measurement, Z1Z4 addendum part A
browser chrome, firefox
profile_type fresh_profile, private, daily_profile
logged_in yes, no
debug_port open, none
slot organic, aio_source
position 1–3
state wrapped, direct
goto_code CAES…, where recorded
payload_bytes length of the protobuf payload field
target_url destination URL, where readable in plain text

License

Split deliberately, because code and data get reused differently:

  • Data, protocols and documentation: CC BY 4.0 — reuse explicitly welcome, attribution required.
  • Scripts under analysis/: MIT — no friction for further development.

Citation

See CITATION.cff. Anyone reusing the figures should state the measurement period: the results describe the state as of 2 September 2026 and rest on a design without repeat measurement.

Contributing

The most useful contribution is a counter-measurement from other regions, providers and account types following the protocol in protocol/. An issue containing a filled-in notebook in the format of data/raw/ergebnisse.md is enough; the analysis then runs through the same scripts.

About

Kontrollierte Messung des google.com/goto-Link-Wrappings auf deutschen Google-Suchergebnisseiten. Rohdaten, Messprotokolle und Auswertungsskripte, erhoben am 2. September 2026.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages