SEO & Digital Marketing Module 11 – Analytics and measurement
Consent, cookies and privacy-safe tracking
Consent mode defaults, a banner where Reject is as easy as Accept, what DPDP, GDPR and ePrivacy ask of marketers, and measurement without hidden tracking.
What you will learn
- Implement consent mode with denied defaults set before any tag sends data, and an update call for each banner choice
- Design a cookie banner on which Reject is as easy as Accept, with nothing pre-ticked and a way to withdraw
- Explain what India's DPDP Act and Rules, the GDPR and the ePrivacy Directive ask of marketers, and when the DPDP duties apply
- Set first-party cookies with Secure, HttpOnly, SameSite and the shortest workable lifetime
- Plan measurement that survives refusals with first-party data instead of hidden tracking
Before you start
What you’ll learn · Before you start
What you will learn
- Implement consent mode with denied defaults set before any tag sends data, and an update call for each banner choice
- Design a cookie banner on which Reject is as easy as Accept, with nothing pre-ticked and a way to withdraw
- Explain what India's DPDP Act and Rules, the GDPR and the ePrivacy Directive ask of marketers, and when the DPDP duties apply
- Set first-party cookies with Secure, HttpOnly, SameSite and the shortest workable lifetime
- Plan measurement that survives refusals with first-party data instead of hidden tracking
Before you start
On this page
Pune Bakes added a cookie banner for its customers in Europe, and in the first week 412 visitors pressed Accept while none pressed Reject, because there was no Reject: only Accept all and a small Settings link. A banner like that does not collect consent; it collects clicks on the one button offered.
Consent is where marketing, law and code meet, and each fails without the others. By the end of this page you can set up consent mode so that Google’s tags know each visitor’s choice, design a banner on which refusing is as easy as agreeing, explain what India’s DPDP Act and Rules, the EU’s GDPR and ePrivacy Directive and the UK’s rules ask of marketers and from when, set your own cookies safely, and plan measurement that still works when people say no.
Consent mode tells Google’s tags what each visitor allowed
consent mode: Google's way for a site to pass each visitor's consent choices to its tags: a default state set before any measurement, then an update when the visitor chooses. The site itself must still obtain the consent. is how a site passes a visitor’s choice to Google’s tags, which then adjust what they store and send. It has seven consent types; four of them matter for measurement and ads (Consent mode overview):
| Consent type | What it controls |
|---|---|
ad_storage |
Storage for advertising, such as cookies |
ad_user_data |
Whether user data may be sent to Google for advertising |
ad_personalization |
Whether ads may be personalised |
analytics_storage |
Storage for measurement, such as how long a visit lasts |
functionality_storage, personalization_storage, security_storage |
Storage for the site’s own functions, personal choices such as suggestions, and security such as sign-in checks and fraud prevention |
Google is plain about who does the asking: the site is “responsible for obtaining users’ consent”, and consent mode
only carries the answer. Its guide sets two commands
(Set up consent mode). The default runs before
any command that sends measurement data, such as config or event, may be scoped to regions with ISO 3166-2 codes,
and may give the banner time to answer with wait_for_update, 500 milliseconds in Google’s example. The update runs
“as soon as a user interacts with your consent management solution”, for any change, granted to denied included.
Google adds that “the order of the code here is vital”. For visitors in the European Economic Area, Google requires
sites that use its tags for measurement, ad personalisation and remarketing to collect consent and pass the signals on,
and it added ad_user_data and ad_personalization to consent mode for this
(EEA update).
The program replays three setups of the bakery’s pages with a stand-in for the tag’s queue, which follows those rules. Run it, then press Edit and move the default in setup B to the top of its list:
"""Replay three ways of setting up consent mode on the bakery's pages, with a stand-in for the Google tag's queue.
The rules come from Google's guide "Set up consent mode on websites": the default command runs before any command that
sends measurement data (config or event), "the order of the code here is vital"; a default with a region applies to
visitors there, one without a region to everyone else, and the more specific region wins; by default no consent mode
values are set; EEA traffic also needs ad_user_data and ad_personalization ("Updates to consent mode for traffic in
EEA"). This is a model of those rules, not Google's code. The bakery and its tag id are fictional.
"""
EEA_UK_CH = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV", "LT",
"LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "GB", "CH"]
ALL_DENIED = {"ad_storage": "denied", "ad_user_data": "denied", "ad_personalization": "denied",
"analytics_storage": "denied"}
SETUPS = {
"A, defaults first": [
("consent", "default", {**ALL_DENIED, "region": EEA_UK_CH, "wait_for_update": 500}),
("config", "G-PB5678"),
("event", "page_view"),
],
"B, defaults after config": [
("config", "G-PB5678"),
("consent", "default", {**ALL_DENIED, "region": EEA_UK_CH, "wait_for_update": 500}),
("event", "page_view"),
],
"C, defaults without the two newer types": [
("consent", "default", {"ad_storage": "denied", "analytics_storage": "denied", "region": EEA_UK_CH}),
("config", "G-PB5678"),
],
}
def state(defaults, country):
"""The consent state a visitor from `country` has: a default for their region wins over one without a region."""
chosen = {}
for settings in sorted(defaults, key=lambda d: "region" in d): # region-less first, so a regional one overrides
if "region" not in settings or country in settings["region"]:
chosen.update({k: v for k, v in settings.items() if k not in ("region", "wait_for_update")})
return ", ".join(f"{k}={v}" for k, v in chosen.items()) or "no consent values set"
for name, queue in SETUPS.items():
print(f"Setup {name}")
defaults_seen = []
for command in queue:
if command[0] == "consent" and command[1] == "default":
defaults_seen.append(command[2])
elif command[0] in ("config", "event"):
when = "after the defaults" if defaults_seen else "BEFORE any default: consent defaults won't work"
print(f" first measurement command, {command[0]} {command[1]}: {when}")
break
for country in ("DE", "IN"):
print(f" a visitor in {country} starts with: {state(defaults_seen, country)}")
missing = [t for t in ALL_DENIED if all(t not in d for d in defaults_seen)]
if defaults_seen and missing:
print(f" missing for EEA traffic: {', '.join(missing)}")
print() Output
Setup A, defaults first first measurement command, config G-PB5678: after the defaults a visitor in DE starts with: ad_storage=denied, ad_user_data=denied, ad_personalization=denied, analytics_storage=denied a visitor in IN starts with: no consent values set Setup B, defaults after config first measurement command, config G-PB5678: BEFORE any default: consent defaults won't work a visitor in DE starts with: no consent values set a visitor in IN starts with: no consent values set Setup C, defaults without the two newer types first measurement command, config G-PB5678: after the defaults a visitor in DE starts with: ad_storage=denied, analytics_storage=denied a visitor in IN starts with: no consent values set missing for EEA traffic: ad_user_data, ad_personalization
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 consent_calls.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Setup A is right. Setup B runs the default after config, so the first measurement goes out with no consent state at
all, the case Google warns “consent defaults won’t work”. Setup C forgets the two newer types. In all three, a visitor
in India starts with no consent values, because the bakery scoped its banner to the EEA, the UK and Switzerland: which
regions need a banner is a question for the law of each place, not for the tag. On a site with Tag Manager, Google
recommends loading the banner through the container with a consent mode template, which uses the container’s own
consent commands rather than gtag('consent', 'update', …).
Checkpoint
Check: Consent mode's defaults and updates
Basic mode waits; advanced mode sends measurements without cookies
Consent mode can be set up in two ways, and the difference is what happens before and after a refusal (Consent mode overview):
| What differs | Basic consent mode | Advanced consent mode |
|---|---|---|
| Before the visitor chooses | Tags do not load; nothing is sent | Tags load with denied defaults and send measurements without cookies |
| After a refusal | No data reaches Google, not even pings | Measurements without cookies continue: the consent state, a timestamp, the user agent, the referrer, a random value per page load |
| After consent | Tags load and run normally | Tags store cookies and send full measurement |
| Modelling of the gaps | A general model | A model specific to the advertiser |
Step through one visitor’s choice on the bakery’s site, under advanced mode:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Defaults before anything. The page runs the consent default first, before the tag's config. For visitors in the EEA, the UK and Switzerland it sets ad_storage, ad_user_data, ad_personalization and analytics_storage to denied.
- The banner asks. The banner offers Accept all, Reject all and Choose, as buttons of the same size and weight, and says what each purpose is for.
- Measured without cookies. In advanced mode the tags have already loaded. While she decides, they set no cookies and send measurements without cookies, such as the consent state, a timestamp, the browser's user agent and the referrer.
- She rejects. She presses Reject all. The banner sends a consent update with all four types denied and keeps her choice in a first-party cookie, so the banner does not ask again on every page.
- Nothing more is stored. On the next pages the tags still set no _ga cookie. Had the bakery used basic mode, its tags would not have loaded at all, and nothing would have reached Google.
- A change of mind. A week later she opens Cookie settings from the footer, a link on every page, and allows analytics only. The banner sends an update with analytics_storage granted and the three advertising types denied.
- Measured with a cookie. Now the tag may store its cookie and send full analytics measurement, while advertising stays off. Withdrawing later is the same update with granted turned back to denied.
Advanced mode exists for modelling: GA4 can estimate the behaviour of visitors who refused from similar visitors who
agreed. The bar is high. A property qualifies only with at least 1,000 events a day with analytics_storage denied for
7 days or more, and at least 1,000 users a day with it granted on 7 of the last 28 days, and it models users, sessions
and new users, not event counts (Behavioral modeling). The
bakery has about 40 sessions a week. It will never reach those numbers, and neither will any site with fewer than
1,000 consenting users a day, so for such sites modelling fills no gaps, and the choice between the modes is only a
choice about what to send before and after a refusal.
Checkpoint
Check: Basic and advanced consent mode
A banner on which Reject is as easy as Accept
A banner decides whether consent is real, and European regulators have said what a bad one looks like. The European Data Protection Board’s cookie banner taskforce found that, for “a vast majority of authorities”, a banner without a refuse option on any layer that has an accept button does not give valid consent; that pre-ticked boxes do not lead to valid consent, citing the GDPR’s recital 32, “Silence, pre-ticked boxes or inactivity should not therefore constitute consent”; that a refuse link embedded in text without enough visual support does not either; and that contrast designed to hide the alternative can be “manifestly misleading” (taskforce report). Withdrawal must match: the GDPR says “it shall be as easy to withdraw as to give consent” (article 7(3)), and India’s DPDP Act asks that withdrawing be comparable in ease to consenting (section 6(4)).
The program checks three banner designs against those findings, and measures the contrast of each Reject option with
the formula of WCAG 2.2, whose minimum for normal text is 4.5:1
(WCAG 2.2). The designs are in BANNERS at the program’s top. Run it,
then press Edit and give banner 2’s Reject the colours of its Accept button:
"""Check three cookie banner designs against the practices the EDPB's cookie banner taskforce assessed.
From the taskforce's report (2023): a banner with no reject option on any layer that has an accept button was, for a
vast majority of authorities, an infringement; pre-ticked boxes do not give valid consent (GDPR recital 32); a
reject option whose text has almost no contrast with its button can be manifestly misleading. Withdrawal must be as easy
as giving consent (GDPR art. 7(3); India's DPDP Act s. 6(4)). Contrast is measured with WCAG 2.2's formula; 4.5:1 is
its minimum for normal text. BANNERS holds the three designs, invented for the bakery: the first layer's options, with
their kind and colours, the second layer's boxes as they are shown, and whether a link on every page reopens the choice.
"""
BLUE, WHITE = "#0B57D0", "#FFFFFF"
BANNERS = [
{"name": "1, accept or go to settings",
"first_layer": [{"label": "Accept all", "kind": "button", "text": WHITE, "background": BLUE},
{"label": "Settings", "kind": "link", "text": BLUE, "background": WHITE}],
"second_layer": {"analytics": False, "ads": False}, "withdraw_link": False},
{"name": "2, reject in faint grey, boxes ticked",
"first_layer": [{"label": "Accept all", "kind": "button", "text": WHITE, "background": BLUE},
{"label": "Reject", "kind": "link", "text": "#E2E2E2", "background": WHITE}],
"second_layer": {"analytics": True, "ads": True}, "withdraw_link": True},
{"name": "3, accept and reject alike",
"first_layer": [{"label": "Accept all", "kind": "button", "text": WHITE, "background": BLUE},
{"label": "Reject all", "kind": "button", "text": WHITE, "background": BLUE},
{"label": "Choose", "kind": "button", "text": BLUE, "background": WHITE}],
"second_layer": {"analytics": False, "ads": False}, "withdraw_link": True},
]
def luminance(hex_colour):
"""Relative luminance of an sRGB colour, as WCAG 2.2 defines it."""
channels = [int(hex_colour[i:i + 2], 16) / 255 for i in (1, 3, 5)]
linear = [c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4 for c in channels]
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2]
def contrast(a, b):
high, low = sorted((luminance(a), luminance(b)), reverse=True)
return (high + 0.05) / (low + 0.05)
for banner in BANNERS:
problems = []
first = {item["label"].split()[0].lower(): item for item in banner["first_layer"]}
accept, reject = first.get("accept"), first.get("reject")
if not reject:
problems.append("no reject option beside Accept on the first layer")
else:
ratio = contrast(reject["text"], reject["background"])
if ratio < 4.5:
problems.append(f"Reject's text contrast is {ratio:.2f}:1, under 4.5:1")
if reject["kind"] != accept["kind"]:
problems.append(f"Accept is a {accept['kind']}, Reject a {reject['kind']}")
ticked = [purpose for purpose, on in banner["second_layer"].items() if on]
if ticked:
problems.append(f"pre-ticked: {', '.join(ticked)}")
if not banner["withdraw_link"]:
problems.append("no link on every page to change or withdraw the choice")
print(f"Banner {banner['name']}: " + ("passes" if not problems else f"{len(problems)} problems"))
for p in problems:
print(" ", p) Output
Banner 1, accept or go to settings: 2 problems
no reject option beside Accept on the first layer
no link on every page to change or withdraw the choice
Banner 2, reject in faint grey, boxes ticked: 3 problems
Reject's text contrast is 1.30:1, under 4.5:1
Accept is a button, Reject a link
pre-ticked: analytics, ads
Banner 3, accept and reject alike: passes
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 banner_check.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Banner 1 is the bakery’s first week: no Reject at all, and no way back. Banner 2 has a Reject whose text has a contrast of 1.30:1, as a link beside a button, with both purposes ticked in advance. Banner 3 passes: three buttons alike, nothing ticked, and a link on every page to change the choice. The bakery’s 412 acceptances in a week were the first design’s work, not the visitors’ choice.
Cookie Consent Banner Generator Generate a banner with Accept and Reject of equal weight, nothing pre-ticked, a link to change the choice and consent mode defaults set to denied.Checkpoint
Check: A banner that makes refusing easy
The law sets the bar: the EU and UK for storage, India’s DPDP Act for consent
Education, not legal advice
This section explains the rules from their official texts so that you can talk to a lawyer about your own business. It does not decide what your business must do: that depends on where you operate, who your visitors are and what you collect.
The European rule is about the device itself. Storing information on a visitor’s device, or reading what is already there, is allowed only with the visitor’s consent, given after clear and comprehensive information, unless it is for “the sole purpose of carrying out the transmission” or strictly necessary for a service the visitor explicitly asked for (ePrivacy Directive, article 5(3)). It is not limited to cookies: the EDPB’s guidelines on its scope confirm that device fingerprinting falls within it (Guidelines 2/2023). The consent itself is the GDPR’s: “freely given, specific, informed and unambiguous”, by “a clear affirmative action” (article 4(11)), which the business must be able to demonstrate (article 7(1)) (GDPR). In the UK, regulation 6 of PECR sets the same kind of rule; the ICO says non-essential cookies need consent before they are set, consent must be more than “simply continuing to use the website”, and its detailed guidance is being revised (ICO).
The rules differ in what triggers them, and they agree on the shape of consent:
| Rule | EU and UK (ePrivacy or PECR, with the GDPR or UK GDPR) | India (DPDP Act and Rules) |
|---|---|---|
| What needs consent | Storing or reading information on a device, beyond the narrow exemptions | Processing personal data, unless a legitimate use applies |
| Standard | Freely given, specific, informed, unambiguous, a clear affirmative action | Free, specific, informed, unconditional, unambiguous, a clear affirmative action |
| Withdrawal | As easy as giving it | Comparable in ease to giving it |
| Children | EU: a parent’s consent under 16, or a lower age a member state sets, never under 13 (article 8); UK: set by its own data protection law | Under 18: a parent’s verifiable consent, and no tracking or targeted ads |
India’s Digital Personal Data Protection Act, 2023, is about personal data rather than the device, and its consent rule is strict. Consent must be free, specific, informed, unconditional and unambiguous, given by a clear affirmative action, for a specified purpose and only the data it needs (section 6(1)); the request for it comes with, or after, a notice that can be read in English or a language of the Constitution’s Eighth Schedule (section 5); withdrawing must be as easy as giving it (section 6(4)); and the business must be able to prove notice and consent (section 6(10)). A child is anyone under 18 (section 2(f)), whose data needs a parent’s verifiable consent; tracking children, monitoring their behaviour and advertising targeted at them are not allowed (section 9). A person may give and withdraw consent through a consent manager: Under India's DPDP Act, a person registered with the Data Protection Board who acts as a single point of contact through which people give, manage, review and withdraw their consent on one platform. registered with the Data Protection Board. The duties start in phases:
- 2023
The Act is passed
Parliament enacted the Digital Personal Data Protection Act in 2023. It comes into force on dates the Central Government notifies, and different provisions may get different dates.
- Rules notified
The Rules and the dates 2025
In 2025 the government notified the DPDP Rules and the dates of commencement, with an eighteen-month phased timeline. The definitions and the Data Protection Board's provisions applied at once.
- One year on
Consent managers can register
The registration of consent managers applies one year after the notification. A consent manager is registered with the Board and lets a person give, manage, review and withdraw consent from one platform; it must be an Indian company.
- Eighteen months on
The core duties apply 2027
Eighteen months after the notification, in 2027, the rest of the Act applies, notice, consent and its withdrawal, the duties of data fiduciaries and the rules for children's data among it, together with the matching Rules.
- From then on
Notice, proof and requests
A business that relies on consent gives a notice that stands on its own, with an itemised description of the data and the purpose, must be able to prove notice and consent, and answers requests to access, correct or erase data within 90 days at most.
The timeline has 5 events, from 2023 to From then on.
Penalties under the DPDP Act
The government’s backgrounder on the Rules lists penalties of up to ₹250 crore for failing to keep reasonable security safeguards, up to ₹200 crore for not notifying the Board or the people affected of a breach and for breaking the obligations about children, and up to ₹50 crore for other violations (Press Information Bureau).
Checkpoint
Check: ePrivacy, the GDPR and India's DPDP Act
Your own cookies need Secure, HttpOnly, SameSite and a short life
Consent decides whether a cookie may be set; how it is set is security. OWASP’s rules for a site’s own cookies are
short (Session Management Cheat Sheet):
Secure, so the cookie travels only over HTTPS; HttpOnly wherever the page’s scripts do not need the value;
SameSite set explicitly to Strict or Lax, and None never without Secure; no Domain attribute, so the cookie
stays with the host that set it; and the __Host- prefix for session cookies. Lifetimes are capped by the browser in any
case: the cookie draft says the limit “SHOULD NOT be greater than 400 days”
(RFC 6265bis), and Google notes that browsers can
shorten even its own two-year _ga cookie, to at most 400 days in Chrome and 7 days in Safari
(cookie usage). The program reads five Set-Cookie headers
copied from the bakery’s server. Run it, then press Edit and add pb_visitor to SCRIPT_READS:
"""Check the bakery's own Set-Cookie headers against the OWASP Session Management Cheat Sheet and RFC 6265bis.
OWASP: Secure on every cookie; HttpOnly where the page's scripts do not need the value; SameSite set explicitly, Lax or
Strict, and None only with Secure; no Domain attribute, so the cookie stays with the host that set it; the __Host- prefix
for session cookies. RFC 6265bis: browsers limit a cookie's lifetime, the limit "SHOULD NOT be greater than 400 days", and
a SameSite=None cookie without Secure is ignored. SCRIPT_READS lists the cookies the bakery's own scripts must read.
The headers are the bakery's (a fictional site), copied from its server's responses into set_cookie_headers.txt.
"""
SCRIPT_READS = {"pb_cart", "pb_consent"}
MAX_DAYS = 400
def parse(header):
first, *attributes = [part.strip() for part in header.split(":", 1)[1].split(";")]
name = first.split("=", 1)[0]
attrs = {}
for a in attributes:
key, _, value = a.partition("=")
attrs[key.lower()] = value
return name, attrs
for line in open("set_cookie_headers.txt", encoding="utf-8").read().splitlines():
name, attrs = parse(line)
problems = []
if "secure" not in attrs:
problems.append("no Secure: it can travel over plain HTTP")
if "httponly" not in attrs and name not in SCRIPT_READS:
problems.append("no HttpOnly, though no script of the bakery reads it")
same_site = attrs.get("samesite", "").lower()
if not same_site:
problems.append("no SameSite: set Lax or Strict explicitly")
elif same_site == "none" and "secure" not in attrs:
problems.append("SameSite=None without Secure: browsers ignore this cookie")
if "domain" in attrs:
problems.append(f"Domain={attrs['domain']} also sends it to every subdomain")
if "max-age" in attrs:
days = int(attrs["max-age"]) / 86400
if days > MAX_DAYS:
problems.append(f"asks for {days:.0f} days; browsers cap lifetimes, Chrome at {MAX_DAYS} days")
lifetime = f"{int(attrs['max-age']) / 86400:.0f} days" if "max-age" in attrs else "until the browser session ends"
print(f"{name} ({lifetime})" + (": ok" if not problems else ":"))
for p in problems:
print(" ", p) Output
__Host-pb_session (until the browser session ends): ok
pb_cart (7 days): ok
pb_consent (180 days): ok
pb_visitor (1825 days):
no Secure: it can travel over plain HTTP
no HttpOnly, though no script of the bakery reads it
no SameSite: set Lax or Strict explicitly
Domain=punebakes.example also sends it to every subdomain
asks for 1825 days; browsers cap lifetimes, Chrome at 400 days
pb_admin (until the browser session ends):
no Secure: it can travel over plain HTTP
SameSite=None without Secure: browsers ignore this cookie
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 cookie_attrs.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Set-Cookie: __Host-pb_session=3f8a1c; Path=/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: pb_cart=eggless-1kg:1; Path=/; Secure; SameSite=Lax; Max-Age=604800
Set-Cookie: pb_consent=analytics:denied,ads:denied; Path=/; Secure; SameSite=Lax; Max-Age=15552000
Set-Cookie: pb_visitor=7d2e9b; Domain=punebakes.example; Path=/; Max-Age=157680000
Set-Cookie: pb_admin=91ac; Path=/admin; HttpOnly; SameSite=None Three cookies pass: the session cookie with __Host-, Secure, HttpOnly and SameSite=Lax, and the cart and consent
cookies, which the page’s scripts read and so go without HttpOnly. pb_visitor breaks five rules at once and asks for
five years; pb_admin sets SameSite=None without Secure, which browsers ignore. A cookie nobody can explain, like
pb_visitor, is usually best removed: it is one fewer identifier to justify under any of the laws above.
Checkpoint
Check: Secure attributes for your own cookies
Measurement that survives a refusal is built on first-party data
When a visitor refuses, the honest answer is to measure less, not to measure around the refusal. Three things keep working. Orders, enquiries and bookings sit in the business’s own systems whatever the banner said, so the KPIs of the goals lesson can always be counted there, and GA4’s numbers become a sample to read trends from. Reports in totals, such as orders a week by channel from tagged links, need no identifier for each person. And where traffic is large enough, consent mode’s modelling fills some of the gaps.
The industry is not coming to the rescue with new identifiers. In 2025 Google announced that Chrome keeps users’ choice about third-party cookies and that most Privacy Sandbox APIs, the Attribution Reporting API, Protected Audience and Topics among them, are being retired “in light of their low levels of adoption”, while CHIPS, FedCM and Private State Tokens stay and an interoperable attribution standard is being worked on at the W3C (Privacy Sandbox update).
| After a refusal | Keeps working | Stops, and should |
|---|---|---|
| Counting | Orders and enquiries in your own systems | A cookie or identifier for that visitor |
| Channels | Totals from tagged links, in your own order records | Following that visitor across visits |
| Estimates | Consent mode modelling, above its thresholds | Fingerprinting, which the EU’s rule covers too |
Key idea
Ask before you store, make refusing as easy as agreeing, and measure what refusals leave you from your own records: a refusal is an answer to respect, not a gap to engineer around.
Checkpoint
Check: Measurement that survives a refusal
Practise: the consent calls of a shop that sells to Europe
Write a shop’s consent mode calls and banner settings: denied defaults for exactly the regions that see the banner, an update for each choice, and a banner that passes this page’s rules:
Exercise · Medium · Python
Write the consent defaults, updates and banner rules
A shop that sells to Europe shows its cookie banner to visitors in the EEA, the UK and Switzerland, the countries in CONSENT_REGIONS in consent_plan.py. What it does elsewhere is for its lawyer, so the exercise leaves other regions alone. A contractor wrote the shop's consent mode calls and banner settings; fix them.
default_commands()returns the parameters of everygtag('consent', 'default', …)call, as dictionaries. One of them must apply to exactly the countries ofCONSENT_REGIONS(itsregion), setad_storage,ad_user_data,ad_personalizationandanalytics_storageto'denied', and give the banner time to answer withwait_for_updatebetween 1 and 2,000 milliseconds. No default may grant anything to those regions.update_for(choice)returns the parameters of thegtag('consent', 'update', …)call for a choice:'accept_all'grants the four types,'reject_all'denies all four, and'analytics_only'grantsanalytics_storageand denies the three advertising types.BANNERdescribes the banner: Reject on the first layer, Accept and Reject alike, nothing pre-ticked, and a link on every page to change the choice.
The sample tests check the calls the way Google's guide writes them, and the banner against this lesson's rules.
Starter code · consent_plan.py
"""Consent mode calls and banner settings of a shop that shows its banner in the EEA, the UK and Switzerland."""
CONSENT_REGIONS = ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", "HU", "IE", "IT", "LV",
"LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", "SI", "ES", "SE", "IS", "LI", "NO", "GB", "CH"]
TYPES = ["ad_storage", "ad_user_data", "ad_personalization", "analytics_storage"]
def default_commands():
"""The parameters of each gtag('consent', 'default', ...) call, run before the tag's config."""
return [{"ad_storage": "granted", "analytics_storage": "granted"}]
def update_for(choice):
"""The parameters of the gtag('consent', 'update', ...) call for 'accept_all', 'reject_all' or 'analytics_only'."""
if choice == "accept_all":
return {"ad_storage": "granted", "analytics_storage": "granted"}
return {}
BANNER = {
"reject_on_first_layer": False,
"accept_and_reject_alike": False,
"pre_ticked": ["analytics", "ads"],
"change_choice_link": False,
} The sample tests · test_consent_plan.py
from consent_plan import BANNER, CONSENT_REGIONS, TYPES, default_commands, update_for
SEVEN = {"ad_storage", "ad_user_data", "ad_personalization", "analytics_storage", "functionality_storage",
"personalization_storage", "security_storage"}
def regional(commands):
return [c for c in commands if set(c.get("region", [])) == set(CONSENT_REGIONS)]
def the_regional_default():
found = regional(default_commands())
assert len(found) == 1, f"{len(found)} defaults have region equal to CONSENT_REGIONS; there must be exactly one"
return found[0]
def test_a_default_for_exactly_the_consent_regions():
"""sets one default whose region is exactly the countries that see the banner"""
the_regional_default()
def test_the_default_denies_all_four():
"""denies the four advertising and analytics types for those regions"""
command = the_regional_default()
for t in TYPES:
assert command.get(t) == "denied", f"{t}: {command.get(t)}"
def test_wait_for_update_gives_the_banner_time():
"""waits between 1 and 2,000 milliseconds for the banner's update"""
wait = the_regional_default().get("wait_for_update")
assert isinstance(wait, int) and 1 <= wait <= 2000, wait
def test_no_default_grants_anything_in_those_regions():
"""grants nothing by default to a visitor in the consent regions"""
for command in default_commands():
if not command.get("region") or set(command["region"]) & set(CONSENT_REGIONS):
assert "granted" not in command.values(), command
def test_updates_for_each_choice():
"""grants the four for accept_all, none for reject_all, and only analytics for analytics_only"""
assert update_for("accept_all") == {t: "granted" for t in TYPES}
assert update_for("reject_all") == {t: "denied" for t in TYPES}
only = update_for("analytics_only")
assert only["analytics_storage"] == "granted"
assert all(only[t] == "denied" for t in TYPES if t != "analytics_storage")
def test_only_known_types_and_values():
"""uses only consent mode's own types and the values granted and denied"""
for params in [*default_commands(), *(update_for(c) for c in ("accept_all", "reject_all", "analytics_only"))]:
for key, value in params.items():
if key in ("region", "wait_for_update"):
continue
assert key in SEVEN, key
assert value in ("granted", "denied"), f"{key}: {value}"
def test_the_banner_follows_the_rules():
"""puts Reject on the first layer, alike Accept, ticks nothing and offers a link to change the choice"""
assert BANNER["reject_on_first_layer"] is True
assert BANNER["accept_and_reject_alike"] is True
assert BANNER["pre_ticked"] == []
assert BANNER["change_choice_link"] is True A hint
Start from the four advertising and analytics types in TYPES. A dictionary comprehension such as {t: "denied" for t in TYPES} gives the denied state; add "region": list(CONSENT_REGIONS) and "wait_for_update": 500 for the default. For update_for, decide for each type whether the choice grants it, and build the dictionary the same way.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then practise the page’s six ideas, from the consent calls to your own cookies, weakest first:
Practice that adapts to you
Practise this lesson
12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2–5 years): basic or advanced consent mode, and how they behave when a visitor refuses
In basic consent mode, Google’s tags do not load until the visitor chooses; if they refuse, no data reaches Google, not even pings, and Google Ads conversion modelling falls back to a general model. In advanced consent mode, the tags load at once with denied defaults; they store no cookies and send measurements without cookies, such as the consent state, a timestamp, the user agent and the referrer, and after consent they store cookies and send everything. Advanced mode allows modelling specific to the advertiser, but only above thresholds: GA4’s behavioural modelling needs at least 1,000 events a day with analytics storage denied for 7 days and 1,000 consenting users a day on 7 of 28 days. Either way, the default must run before any measurement command, and obtaining valid consent stays the site’s job.
Two more questions follow, about cookies set before anyone clicks and about the DPDP Act. Choose your first move for each, then open it:
Checkpoint
Your first decision
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A shop's banner is live, yet visitors in Germany get a _ga cookie before they touch it, and the advertising pixel fires on the first page. How do you find the cause, fix it and prove the fix?
-
Mid to senior
An Indian direct-to-consumer brand asks what the DPDP Act and Rules mean for its website tracking, its email list and its WhatsApp marketing, and by when it must be ready. How do you answer, as a marketer and not as its lawyer?
Key takeaways
- Consent mode passes each visitor’s choice to Google’s tags with a default that runs before any measurement command, scoped to the regions that see the banner, and an update for every change; obtaining consent stays the site’s job.
- Basic mode loads no tags until the visitor chooses; advanced mode sends measurements without cookies, which feed modelling only above thresholds that small sites rarely reach.
- A banner gives valid consent only when refusing is as easy as accepting: Reject on the first layer, alike Accept, nothing pre-ticked, and withdrawal as easy as consent.
- The EU and UK rules cover storing and reading information on a device, fingerprinting included; India’s DPDP Act covers personal data, with strict consent, protection for everyone under 18, and duties that apply from 2027.
- A site’s own cookies are Secure, HttpOnly where scripts do not read them, SameSite Lax or Strict, without Domain, and short-lived; browsers cap their lifetimes anyway.
- Measurement that survives refusals comes from first-party records and totals, not from identifiers engineered around the refusal.
Terms in this lesson
- consent manager
- Under India's DPDP Act, a person registered with the Data Protection Board who acts as a single point of contact through which people give, manage, review and withdraw their consent on one platform.
- consent mode
- Google's way for a site to pass each visitor's consent choices to its tags: a default state set before any measurement, then an update when the visitor chooses. The site itself must still obtain the consent.
References
- Consent mode overview (Google for Developers)
- Set up consent mode on websites (Google for Developers)
- Updates to consent mode for traffic in European Economic Area (EEA) (Tag Manager Help)
- [GA4] Behavioral modeling for consent mode (Google Analytics Help)
- Report of the work undertaken by the Cookie Banner Taskforce (European Data Protection Board)
- Guidelines 2/2023 on Technical Scope of Art. 5(3) of ePrivacy Directive (European Data Protection Board)
- Directive 2002/58/EC (ePrivacy Directive), consolidated text (EUR-Lex, Publications Office of the European Union)
- Regulation (EU) 2016/679 (General Data Protection Regulation) (EUR-Lex, Publications Office of the European Union)
- Cookies and similar technologies (Information Commissioner's Office)
- The Digital Personal Data Protection Act, 2023 (Ministry of Electronics and Information Technology, Government of India)
- Digital Personal Data Protection Rules, 2025 (G.S.R. 846(E)) (Ministry of Electronics and Information Technology, Government of India)
- Notification G.S.R. 843(E): dates of commencement of the Digital Personal Data Protection Act, 2023 (Ministry of Electronics and Information Technology, Government of India)
- Government notifies DPDP Rules to empower citizens and protect privacy (Press Information Bureau, Government of India)
- DPDP Rules, 2025 notified (backgrounder) (Press Information Bureau, Government of India)
- Session Management Cheat Sheet (OWASP)
- Cookies: HTTP State Management Mechanism (draft-ietf-httpbis-rfc6265bis) (IETF)
- [GA4] Cookie usage on websites (Google Analytics Help)
- Update on Plans for Privacy Sandbox Technologies (Google)
- Web Content Accessibility Guidelines (WCAG) 2.2: contrast (minimum) (W3C)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress