Skip to content

Switch cookie consent runtime to WCP API - #1403

Merged
IEvangelist merged 8 commits into
mainfrom
dapine/wcp-consent-banner-fix
Jul 30, 2026
Merged

Switch cookie consent runtime to WCP API#1403
IEvangelist merged 8 commits into
mainfrom
dapine/wcp-consent-banner-fix

Conversation

@IEvangelist

@IEvangelist IEvangelist commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • replace runtime use of the jop/vanilla cookie-consent integration with WCP''s wcp-consent.js API
  • keep the existing Aspire site UX entry points (header/footer cookie buttons), but wire them to siteConsent.manageConsent()
  • initialize WCP consent with page culture, sync consent-change events, and force a page refresh after consent changes

Why this fixes the scan issue

WCP validation expects WCP consent behavior (WcpConsent + MSCC consent model). The current implementation was using a different consent stack (cc_cookie), which can render a banner for users but still fail WCP banner validation heuristics.

Validation

  • pnpm -C src/frontend lint

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

Frontend HTML artifact ready

The latest frontend build uploaded the frontend-dist artifact for PR #1403. Use the VS Code button below to open this PR with GitHub Artifacts Explorer and browse the built HTML locally.

VS Code: Open PR #1403 artifacts

This comment updates automatically when a new frontend build artifact is uploaded.

@IEvangelist
IEvangelist marked this pull request as ready for review July 27, 2026 16:07
Copilot AI review requested due to automatic review settings July 27, 2026 16:07
@IEvangelist
IEvangelist enabled auto-merge (squash) July 27, 2026 16:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

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 migrates the site’s cookie-consent runtime from the @jop-software/astro-cookieconsent / vanilla-cookieconsent stack to Microsoft WCP’s wcp-consent.js API, keeping the existing header/footer “cookie preferences” entry points but wiring them to siteConsent.manageConsent() and restyling the WCP UI to match Aspire.

Changes:

  • Replace consent initialization/runtime with an inline WCP initializer in Head.astro and update cookie preference buttons to use a new data-cookie-manage-consent hook.
  • Remove the old cookie consent configuration, custom styles, and npm dependencies; add wcp-consent.css and import it via site.css.
  • Adjust Playwright e2e tests to avoid WCP banner overlap and remove tests that asserted old cc_cookie-based consent state.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/frontend/tests/e2e/ui-regressions.spec.ts Removes consent-category assertions tied to the previous cookie-consent stack.
src/frontend/tests/e2e/integrations-gallery.spec.ts Adds a pre-paint CSS suppression to prevent WCP banner overlap in local EU runs.
src/frontend/tests/e2e/helpers.ts Deletes helpers that interacted with the old preferences modal / cc_cookie.
src/frontend/src/styles/wcp-consent.css New WCP banner + preferences-dialog restyling scoped to stable WCP container IDs/roles.
src/frontend/src/styles/site.css Switches imported consent stylesheet to wcp-consent.css.
src/frontend/src/styles/cookieconsent-custom.css Removes old vanilla-cookieconsent customization stylesheet.
src/frontend/src/components/starlight/SocialIcons.astro Updates cookie button to data-cookie-manage-consent.
src/frontend/src/components/starlight/Header.astro Updates header cookie buttons to data-cookie-manage-consent.
src/frontend/src/components/starlight/Head.astro Loads WCP CDN script and initializes consent + consent-change handling and theming sync.
src/frontend/src/components/FooterSocials.astro Updates footer cookie button to data-cookie-manage-consent.
src/frontend/pnpm-lock.yaml Drops @jop-software/astro-cookieconsent and vanilla-cookieconsent from the lockfile.
src/frontend/package.json Removes old cookie-consent dependencies.
src/frontend/config/cookie.config.ts Deletes the old cookie-consent configuration (categories/translations).
src/frontend/astro.config.mjs Removes cookie-consent integration plugin wiring.
Files not reviewed (1)
  • src/frontend/pnpm-lock.yaml: Generated file
Comments suppressed due to low confidence (1)

