A landing page can load successfully while its redirect loses the click data. Test the complete arrival path with a mixed-case marker, find the first hop that changes it, then verify the measurement tag separately. A final HTTP 200 alone proves neither attribution nor a conversion.

This guide covers GCLID and campaign-query transport. It also explains why a GCLID missing from a later thank-you-page URL does not, by itself, establish that the original click information was lost.

1. Identify which boundary failed

ObservationWhat to inspectWhat it does not prove
Arrival redirect drops or changes the test GCLIDCDN, web-server, CMS or browser redirect ruleBot traffic or a Google billing error
Arrival query survives, but expected collection is absentTag execution, permitted consent state, errors and storageThat another query parameter will repair the tag
Thank-you URL has no GCLIDWhether click information was captured earlier and remains availableThat every page must repeat the identifier in its URL
Conversion happens on a different domain or embedded serviceSupported linking, tag access and third-party cooperationThat copying UTMs creates a complete cross-domain measurement setup
Collection exists, but a report is missing the outcomeAccount/property link, action, attribution, date basis and processingThat the redirect remains the cause

Google’s redirect click-data diagnostic identifies lost GCLID transport as a measurement failure. The first step is to locate that failure, rather than assuming every missing conversion has the same cause. Sources checked 3 November 2026.

2. Trace a synthetic arrival without clicking your ad

Copy the intended landing-page URL. Add a clearly fake, mixed-case marker and a small set of test campaign parameters. Use ? when no query exists, or & when it already has one. Replace an existing test GCLID instead of adding a second one.

https://example.com/quote?gclid=AdP_Test_Only_123&utm_source=google&utm_medium=cpc

Replace the example host and path with the page you control. Open the browser’s developer tools, preserve the network log, then navigate directly to that synthetic URL. Do not spend money clicking your live ad to perform a transport test.

  1. Record each document request and its status. For an HTTP redirect, inspect the response’s Location header and the next request URL.
  2. Find the first request where gclid disappears, becomes empty or changes value. Preserve capitalization and symbols. Inspect UTM values separately.
  3. Continue through HTTP-to-HTTPS, host, language, trailing-slash and CMS redirects. Check the final document, not just the first successful response.
  4. Watch for browser navigation or URL cleanup after the document loads. A command-line HTTP trace does not execute JavaScript, a meta refresh or a single-page application.
  5. Repeat on the relevant device/browser and record the consent state. Retest the bare URL and an already-tagged URL after any fix.

A redirect may reorder or encode a query without changing its decoded values. Compare the complete identifier value; do not lowercase, truncate or decode it repeatedly. Duplicate keys and a query accidentally placed after # need separate review.

Google’s missing Google Ads data checklist recommends a synthetic GCLID test and checking that its symbols and capitalization remain unchanged. That transport test does not validate a real ad-click match.

3. Compare the two URLs without sending them to an account

Paste the original and final URLs into the checker. It analyzes the supplied text locally, identifies missing or changed supported parameters and lets you copy a findings summary. It does not visit the URLs, trace intermediate hops, inspect cookies or verify conversion delivery.

Pasted URLs stay in this browser. No URL visits, uploads or saved inputs.

Paste complete http:// or https:// URLs, including their query strings. For a comparison, use URLs from the same test journey. Remove personal information first.

Checks the URLs you paste. Use Google Ads’ URL test and browser diagnostics to verify live redirects, tag loading and consent.

When there are several redirects, compare adjacent hops to locate the first change. Use synthetic URLs in shared notes. Real click IDs, cookies, form data and customer contact details do not belong in public screenshots.

If the question is a difference between Ads clicks and GA4 sessions, continue with the collection and counting diagnostic. Preserving a query cannot make those two metrics identical.

If the ad itself is disapproved, trace the expanded URL and the crawler response in the destination error guide.

4. Run three real HTTP redirects on your own computer

Download the redirect-query demo and run it with Python 3.9 or later:

python3 redirect-query-demo.py

The script starts an HTTP server on an automatically chosen 127.0.0.1 port, makes requests only to that server, follows its fixed local redirects and shuts the server down. It bypasses proxy settings, uses only the Python standard library and writes no files. It accepts no destination URL.

Inspect the full demo before running it
#!/usr/bin/env python3
"""Synthetic redirect demo. Run: python3 redirect-query-demo.py

Binds an ephemeral port on 127.0.0.1, requests only its own HTTP server,
bypasses system proxies and writes no files. No tags or real click IDs.
This is a transport exercise, not a conversion-attribution test.
"""
import json
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import TCPServer
from threading import Thread
from urllib.parse import parse_qsl, parse_qs, urlencode, urlsplit
from urllib.request import HTTPRedirectHandler, ProxyHandler, build_opener

