Skip to content

WT-964 Refactor stub attribution#1296

Draft
maureenlholland wants to merge 23 commits into
mainfrom
WT-964--refactor-stub-attribution
Draft

WT-964 Refactor stub attribution#1296
maureenlholland wants to merge 23 commits into
mainfrom
WT-964--refactor-stub-attribution

Conversation

@maureenlholland
Copy link
Copy Markdown
Collaborator

@maureenlholland maureenlholland commented Apr 23, 2026

One-line summary

Simplify and clarify download attribution data flow

Significant changes and points to review

Refactored attribution approach should be gated behind a switch. When that switch is active, the following changes should be applied:

REMOVED

  • stub-attribution-consent (now handled through GTM consent mode)

REPLACED

  • stub-attribution (now download-attribution)
  • stub-attribution-init (now download-attribution-init)
  • thanks, thanks-direct, thanks-init (now auto-download)

ADDED

  • download-attribution
    • completely isolates essential and analytics data (with separate triggers)
  • download-attribution-init (the default that runs on all pages)
    • initializes an essential attribution update (the LAST essential information is what we pass to the installer, any previous essential information will be overwritten or removed)
    • attaches any existing download-attribution information to download links
  • auto-download (replaces the default init behaviour on pages where downloads should start automatically)
    • if essential information, adds/updates the download attribution cookie before starting download
    • if no essential information, starts download immediately

UPDATED

  • Analytics initialization happens inside consent util for GTM analytics storage
  • Download as default JS no longer contains consent logic (this is already handled with GTM)
  • RTAMO uses a new template with a forced essential campaign and auto-download JS
  • Thanks template used auto-download JS

Rough edges:

  • data validation mixes rtamo-specific validation with general content length validation

Issue / Bugzilla link

https://mozilla-hub.atlassian.net/browse/WT-964 [moz only]

  • links to docs on current behaviour and refactor plan on ticket

Follow up work:

  • chore: remove the override campaign value (fallback and force cover our use cases)
  • decouple the thanks prefix from auto-download

Testing

Consent gating

Analytics

  • should not fire when

    • consent required geo and no pref allowing analytics
    • any geo and DNT
    • any geo and GPC
    • any geo and pref denying analytics
  • should fire

    • consent required geo and pref allowing analytics
    • not consent required geo and no explicit denial signal (DNT/GPC/pref)

Essential

  • should always fire
    • consent required geo and pref allowing analytics
    • not consent required geo and no explicit denial signal (DNT/GPC/pref)
    • consent required geo and no pref allowing analytics
    • any geo and DNT
    • any geo and GPC
    • any geo and pref denying analytics

No download attribution cookie (create)

Analytics

  • Add if consent gates pass

Essential

  • Add if essential forced campaign in HTML
  • Add if checked checkbox for essential campaign

Existing download attribution cookie (update)

Analytics

  • add if consent granted
  • preserve if essential update happens
  • preserve if new analytics data (first touch, pre-existing wins over new)

Essential

  • preserve if analytics update happens
  • add new essential data to replace old (last touch, new wins over pre-existing)

Existing download attribution cookie (remove)

Analytics

  • remove if consent denied

Essential

  • remove if user unchecks essential feature checkbox (i.e. download as default)
  • remove if user leaves essential feature page before clicking download CTA (i.e. smart window)
    • this covers the case where essential data is now stale and would provide an unexpected feature flow on download
    • auto-download page is an exception, where we can assume any essential data is from the immediately preceding page and is the expected feature flow

Auto-download

  • should start download immediately with existing download attribution if no new essential data on page
  • should create/update essential data attribution before download if new essential data is on page (i.e. rtamo)

Regression tests

No user-facing change (unless indicated)

RTAMO auto download (DNT/GPC check no longer needed now we can cleanly remove analytics data according to consent and only apply essential data)

  • might be easiest to fully test once on dev/stage, instructions here [Moz only]
  • this has no impact on prod until we start directing RTAMO links to www.firefox.com (currently directing to www.mozilla.org)

Download as default checkbox (should now work for consent required geos, only applying essential data)

Smart Window

/all after user selections

/thanks auto download

/landing/get with marketing opt-out checkbox

Fallback campaign

@maureenlholland maureenlholland added the WIP Work in progress label Apr 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 45.94595% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.85%. Comparing base (e0d69b9) to head (62c6b39).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
springfield/firefox/views.py 45.94% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1296      +/-   ##
==========================================
- Coverage   79.96%   79.85%   -0.12%     
==========================================
  Files         149      149              
  Lines        9655     9688      +33     
==========================================
+ Hits         7721     7736      +15     
- Misses       1934     1952      +18     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread media/js/base/download-attribution/download-attribution-init.es6.js Outdated
Comment thread media/js/firefox/download/auto-download.es6.js
@maureenlholland maureenlholland force-pushed the WT-964--refactor-stub-attribution branch 2 times, most recently from 8e34134 to 5047f3b Compare April 29, 2026 14:33
Comment thread springfield/firefox/views.py Outdated
@maureenlholland maureenlholland force-pushed the WT-964--refactor-stub-attribution branch 3 times, most recently from 1e53908 to e945dc7 Compare May 7, 2026 17:13
@maureenlholland maureenlholland force-pushed the WT-964--refactor-stub-attribution branch from e945dc7 to bafc009 Compare May 13, 2026 13:46
…analytics data