src/frontend/tests/e2e/ui-regressions.spec.ts:226

  • The PR replaces the cookie-consent runtime but removes the e2e coverage that verified consent choices affected analytics consent/categories. There doesn’t appear to be replacement coverage for the new WCP flow, so regressions in the consent wiring (manage-consent buttons, consent-change reload, etc.) could slip through unnoticed.
test('footer preferences persist theme and keyboard style selections', async ({ page }) => {
  await page.goto('/get-started/aspire-vscode-extension/');
  await dismissCookieConsentIfVisible(page);


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/frontend/src/components/starlight/Head.astro Outdated
Comment thread src/frontend/src/components/starlight/Head.astro
@IEvangelist
IEvangelist marked this pull request as draft July 27, 2026 16:12
auto-merge was automatically disabled July 27, 2026 16:12

Pull request was converted to draft

@IEvangelist IEvangelist left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

🤖 Automated docs-accuracy review (doc-pr-reviewer)

Verdict: COMMENT — no docs-accuracy issues found, because this PR changes no documentation content.

Summary

PR head reviewed 11b5d24aa6943dd71e7e63e679cc2c2a39d3e0ea (base main)
Source-of-truth repos read (Phase A) none needed — no in-scope Aspire claims exist to verify
Claims extracted 0 verifiable Aspire claims (all changes are site infrastructure + a third-party consent library)
Per-verdict counts verified: 0 · verified-with-nuance: 0 · unverifiable: 0 · contradicted: 0 · out-of-scope: n/a
Phase B (doc-tester) not run — 0 changed documentation routes; see reason below

All 14 changed files live under src/frontend/ and are site infrastructure (Astro config, .astro components, CSS, cookie config, package.json/lockfile, and e2e tests). There are no content/docs/** .md/.mdx pages, integration docs, CLI docs, or API-reference changes in this diff, so the skill's docs-accuracy machinery has nothing to check.


Phase A — Claim verification

Result: no verifiable Aspire documentation claims found.

Every factual/API assertion in this diff is about Microsoft's WCP (Web Consent Platform) wcp-consent.js runtime — e.g. window.WcpConsent.init(...), wcp.themes.dark/light, siteConsent.getConsent(), siteConsent.onConsentChanged(...), siteConsent.isConsentRequired, siteConsent.manageConsent(), the theme-changed event, and the stable container IDs #wcpConsentBannerCtrl / #wcpCookiePreferenceCtrl.

WCP is not owned by any of this skill's sources of truth (microsoft/aspire, CommunityToolkit/Aspire, Azure/azure-sdk-for-net), so those repos cannot confirm or contradict this surface. Per the skill's protocol these are knowledge gaps, not verifiable claims — I have not fabricated a verdict for them. If any external validation is desired, the authoritative source is Microsoft's WCP/1DS consent documentation and the live CDN payload at https://wcpstatic.microsoft.com/mscc/lib/v2/wcp-consent.js, none of which are in scope for this reviewer.

No contradicted or unverifiable in-scope claims → no inline claim comments to post.


Phase B — Doc-tester results

Not run. Reason: 0 documentation routes are affected by this diff, so there is no doc content to exercise as a "blind new user."

The only behavioral change is the site-wide cookie-consent banner. Note (from the PR's own code comments) that a doc-tester render pass would produce little signal here regardless:

  • The WCP banner is geo-gated — the PR states it "never renders in CI" and is only shown to certain regions (e.g. EU), so it would not reliably appear on a local dev server.
  • The banner/prefs UI is injected by a live external CDN (wcpstatic.microsoft.com), whose payload is outside the repo and outside the skill's sources of truth.

Per this session's guidance, no local pnpm dev server or Playwright automation was started for this non-doc PR.


Notes for the author (non-blocking, outside skill scope)

This reviewer only judges documentation accuracy; it does not assess the correctness of the WCP integration itself, CSS behavior, or the e2e test changes. Those are best covered by the frontend e2e suite and a human/functional review of the WCP banner in a geo-region where it renders. This review makes no claim that the WCP wiring is correct — only that there is nothing here for a docs-accuracy check to verify.

Generated by the doc-pr-reviewer skill. Phase A read no source-of-truth repos (no in-scope claims); Phase B was not run (no changed doc routes).

IEvangelist added a commit that referenced this pull request Jul 27, 2026
Two fixes for the Copilot review comments on #1403, both in the WCP
initializer in Head.astro:

- Remove the toLegacyCategories / syncConsentState path that only ever
  wrote window.__aspireConsentCategories, a global nothing in the frontend
  (or anywhere in the repo) reads. This also drops the astro:page-load
  handler and getConsent() calls that existed solely to feed that dead
  global, removing a runtime side effect with no consumer.

- Resolve the WCP init theme through toWcpThemeName instead of
  dereferencing wcp.themes.dark/light directly. If a future WCP payload
  omits or renames themes, the direct access threw, the throw was swallowed
  by the surrounding try/catch, and initialization stopped permanently with
  no retry. toWcpThemeName already falls back to a plain string, and init
  now reschedules itself on exceptions within the existing bounded window.

Validated locally against the WCP CDN: consent still initializes (US ->
consent not required, manage-cookies buttons hidden), the removed global is
now undefined, and there are no console errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
@IEvangelist
IEvangelist marked this pull request as ready for review July 27, 2026 18:17
@IEvangelist
IEvangelist enabled auto-merge (squash) July 27, 2026 18:17

@adamint adamint left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this stops analytics site-wide — worth checking before it merges.

The consent decision isn't wired to anything

config/head.attrs.ts:70-96 ships all three analytics scripts as type: 'text/plain' with data-category="analytics". That's the vanilla-cookieconsent activation contract — manageScriptTags defaults to true and the deleted config never overrode it, so the old stack was the thing flipping them to executable. This PR removes that and nothing takes over: grepping src/ for getConsent|getConsentFor|text/plain only hits head.attrs.ts, and Head.astro:363-399 never calls siteConsent.getConsent().

Production still serves <script type="text/plain" src="/scripts/analytics/1ds.js" defer data-category="analytics">, so after this lands 1DS/App Insights stops collecting everywhere — including regions where consent isn't required. The banner would be recording a choice that nothing acts on, which is a worse place to be than the scan failure we're fixing.

Looks like it was closer earlier: 1a62a356 deleted the getConsent() calls while resolving the Copilot thread, rather than wiring up the consumer.

The banner covers the header

Head.astro:310-315 inserts the placeholder as document.body.firstChild with position:fixed; top:0; z-index:2147483647, and nothing adds compensating padding-top. Our header is also fixed at top:0 with z-index:10 (site.css:1570), so while the banner is up the logo, search, theme toggle and the Manage cookies button itself sit underneath it. Tabbing into the header puts focus behind the banner, which is 2.4.11. At 320px it takes a big slice of the viewport and can't be scrolled away.

Reject is visually weaker than Accept

WCP renders all three buttons with the same class on purpose. wcp-consent.css:104-129 makes Accept a filled CTA and Reject a transparent ghost whose only affordance is a color-mix(... 30%, transparent) border — that computes to 1.89:1 light / 2.71:1 dark, so it fails 1.4.11 too. Given the whole point here is passing WCP validation, de-emphasizing Reject seems likely to work against us.

The deleted tests were the ones that would have caught the first issue

cookie consent reject-all keeps analytics disabled and cookie preferences and accept-all enable analytics tracking consent (ui-regressions.spec.ts:228-250) were the only assertions that consent was recorded and that the analytics category followed the user's choice. openCookiePreferences is gone too, which was the one asserting the button actually opens a dialog. The single surviving reference at :104-108 blocks the WCP CDN, so it asserts the button exists while guaranteeing it's inert.

Could we stub window.WcpConsent and assert the four buttons call manageConsent(), plus that the analytics scripts become executable after accept? That'd cover the gap.

Smaller things

  • Head.astro:277-287 calls preventDefault() unconditionally then does nothing when WCP didn't load. The failsafe at :236-249 deliberately keeps the buttons visible and init gives up silently after 15s, so an EU visitor with an ad blocker gets a completely dead button. Hide them on give-up, or swap to a privacy-statement link?
  • Head.astro:319-361 — I don't think theme-changed is a WCP event. I pulled wcp-consent.js off the CDN and it has zero occurrences of it, and no window.addEventListener calls at all. So notifyWcpThemeChanged/watchThemeChanges look like ~40 lines of dead code plus a permanent MutationObserver on <html>. (Caveat: I could only reach the not-required payload from here.)
  • Head.astro:386 and :389 register onConsentChanged twice — once via init's 4th arg, once on the instance — and the bundle fires both lists, so that's two setTimeout(reload, 0) per interaction. The reload is also unconditional; dismissing the banner mid-page loses scroll position and any open search.
  • .github/astro.instructions.md:341 still describes the old stack and tells agents to dismiss a bottom-right modal by clicking "Reject all". That's now wrong on position, on region-gating, and on the third button's label (it's "More info").
  • nit: frontend-build.yml:88-95 still sets E2E_TESTS: 1 with a comment pointing at the deleted cookie.config.ts.

For what it's worth, the integration mechanics look right — I checked that there's no CSP anywhere in the repo, no <ClientRouter /> (so no double-registration across navigations), the WcpConsent.init signature matches, all 15 locales resolve safely, the nth-of-type button mapping is correct, the scroll lock does release, and package.json/lockfile are consistent. It's the consumer side that's missing.

IEvangelist and others added 6 commits July 27, 2026 21:09
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
Fully restyle the WCP cookie banner and preferences dialog to match the
Aspire theme via the team-owned wcp-consent.css (our colors, spacing,
WCAG AA contrast, and the brand icon). The dialog now re-themes live when
the user toggles light/dark, and the underlying page is scroll-locked
while the preferences dialog is open.

Remove the last remnants of the old vanilla-cookieconsent integration:
- delete config/cookie.config.ts and the old cookieconsent-custom.css
- drop @jop-software/astro-cookieconsent + vanilla-cookieconsent deps
- remove the 2 obsolete behavioral cookie e2e tests and their dead
  helpers; the WCP banner is geo-gated/CDN-loaded and can't be exercised
  in CI, so compliance is verified by the WCP scan instead
- rework the integrations-gallery banner suppression to hide the WCP
  banner element instead of pre-seeding the removed cc_cookie shape

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
WCP reports whether consent is required for the visitor's region. Where it
is not (e.g. the US) there is nothing to manage, so hide the four "Manage
cookies" buttons instead of leaving inert controls on the page.

The WCP runtime sets a data-consent-not-required flag on <html> once it
knows the region; a CSS rule hides .cookie-consent-btn when the flag is
present. The flag is primed from localStorage before paint so returning
visitors in non-required regions don't see the buttons flash in. Defaulting
to visible keeps this failsafe: if WCP is slow or blocked we never hide a
control a required region needs. display:none also makes the site tour
auto-skip its cookie-preferences step, so no tour changes are needed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
The compact-header-order regression asserts the full set of header
controls, including the "Manage cookies" button. That button is now
region-gated: WCP hides it where consent isn't required, which is how most
CI runner IPs resolve, so the test could see a shorter header and fail.
Block the WCP CDN for this test so it always exercises the failsafe default
(every control present), keeping the expected order stable regardless of
the runner's geo. The site-tour e2e test needs no change; it advances until
"Finish" without counting steps, so it tolerates the cookie step being
skipped.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
Two fixes for the Copilot review comments on #1403, both in the WCP
initializer in Head.astro:

- Remove the toLegacyCategories / syncConsentState path that only ever
  wrote window.__aspireConsentCategories, a global nothing in the frontend
  (or anywhere in the repo) reads. This also drops the astro:page-load
  handler and getConsent() calls that existed solely to feed that dead
  global, removing a runtime side effect with no consumer.

- Resolve the WCP init theme through toWcpThemeName instead of
  dereferencing wcp.themes.dark/light directly. If a future WCP payload
  omits or renames themes, the direct access threw, the throw was swallowed
  by the surrounding try/catch, and initialization stopped permanently with
  no retry. toWcpThemeName already falls back to a plain string, and init
  now reschedules itself on exceptions within the existing bounded window.

Validated locally against the WCP CDN: consent still initializes (US ->
consent not required, manage-cookies buttons hidden), the removed global is
now undefined, and there are no console errors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
Implements every point from adamint's review on #1403.

Critical:
- Analytics never loaded under WCP. The three analytics scripts still ship
  inert (type="text/plain" data-category="analytics") from the previous
  cookie library, but WCP is a consent-collection API and does not manage
  script tags, so nothing re-activated them. Head.astro now bridges the gap:
  on init it reads siteConsent.getConsent().Analytics and, when granted
  (default where consent isn't required, e.g. the US), promotes each inert
  script to an executable clone. onConsentChanged reloads, so a later grant
  re-applies via the same path. Verified locally: all three scripts flip
  from text/plain to active on the US default-grant page.

Accessibility:
- The geo-gated banner is a fixed top strip that painted over the fixed
  Starlight header (z-index 10), hiding nav/search (WCAG 2.4.11). A new
  ResizeObserver (trackBannerHeight) publishes the banner's live height as
  --wcp-banner-height, and CSS translates .page (the body-level wrapper that
  contains every fixed control) down by that amount only while the banner is
  in the DOM. It is a no-op with no banner (0px), so the US is unaffected.
  Verified: injecting a 60px banner shifts the header to top:60 uncovered.
- Reject / Manage-cookies ghost buttons used a 30% border that failed WCAG
  1.4.11 non-text contrast (1.90:1 light / 2.70:1 dark). Bumped to 55%
  (3.73:1 / 5.82:1) on the banner and the dialog reset button.

Cleanups:
- Guard openManageConsent so preventDefault only fires when WCP's dialog
  actually opened, not on every click.
- Replace the dead `theme-changed` CustomEvent (WCP has no such listener)
  with the real re-theme hook, siteConsent.applyTheme(themeObject), driven
  by the existing data-theme observer.
- Register onConsentChanged once via init's callback argument; drop the
  redundant siteConsent.onConsentChanged(...) call that double-registered it
  (WCP stores callbacks in a list, so it fired two reloads per change).
- Refresh the Cookie Consent + screenshot sections of astro.instructions.md
  to describe WCP (geo-gated top strip, Accept/Reject/More info, MSCC
  cookie) instead of the removed jop-software library.
- Drop the dead E2E_TESTS env (read nowhere) from frontend-build.yml and the
  two playwright configs, plus the stale cookie.config.ts comment.

Tests:
- Restore the consent regression coverage adamint flagged as deleted. New
  tests/e2e/cookie-consent.spec.ts stubs window.WcpConsent (blocking the real
  CDN) so it can assert the behavior that only exists at runtime: analytics
  scripts are promoted from inert to executable when Analytics consent is
  granted, stay inert when it is withheld, and every server-rendered
  manage-cookies control routes a click to siteConsent.manageConsent(). Green
  across the desktop, tablet, and mobile projects.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
@IEvangelist
IEvangelist force-pushed the dapine/wcp-consent-banner-fix branch from 1a62a35 to ac4720f Compare July 28, 2026 03:25
@IEvangelist

Copy link
Copy Markdown
Member Author

Thanks for the thorough pass, @adamint — the substantive points were right, the consumer side was genuinely missing. All addressed in ac4720fb.

The consent decision isn't wired to anything

Fixed. WCP is consent-collection only — it doesn't flip script tags the way vanilla-cookieconsent's manageScriptTags did — so Head.astro now bridges it: on init it reads siteConsent.getConsent().Analytics and, when granted (the default where consent isn't required, e.g. the US), promotes each inert type="text/plain" data-category="analytics" script to an executable clone. onConsentChanged reloads, so a later grant re-applies via the same path. Verified locally that all three scripts (1DS, track, ms.analytics-web) flip from text/plain to active on the US default-grant page.

The banner covers the header

Fixed. A ResizeObserver (trackBannerHeight) publishes the banner's live height as --wcp-banner-height, and CSS translates .page — the body-level wrapper that contains every position:fixed control (header, sidebars, TOC) — down by exactly that amount, but only while the banner is in the DOM. It's a 0px no-op when there's no banner, so the US is untouched. Verified: injecting a 60px banner shifts the header to top:60 fully uncovered.

Reject is visually weaker than Accept

Contrast fixed; prominence intentionally kept. The ghost border went 30% → 55% color-mix, which measures 3.73:1 light / 5.82:1 dark, clearing 1.4.11's 3:1 on both the banner and the dialog reset button. I kept the ghost treatment rather than promoting Reject to an equal filled CTA: WCP's scan checks non-text contrast, not prominence parity, and equal prominence isn't a 1.4.11 requirement. Easy to make Reject a filled button too if we'd prefer that — just a design call.

The deleted tests

Restored as a stub-based e2e (tests/e2e/cookie-consent.spec.ts), the way you suggested. It stubs window.WcpConsent (blocking the real CDN) and asserts the runtime behavior: analytics scripts become executable when Analytics is granted, stay inert when it's withheld, and every server-rendered manage-cookies control routes a click to manageConsent(). Green across the desktop/tablet/mobile projects.

Smaller things

  • preventDefault — now guarded so it only fires when WCP's dialog actually opened; a blocked/slow EU visitor keeps the button's default behavior instead of a dead click.
  • theme-changed — correct, it's not a WCP event (zero occurrences in the CDN bundle). Replaced the dead dispatch with the real hook, siteConsent.applyTheme(themeObject), driven by the existing data-theme observer.
  • double onConsentChanged — dropped the redundant instance registration; it's registered once via init's 4th arg now, so no more two reloads per change. The single remaining reload is intentional (re-applies consent-gated behavior consistently); the scroll/search reset is a known trade-off I'm happy to revisit by re-running the analytics bridge in place instead of reloading.
  • astro.instructions.md — rewritten for WCP (geo-gated top strip, Accept / Reject / More info, MSCC cookie) instead of the removed jop-software modal.
  • E2E_TESTS / stale comment — removed from frontend-build.yml and both playwright configs.

Thanks also for verifying the mechanics (no CSP, no <ClientRouter />, init signature, locale resolution, scroll lock, lockfile) — that narrowed it down. Left this as a top-level reply since the review had no inline threads to resolve.

@JamesNK
JamesNK disabled auto-merge July 28, 2026 06:36

@JamesNK JamesNK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Approved with two non-blocking inline comments: please clarify the intended execution-order contract for the dynamically activated analytics scripts, and reconcile the theme update path with the live WCP v2 siteConsent API.

Comment thread src/frontend/src/components/starlight/Head.astro
Comment thread src/frontend/src/components/starlight/Head.astro

@adamint adamint left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The WCP migration itself looks good — the old library is fully removed, the bridge is clean, and the site degrades gracefully when the CDN is blocked. But I can't approve this yet: JamesNK flagged two things that are both real, and I went and measured them.

1. Analytics is dead on the default path.

activateAnalyticsScripts() rebuilds each inert script with document.createElement('script') and copies attributes, but never sets next.async = false. Dynamically-created scripts default to async = true, and the copied defer is a no-op for non-parser-inserted scripts, so execution order is gone. The three scripts are order-dependent and each bails silently — 1ds.js:9 returns if oneDS is undefined, track.js:4 returns if window.analytics.__initialized isn't set.

I tested this with the real built assets and the real 140 KB 1DS SDK, served so location.origin === 'https://aspire.dev' (needed, because 1ds.js:4 early-returns on any other origin — worth knowing, it means localhost testing can never exercise this path).

Before, with consent granted:

{ activated: true, hasAnalytics: "undefined", initialized: <absent>, hasOneDS: "object" }
[track] Analytics not initialized, skipping event tracking setup.

Fetch completion order: 1ds.js 169ms, track.js 169ms, SDK 364ms. The two tiny same-origin scripts win the race, so oneDS doesn't exist yet when 1ds.js runs.

After adding exactly one line, next.async = false;:

{ activated: true, hasAnalytics: "object", initialized: true, hasOneDS: "object" }
[track] Event tracking bound.

Fetch order was identical (SDK still last). Only async changed. 3/3 runs dead unpatched, 3/3 initialized patched — deterministic, not a race we got unlucky on.

The reason this is easy to miss is that all three scripts still fetch successfully, so a network-panel check looks perfectly green while nothing initializes. And this is the default path — isConsentRequired=false for US traffic means analytics is granted automatically, so it's most of the traffic, not an edge case.

So the suggestion of adding a comment saying ordering is unimportant would be the wrong way out — ordering is load-bearing.

2. applyTheme is being called with the wrong type, so it always throws.

applyWcpTheme() passes resolveWcpTheme(...), which returns wcp.themes.dark / wcp.themes.light — an object — on exactly the path that's guarded by if (!wcp || !wcp.themes) return;. I pulled the live library to check what it actually wants:

e.prototype.applyTheme=function(e){
  if(!this.themes[e]) throw new Error("Theme not found error");
  var t=this.themes[e]; s.applyTheme(t)
}
// themes={light:…,dark:…,"high-contrast":…}

It's a string key. this.themes[<object>] is undefined, so it throws every time, and withSiteConsent's try/catch swallows it. The comment above resolveWcpTheme says applyTheme "reads color fields off the theme OBJECT" — that's true of the module-private s.applyTheme, but the public method on the prototype takes a key. So the WCP-internal re-theme never actually happens.

siteConsent.applyTheme(isDark ? 'dark' : 'light') should do it. Also worth noting the 5th theme arg to wcp.init(...) is ignored by this build — init=function(e,t,n,r,i,a) never references i — so if we're relying on it for initial theming, we aren't getting it.

Both of these are masked by the e2e stub, which defines applyTheme(){} accepting anything and stubs out the script bodies. Not a criticism of using a stub — the real banner is server-side IP-geo-gated and genuinely can't be driven from a US runner — but it's worth knowing the stub can't catch either of these.

Non-blocking:

  • The new spec covers granted and denied but not the consent-changed/reload path; the stub ignores init's 4th arg (onConsentChanged). The removed ui-regressions tests did cover accept/reject persistence, so there's a small net loss there.
  • wcp-consent.css comments button:nth-of-type(3) as "Manage cookies", but the live banner's third action is "More info" (astro.instructions.md has it right). Just the CSS comment.
  • pnpm-lock.yaml bumps a transitive satteri 0.9.3 → 0.9.4 with no package.json change — intended, or regeneration drift?

Everything else checked out: production build ran clean (13,857 pages), the real CDN loads with zero console errors, all manage-consent controls route to manageConsent(), analytics stays inert when consent is withheld, and the site still renders with the CDN blocked. cookie-consent.spec.ts was 9/9.

Happy to approve once async = false and the applyTheme argument are sorted.

IEvangelist and others added 2 commits July 28, 2026 10:48
adamint's CHANGES_REQUESTED (with JamesNK's two inline notes) caught two real
bugs the e2e stub had masked. Both verified against the live library and fixed.

1. Analytics was dead on the default (US) path. activateAnalyticsScripts()
   rebuilds each inert script with document.createElement('script') but never
   reset `async`. Dynamically-created scripts default to async=true and the
   copied `defer` is a no-op for them, so the two tiny same-origin scripts
   (1ds.js, track.js) won the race against the ~140 KB 1DS SDK and each bailed
   silently (1ds.js returns without `oneDS`; track.js returns without
   `window.analytics.__initialized`). Set `next.async = false` so they execute
   in insertion (document) order. Since consent isn't required in the US,
   Analytics is auto-granted there, so this was most traffic, not an edge case.

2. applyTheme always threw. WCP v2's public `siteConsent.applyTheme(name)`
   takes a string theme KEY and looks it up in its internal map; we were
   passing `wcp.themes.dark/.light` (an object), so `this.themes[object]` was
   undefined -> "Theme not found error", swallowed by withSiteConsent's
   try/catch. resolveWcpTheme now returns the 'dark'|'light' string for both
   applyTheme and init, so the WCP-internal re-theme actually runs on toggle.
   (init's 5th theme arg is ignored by the current CDN build; our CSS overrides
   theme the visible surfaces regardless.)

Tests: strengthen the stub so it can no longer hide either bug and add the
missing coverage adamint flagged.
- applyTheme now validates its argument (string key in the themes map) and
  throws otherwise, exactly like the live library; a permissive stub could not
  catch a wrong-argument regression.
- New assertion: every promoted analytics script has async===false.
- New test: toggling data-theme drives applyTheme with the string key.
- New test: WCP's onConsentChanged (init's 4th arg, now captured by the stub)
  reloads the page — restores the accept/reject-persistence coverage lost with
  the old ui-regressions tests.
Verified the strengthened guards fail when either fix is reverted, and pass with
both applied: cookie-consent.spec.ts is 15/15 across desktop/tablet/mobile.

Also:
- Revert an unintended pnpm-lock transitive drift (satteri 0.9.3 -> 0.9.4) so
  the lockfile diff is only the removed cookie-consent packages.
- Fix a stale CSS comment: the banner's third action is "More info", not
  "Manage cookies".

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
Brings the branch up to date with main (security dependency remediation #1392
+ interaction-service docs #1408) so it's no longer behind or conflicting.

Conflicts were limited to package.json and pnpm-lock.yaml, where main's
dependency bumps overlapped this PR's removal of the cookie-consent library.
Resolved by keeping main's bumped versions and re-removing the two cookie
packages (@jop-software/astro-cookieconsent and vanilla-cookieconsent) plus
their lockfile entries. Verified the resolved package.json has an identical
dependency-name set to the branch (only versions differ) and that neither file
retains any cookie-consent reference.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 4182d84d-5dc1-4c74-be09-15995a872350
@IEvangelist

Copy link
Copy Markdown
Member Author

@adamint — you nailed both. They're real, both fixed in 81eb3ffb, and I strengthened the stub so CI catches them now instead of hiding them. Also brought the branch current with main (0111d49a) so it's mergeable again.

1. Analytics dead on the default path — fixed

Added exactly next.async = false in activateAnalyticsScripts(), right after the attribute copy, so the SDK → 1ds → track chain runs in document order instead of the dynamically-created scripts defaulting to async and racing. Matches your measurement — all three fetch green, nothing initializes without it. The comment now says ordering is load-bearing (not the reverse).

2. applyTheme always throwing — fixed

resolveWcpTheme now returns the string key ('dark' | 'light'), so siteConsent.applyTheme(key) hits this.themes[key] instead of this.themes[<object>] → no more swallowed "Theme not found error". Dropped the stale "reads color fields off the theme OBJECT" comment (that described the module-private s.applyTheme, not the public prototype method). Also noted the ignored 5th init arg — our CSS themes the visible surfaces regardless, and applyWcpTheme() re-syncs WCP's own nodes on toggle.

The stub couldn't catch either — fixed that too

The stub now mirrors the real API: applyTheme throws on any non-string-key argument, and init exposes its 4th-arg onConsentChanged. On top of the granted/denied tests I added:

  • an assertion that every promoted analytics script has async === false;
  • a theme test that toggles data-theme and asserts applyTheme was called with the string key;
  • a reload test that drives onConsentChanged and asserts the page reloads — restoring the accept/reject-persistence coverage the removed ui-regressions tests had.

I verified these fail when I revert either fix and pass with both applied: cookie-consent.spec.ts is 15/15 across desktop/tablet/mobile.

Non-blocking

  • CSS comment — fixed; the third banner action is "More info", not "Manage cookies".
  • satteri lockfile bump — regeneration drift; I reverted it. It's now moot anyway: merging main (which bumps satteri to 0.9.5) means the branch carries only the cookie-consent removals on top of main's deps.

JamesNK's two inline notes (Head.astro:307 async, :415 applyTheme) are the same two fixes — replying + resolving those threads.

New SHAs: 81eb3ffb (fixes) and 0111d49a (merge with main). Thanks for measuring these against the live assets — that made them unambiguous.

@IEvangelist
IEvangelist requested a review from adamint July 29, 2026 18:51
@IEvangelist
IEvangelist dismissed adamint’s stale review July 30, 2026 11:52

Changes addressed concern

@IEvangelist
IEvangelist merged commit 2de702c into main Jul 30, 2026
10 checks passed
@IEvangelist
IEvangelist deleted the dapine/wcp-consent-banner-fix branch July 30, 2026 11:52
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.

4 participants