QUERY = 'gclid=AdP_Test_Only_123&utm_source=google&utm_medium=cpc'


class LoopbackServer(HTTPServer):
    def server_bind(self):
        # HTTPServer normally reverse-resolves its address. This fixed numeric
        # loopback exercise needs no hostname lookup or external DNS dependency.
        TCPServer.server_bind(self)
        self.server_name = '127.0.0.1'
        self.server_port = self.server_address[1]


class DemoHandler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        request = urlsplit(self.path)
        if request.path == '/landing':
            body = b'Synthetic landing page. No measurement tags.\n'
            self.send_response(200)
            self.send_header('Content-Type', 'text/plain; charset=utf-8')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            self.wfile.write(body)
            return
        if request.path not in ('/strip', '/lower', '/keep'):
            self.send_error(404)
            return
        query = request.query
        if request.path == '/strip':
            query = ''
        elif request.path == '/lower':
            query = urlencode([(key, value.lower() if key == 'gclid' else value)
                               for key, value in parse_qsl(query, keep_blank_values=True)])
        # Fixed local destination: this server does not accept a redirect URL.
        self.send_response(302)
        self.send_header('Location', '/landing' + ('?' + query if query else ''))
        self.send_header('Content-Length', '0')
        self.end_headers()


class LocalTrace(HTTPRedirectHandler):
    def __init__(self, origin):
        self.origin = origin
        self.hops = []

    def redirect_request(self, req, fp, code, msg, headers, newurl):
        target = urlsplit(newurl)
        if (target.scheme + '://' + target.netloc != self.origin or
                target.path != '/landing' or len(self.hops) >= 3):
            raise ValueError('Demo redirect outside its fixed local destination')
        self.hops.append({'status': code, 'from_path': urlsplit(req.full_url).path,
                          'to_path': target.path})
        return super().redirect_request(req, fp, code, msg, headers, newurl)


def run_demo(query=QUERY):
    server = LoopbackServer(('127.0.0.1', 0), DemoHandler)
    thread = Thread(target=server.serve_forever, daemon=True)
    origin = 'http://127.0.0.1:' + str(server.server_port)
    output = []
    thread.start()
    try:
        before = parse_qs(query, keep_blank_values=True).get('gclid', [])
        for scenario, path in [('stripped_query', '/strip'),
                               ('lowercased_value', '/lower'),
                               ('preserved_query', '/keep')]:
            trace = LocalTrace(origin)
            # Disable proxy environment variables even for the loopback request.
            opener = build_opener(ProxyHandler({}), trace)
            with opener.open(origin + path + ('?' + query if query else ''), timeout=5) as response:
                response.read()
                final = urlsplit(response.geturl())
                after = parse_qs(final.query, keep_blank_values=True).get('gclid', [])
                state = 'missing' if not after else 'retained' if after == before else 'changed'
                output.append({'scenario': scenario, 'http_status': response.status,
                               'redirects': trace.hops, 'query_before': query,
                               'query_after': final.query, 'gclid_before': before,
                               'gclid_after': after, 'gclid_status': state})
    finally:
        server.shutdown()
        server.server_close()
        thread.join(timeout=5)
    return output


if __name__ == '__main__':
    if len(sys.argv) != 1:
        sys.exit('Usage: python3 redirect-query-demo.py (no URL arguments)')
    print(json.dumps(run_demo(), indent=2))

All three synthetic routes return a 302 followed by a successful 200 landing page. Their query handling differs:

ScenarioFinal HTTPFinal GCLID valueStatus
stripped_query200missingmissing
lowercased_value200adp_test_only_123changed
preserved_query200AdP_Test_Only_123retained

The first route drops the query. The second keeps a GCLID parameter but corrupts its mixed-case value. The third passes the original query through unchanged. Inspect redirects, query_before, query_after and the GCLID arrays in the JSON output.

A transport demonstration has a specific boundary

No Google tag runs in this demo. The identifiers are fake. A retained marker proves the local HTTP transport behavior; it does not record a conversion, reproduce consent behavior, test your live server or establish that Google can attribute a real click.

5. Change the rule that actually removes the query

Assign the fix to the layer that produced the first bad destination: CDN redirect, web server, CMS plugin, application route or browser script. Pointing ads at the intended final page can remove an unnecessary hop, but verify URL templates, policies and the remaining journey too.

For a specific NGINX canonical-path redirect, the pattern below keeps the incoming query on a fixed, reviewed destination. Replace the example host and paths, and adapt it to the intended site’s configuration:

# Example inside the intended site's server block.
# Replace example.com and the paths; validate in staging before deployment.
location = /quote {
    return 302 https://example.com/quote/$is_args$args;
}

$is_args supplies ? only when arguments exist; $args supplies the current query. Earlier rewrite rules can modify it, so test the whole chain. This example’s destination starts without a query; an existing destination query needs an explicit merge policy that avoids duplicate keys.

Keep the destination host fixed and reviewed. Do not turn an untrusted query value into a redirect target. Do not forward customer data or every tracking parameter to an unrelated domain. This is a configuration example, not a change applied to your server or a universal CMS fix.

NGINX documents variable-bearing redirects in the return directive and defines $args and $is_args. The local Python exercise demonstrates the transport failure; it does not execute or validate this NGINX configuration.

Validate your actual configuration in staging, check for loops and unexpected hosts, then repeat the browser trace. Include encoded values, mixed case, multiple parameters, an existing query and a bare URL. Plan rollback and cache handling for the redirect rule you change.

6. Verify tags and stored click information separately

After transport works, use Tag Assistant to inspect the actual sitewide Google tag or Conversion Linker setup under permitted consent states. Do not add duplicate tags merely because a later page has a clean URL.

Google’s linker can read landing-page click information and keep it in first-party cookies and browser local storage. Consequently, the identifier need not appear in every later URL for the original information to remain available. Check actual capture, storage scope and availability at conversion; a clean thank-you URL alone cannot answer that.

The Conversion Linker documentation explains storage, domain/path settings and supported linking across domains. It also says a container that loads a Google tag on every page does not additionally need a Conversion Linker tag.

For a form or checkout on another domain, verify the supported cross-domain setup and whether the destination can run compatible measurement. A third-party iframe or redirect is not repaired by appending UTMs everywhere. Preserve consent choices; do not route around them to make a test pass.

Finish with the real business chain: the form is accepted, the enquiry reaches the business, the selected conversion action receives its supported event, and the report is inspected in the correct account, date basis and processing state. The lead acceptance diagnostic covers that event boundary; the call-import checklist covers missing qualified-call outcomes.

7. Hand off one reproducible failure and acceptance test

Download the redirect click-data worksheet. Record the first changed hop, rule owner, destination, consent state, storage test, fix and rollback. Leave unresolved attribution separate from a repaired redirect.

Open the complete handoff worksheet
REDIRECT CLICK-DATA HANDOFF

Test date / owner:
Ad account / campaign / landing-page path (no real click ID):
Browser / device / consent state:
Fresh navigation or reused browser session:
Auto-tagging and linked-property checks:

SYNTHETIC TRANSPORT TEST
Use a mixed-case test marker, not a real ad click.
Starting URL:
Each HTTP hop: status / source host+path / destination host+path:
First hop that loses or alters the test parameter:
GCLID name and complete value unchanged:
UTM names/values / duplicate keys / existing-query handling:
Final document URL:
Client-side redirect or URL-cleanup script after document load:
HTTP trace and browser trace agree / explain difference:

TAG AND STORAGE TEST (separate from query transport)
Actual sitewide Google tag or Conversion Linker setup:
Tag Assistant result under each permitted consent state:
Relevant cookie/storage scope and availability:
Landing host versus conversion host / embedded third-party form:
Required supported cross-domain setup and destination cooperation:
Accepted form / received enquiry / real conversion action:
Report account / property / action / date basis / processing status:
What the synthetic marker cannot prove:

CHANGE AND ACCEPTANCE
Rule owner: CDN / web server / CMS / browser script / external service:
Exact rule to change and reviewed destination host:
Staging result for bare URL and already-tagged URL:
Query encoding, mixed case, multiple parameters and existing-query test:
No redirect loop, duplicate parameter, unexpected host or failed page:
Change date / rollback / cache invalidation if applicable:
Repeated browser and platform-supported verification:
Remaining attribution or business-outcome issue:

Keep real identifiers, cookies and customer details out of shared notes.
Preserving parameters does not establish a recorded conversion or prevent fraud.

If suspicious visits remain after measurement checks, use the traffic-audit checklist. Missing GCLID is not itself a bot classification, a refund entitlement or proof that an interaction was invalid.

Where AdProtektor fits

AdProtektor provides evidence about visits its integration observes and configurable protection. Eligible Google Ads exclusion updates depend on the active connection, permissions, supported scope and synchronization. Website enforcement and ConversionOS event handling are separate configured steps; neither reconstructs a click identifier stripped before collection.

Start a free AdProtektor trial to review observed visits and configured protection alongside the repaired measurement path and real business outcomes. Find the URL checker, IP and placement builders, conversion-action audit and both read-only scripts in the free tools hub.

If the person called straight from an ad without visiting the website, use the direct-call attribution guide before investigating website redirects.

An intact parameter is a transport result. Use the GCLID provenance and account-match guide before treating it as authenticated click or billing evidence.