We always want to include essential data for the download.
We conditionally want to include analytics data (based on consent status).

We also want to simplify the logic of this flow. The existing stub-attribution JS
contains a lot of complex, nested conditionals which has resulted in hard-to-debug
issues and makes further development difficult.

Refactored attribution approach should be gated behind a switch.
When that switch is active, the following changes should be applied:

REMOVED
- stub-attribution-consent (now handled through GTM consent mode)

REPLACED
- stub-attribution (now download-attribution)
- stub-attribution-init (now download-attribution-init)
- thanks, thanks-direct, thanks-init (now auto-download)

ADDED
- download-attribution
  -  completely isolates essential and analytics data (with separate triggers)
- download-attribution-init (the default that runs on all pages)
  -  initializes an essential attribution update (the LAST essential information is what we pass to the installer, any previous essential information will be overwritten or removed)
  - attaches any existing download-attribution information to download links
- auto-download (replaces the default init behaviour on pages where downloads should start automatically)
  - if essential information, adds/updates the download attribution cookie before starting download
  - if no essential information, starts download immediately

UPDATED
- Analytics initialization happens inside consent util for GTM analytics storage
- Download as default JS no longer contains consent logic (this is already handled with GTM)
- RTAMO uses a new template with a forced essential campaign and auto-download JS
- Thanks template used auto-download JS
We can trigger initAnalytics based on GTM consent analytics storage

This allows consent logic to live in one place and ensures it is
consistently applied for analytics download attribution.

The search params JS has moved into the site bundle to ensure it is
defined at the time the GTM logic runs. Without this change, analytics
initialization would always fail at download attribution functional
requirements check.

In future, we may create a custom GTM trigger that fires based on the
analytics storage consent state.
This is a combination of thanks, thanks-init, and thanks-direct logic

Copy-pasted with superficial changes:
- shouldAutoDownload
- getDownloadURL
- beginFirefoxDownload
- onSuccess
- onTimeout

Auto-download pages will not use the default download attribution init
logic from the stub attribution block.

These pages will only create/update download attribution if there is essential
data to pass to the download installer. Otherwise, they will attempt to start
download immediately.

There is a timeout to prevent an overly long wait before download auto-starts
(i.e. if the stub attribution service response is slow). This means we prioritize
a timely download over passing essential information.
This applies to all except auto-download pages. On those pages, we
can assume the user clicked to download from the preceding page, and
the preceding page set all necessary attribution data.

This change also removes fallback values in python code because we
cannot reliably know if marketing data is allowed at this point.
We should only apply defaults for analytics values once we confirm
there is a ga4 client id value

Otherwise, consider essential only and pass nothing but the campaign
@maureenlholland maureenlholland force-pushed the WT-964--refactor-stub-attribution branch from bafc009 to b7b3b92 Compare May 13, 2026 14:44
To recreate:
- Consent required geo goes to /landing/get and accepts analytics
from banner
- Analytics information is added to download attribution
- Consent required geo goes to /privacy/websites/cookie-settings and
updates analytics preference to denied
- Analytics information should be removed from download attribution

This is commented as a temporary workaround because we are still
conditionally loading GTM (and GTAG initialization is related to
that). This means there are pages where GTAG is not defined but we
might still need to remove pre-existing analytics information.

Moving the removal action above the GTAG check ensures it always runs
when consent is denied.
@maureenlholland
Copy link
Copy Markdown
Collaborator Author

TODO:

  • address any legit copilot feedback
  • add tests for adding the attribution params to download links (and updating and removing)
  • debug missing GA4 on demo

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a refactored “download attribution” flow (renaming/replacing parts of the legacy “stub attribution” approach) with a goal of cleanly separating essential attribution data from analytics attribution data, and wiring the new behavior behind a Waffle switch.

Changes:

  • Added a new DownloadAttribution client module with separate essential vs analytics triggers, plus an init bundle and an auto-download bundle.
  • Updated templates/bundles to conditionally load new v2 JS bundles (download attribution, auto-download, download-as-default v2, firefox_all v2) when the switch is enabled.
  • Added Playwright coverage for create/update/remove behaviors of the new attribution cookies and service calls.

Review completed using the repository’s custom Copilot instructions and AGENTS.md guidance.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
tests/playwright/specs/download-attribution/create.spec.js Adds Playwright coverage for creating analytics/essential attribution cookies and consent gating.
tests/playwright/specs/download-attribution/update.spec.js Adds Playwright coverage for “preserve/override” update rules between essential and analytics attribution.
tests/playwright/specs/download-attribution/remove.spec.js Adds Playwright coverage for removing analytics and essential attribution under consent/flow changes.
springfield/privacy/templates/privacy/cookie-settings-flare26.html Adds a test id hook used by Playwright to target the analytics consent UI.
springfield/firefox/views.py Refactors server-side stub attribution signing defaults behind a switch; updates template selection for direct downloads.
springfield/firefox/templates/firefox/download/rtamo.html New RTAMO thanks template that forces an essential campaign via <html> attributes.
springfield/firefox/templates/firefox/download/desktop/thanks.html Switches /thanks behavior to use auto-download JS and suppress attribution init when refactor is enabled.
springfield/firefox/templates/firefox/download/basic/thanks.html Same as above for the basic thanks template.
springfield/firefox/templates/firefox/all/base.html Switches /all JS bundle to a v2 initializer when refactor is enabled.
springfield/cms/templates/cms/thanks_page.html Switches CMS thanks pages to auto-download and suppresses attribution init when refactor is enabled.
springfield/cms/templates/cms/base-flare26.html Conditionally swaps stub-attributiondownload-attribution and download_as_defaultdownload_as_default-v2.
media/static-bundles.json Registers new v2 bundles and repositions search-params into the base bundle.
media/js/firefox/download/desktop/download-as-default-v2.es6.js New download-as-default integration that updates essential attribution on checkbox changes.
media/js/firefox/download/desktop/download-as-default-init-v2.es6.js Initializes the new download-as-default v2 module.
media/js/firefox/download/auto-download.es6.js New auto-download flow that can force an essential attribution update before starting a download.
media/js/firefox/all/all-init-v2.es6.js New /all initializer that re-applies attribution to partially-fetched download links.
media/js/base/download-attribution/download-attribution.es6.js New core implementation for essential+analytics attribution with separate raw cookies and re-signing.
media/js/base/download-attribution/download-attribution-init.es6.js Default init that refreshes essential attribution and applies signed attribution to links.
media/js/base/consent/utils.es6.js Hooks analytics consent changes to attribution analytics init/removal.

Comment thread springfield/firefox/views.py Outdated
Comment thread media/js/base/consent/utils.es6.js
Comment on lines +7 to +9
window.dataLayer = window.dataLayer || [];

/**
Comment on lines +17 to +29
/**
* Update essential data based on checkbox state
*/
if (!checked) {
window.Mozilla.DownloadAttribution.initEssential(null, () => {
DownloadAsDefault.bindEvents();
});
} else {
window.Mozilla.DownloadAttribution.initEssential(
DownloadAsDefault.CAMPAIGN,
() => {
DownloadAsDefault.bindEvents();
}
Comment on lines +161 to +178
if (
Mozilla.DownloadAttribution !== undefined &&
document.documentElement.hasAttribute(
'data-stub-attribution-campaign-force'
)
) {
// Custom success and timeout callbacks are only relevant for direct attribution
// (i.e. when we have updated the cookie on the auto-download page)
Mozilla.DownloadAttribution.successCallback = onSuccess;
Mozilla.DownloadAttribution.timeoutCallback = onTimeout;
// Don't wait too long before starting download
// (even if it means we have to leave the essential information out)
timeout = setTimeout(onTimeout, 2000);
Mozilla.DownloadAttribution.initEssential();
} else {
// Otherwise, we can assume an existing cookie has latest data
Mozilla.DownloadAttribution.applyAttributionDataToLinks();
beginFirefoxDownload();
Comment on lines +343 to +356
const existingCookies = await page.context().cookies();
const existingEssentialCookie = existingCookies.find(
(c) => c.name === 'moz-download-attribution-essential-raw'
);
const existingAnalyticsCookie = existingCookies.find(
(c) => c.name === 'moz-download-attribution-essential-raw'
);
expect(existingEssentialCookie).toBeDefined();
const existingEssentialCookieData = JSON.parse(
decodeURIComponent(existingEssentialCookie.value)
);
const existingAnalyticsCookieData = JSON.parse(
decodeURIComponent(existingAnalyticsCookie.value)
);
const acceptButton = page.getByTestId(
'consent-banner-accept-button'
);
acceptButton.click();
const acceptButton = page.getByTestId(
'consent-banner-accept-button'
);
acceptButton.click();
Comment on lines +220 to +236
// change consent status
const analyticsCategory = await page.getByTestId(
'cookie-consent-analytics'
);
const disagreeOption =
await analyticsCategory.getByLabel(/I do not agree/i);
await disagreeOption.click();
const saveButton = await page.getByRole('button', {
name: /Save changes/i
});
await saveButton.click();

// Confirm stub attribution service call uses essential campaign param only
expect(capture.params).not.toBeNull();

expect(capture.params.utm_campaign).toBe(
existingEssentialCookieData.utm_campaign
Comment on lines +258 to +283
await page.waitForFunction(() => {
return document.cookie
.split(';')
.some((c) =>
c
.trim()
.startsWith(
'moz-download-attribution-analytics-raw='
)
);
});

// Confirm new essential cookie added
const cookies = await page.context().cookies();
const essentialCookie = cookies.find(
(c) => c.name === 'moz-download-attribution-essential-raw'
);
expect(essentialCookie).toBeDefined();

// Confirm stub attribution service params preserve pre-existing analytics data
// (with exception of campaign)
expect(capture.params).not.toBeNull();

expect(capture.params.utm_source).toBe(
existingAnalyticsCookieData.utm_source
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

WIP Work in progress

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants