Skip to content

Add "Enabled only with ?promoted=1" mode to marketing attribution field#1351

Open
slightlyoffbeat wants to merge 1 commit into
mainfrom
promoted-landing-on-param
Open

Add "Enabled only with ?promoted=1" mode to marketing attribution field#1351
slightlyoffbeat wants to merge 1 commit into
mainfrom
promoted-landing-on-param

Conversation

@slightlyoffbeat
Copy link
Copy Markdown
Contributor

Summary

Converts FreeFormPage2026's enable_marketing_attribution boolean into a three-state marketing_attribution_mode CharField:

  • Disabled (default) — normal page, no promoted treatment
  • Enabled — always treat the page as promoted (current behavior)
  • Enabled only when ?promoted=1 in URL — new

The "param" mode lets a single landing page serve both organic and paid traffic. The promoted treatment (data-promoted-page attribute, marketing opt-out checkbox, consent banner allowlisting, stub attribution) only renders for visitors arriving via ?promoted=1. Organic visitors see a normal page without the "Share how you discovered Firefox" checkbox.

Builds on PR #1189 (merged) — no JS changes; the existing isPromotedPage() / shouldShowCheckbox() infrastructure continues to work via the data-promoted-page attribute.

How it works

A new is_promoted_page(request) method on PromotedPageMixin decides per-request whether to render the promoted treatment:

Mode URL has ?promoted=1 data-promoted-page Bundles Checkbox HTML noindex
Disabled not rendered no no no
Enabled "True" yes yes yes
Param yes "True" yes yes no
Param no not rendered no no no

Rows 1 and 4 (Disabled / Param-without-param) render identical HTML — the mode value is preserved server-side but the rendered output is the same. noindex differs by mode: only "Enabled (always)" gets it. "Param" mode keeps the base URL indexable for organic search (Googlebot crawls without query params, so it sees the organic version).

Migration

Two migrations:

  • 0086_freeformpage2026_marketing_attribution_mode_and_more.py — adds the new marketing_attribution_mode CharField. Auto-generated AlterField on the legacy boolean is a functional no-op (Django bookkeeping for removed help_text).
  • 0087_migrate_marketing_attribution.py — data-migrates rows where enable_marketing_attribution=True to marketing_attribution_mode="enabled". Includes forwards/backwards for safe rollback.

The legacy enable_marketing_attribution boolean is kept in the model and DB for one deploy cycle. This avoids create-then-drop in the same PR — old pods running pre-merge code can still query the legacy column harmlessly during a rolling deploy. A follow-up PR will remove it after this one is fully deployed.

Open questions

  1. Wagtail revisions — old page revisions stored in wagtailcore_revision still have enable_marketing_attribution: true but no marketing_attribution_mode. If an editor reverts to a pre-merge revision, the restored page would have marketing_attribution_mode="" (Disabled), losing the promoted setting. Acceptable risk, or worth a revision-migration script?

  2. Cache safety for param mode — pages in "param" mode render different HTML for ?promoted=1 vs no param. The CDN cache key needs to vary by the promoted query string for these URLs (otherwise an organic visitor could see a cached promoted response or vice versa). Worth confirming the CDN config before merging.

  3. No new unit tests — the original enable_marketing_attribution boolean shipped without unit tests, so I matched that pattern. Should we add tests for is_promoted_page() per-mode behavior and the data migration's forwards/backwards? Happy to add in this PR or a follow-up.

  4. Scope — only FreeFormPage2026 uses PromotedPageMixin today. Other page types (HomePage, WhatsNewPage2026) don't. If MarTech wants promoted treatment on those too, that's a one-line model change per page type. Intentional to scope this PR to FreeFormPage2026 only?

  5. Display label wording — "Enabled only when ?promoted=1 in URL" is wordy in the dropdown. Open to suggestions like "Enabled with ?promoted=1 param" or similar.

Risks

  • Cache poisoning (medium, mitigated by CDN config check) — described above
  • Rolling deploy (low) — legacy field stays in the model and DB during this PR's deploy window; old pods can read it harmlessly
  • No JS changes (none) — PR Refactor consent JS to detect promoted pages via HTML attribute inste… #1189's infrastructure is the gate; this PR only changes what the server sends down
  • Migration idempotency (low) — if 0087 fails between 0086 and 0087, the new column is just empty (all pages behave as Disabled). 0087 is safe to re-run.

Next steps

  • Follow-up PR (mechanical): after this PR is fully deployed to production, remove enable_marketing_attribution from PromotedPageMixin, run makemigrations cms, ship 0088_remove_freeformpage2026_enable_marketing_attribution.py. Branch name suggestion: promoted-landing-on-param-cleanup.
  • MarTech coordination: confirm paid campaign URLs going forward will include ?promoted=1 when pointing at marketing_attribution_mode="param" pages.
  • Optional: add unit tests for is_promoted_page() and the data migration (Q3 above).

Test plan

  • pytest springfield/cms/ (558 passed)
  • npm run jasmine (470 specs, 0 failures)
  • pre-commit run --all-files clean (only the expected "untracked migrations" — now resolved)
  • python manage.py makemigrations --check --dry-run exits cleanly
  • Local manual tests for each mode × ?promoted=1 combination (see plan file section "End-to-end manual tests")

Related

Converts FreeFormPage2026's enable_marketing_attribution boolean into
  a three-state marketing_attribution_mode CharField:

  - Disabled (default)
  - Enabled (always promoted)
  - Enabled only when ?promoted=1 in URL

  The new "param" mode lets a single landing page serve both organic and
  paid traffic — the promoted treatment (data-promoted-page attribute,
  marketing opt-out checkbox, consent banner allowlisting, stub
  attribution) only renders for visitors arriving via the ?promoted=1
  query param. Organic visitors see a normal page without the
  "Share how you discovered Firefox" checkbox.

  When mode is "param" and the URL is missing the param, the page
  renders identically to "Disabled" — no data-promoted-page attribute,
  no marketing opt-out bundles, no checkbox HTML. No JS changes
  required; the existing isPromotedPage() / shouldShowCheckbox()
  infrastructure from PR #1189 continues to work via the
  data-promoted-page attribute.

  The legacy enable_marketing_attribution boolean is kept in
  PromotedPageMixin for one deploy cycle to avoid create-then-drop in
  the same PR (per wagtail-snippet skill guidance). A follow-up PR will
  remove it after this one is fully deployed.

  Migration 0086 adds the new field; 0087 data-migrates existing
  True values to "enabled" mode.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

❌ Patch coverage is 85.71429% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.99%. Comparing base (a3c90de) to head (9bbc6b1).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
springfield/cms/models/base.py 84.61% 2 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1351   +/-   ##
=======================================
  Coverage   79.99%   79.99%           
=======================================
  Files         149      149           
  Lines        9666     9678   +12     
=======================================
+ Hits         7732     7742   +10     
- Misses       1934     1936    +2     

☔ 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.

@slightlyoffbeat
Copy link
Copy Markdown
Contributor Author

I should also note that I tucked in an addition of a skill that helps with snippet creation and modification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant