Skip to content

New Module: DAA AdChoices Signal#15138

Open
rvidakovic wants to merge 5 commits into
prebid:masterfrom
rvidakovic:adchoices-module
Open

New Module: DAA AdChoices Signal#15138
rvidakovic wants to merge 5 commits into
prebid:masterfrom
rvidakovic:adchoices-module

Conversation

@rvidakovic

@rvidakovic rvidakovic commented Jun 18, 2026

Copy link
Copy Markdown

Type of change

  • Feature

  • Does this change affect user-facing APIs or examples documented on http://prebid.org? — yes, docs PR to follow (see below)

Description of change

Adds a new standalone module, adChoices (modules/adChoices.ts), that reads the DAA AdChoices Signal in the browser and conveys it in the OpenRTB bid stream as the community extension regs.ext.adchoices, per Appendix 5 of the specification.

How it works:

  • On inclusion the module automatically listens for the signal from the DAA's Protect My Choices (PMC) extension using the documented postMessage protocol (ExtensionLoadedGetAdPreferencesAdPreferences), and also proactively requests preferences on startup to cover the case where ExtensionLoaded fired before the listener attached. Messages are only trusted when event.source === window.
  • The value is written to the global ORTB2 via the FPD enrichment hook (enrichFPD.before(...)), the same mechanism used by the USP/GPP consent modules, so it reaches both client adapters and Prebid Server.
  • Standalone adChoices config namespace:
    • signal (string, optional): a statically supplied signal (e.g. read server-side from the X-AdChoices header); takes precedence over the extension-read value.
    • timeout (integer, optional): max ms to delay the first auction while waiting for the signal. Defaults to 0 (non-blocking) so users without the extension are not penalized; the requestBids delay hook is only installed when a positive timeout is configured. The delay window starts when an auction actually begins waiting (not at setConfig time) and applies once — after the signal arrives or the timeout elapses, later auctions proceed without further delay.
  • Public TypeScript types are provided for the config interface (AdChoicesConfig).

Maintainer contact: prebid@aboutads.info

Note: page JavaScript cannot read the X-AdChoices/Cookie2 headers the extension injects into outbound requests (those are for server-side consumption), so the browser path is the postMessage protocol or the signal config option.

Testing

  • gulp lint (eslint on changed files): passes, no output.
  • gulp test --file test/spec/modules/adChoices_spec.js: 20/20 tests passing; strict TypeScript compile (ts-strict) clean.

Docs


Issue template

Type of issue

Feature

Description

Enables publishers to read the DAA AdChoices Signal in the browser (via the Protect My Choices extension) and pass it into the bid stream at regs.ext.adchoices.

Steps to reproduce

  1. Build Prebid with the adChoices module: gulp build --modules=adChoices.
  2. Load a page in a browser with the DAA Protect My Choices extension installed.
  3. Observe that outgoing bid requests include regs.ext.adchoices with the signal string. Alternatively, set pbjs.setConfig({ adChoices: { signal: '<value>' } }) to supply the signal directly.

Expected results

regs.ext.adchoices is present on bid requests when a signal is available.

Actual results

As expected with this change.

Platform details

Prebid.js master (11.20.0-pre); module written in TypeScript; Node >= 20.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40ba90a5cc

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread modules/adChoices.ts
import { getHook } from '../src/hook.js';
import { enrichFPD } from '../src/fpd/enrichment.js';

const MODULE_NAME = 'adChoices';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Align the module name with the build filename

With this file named daaAdChoices.ts, Prebid's build helper derives the selectable module name from the file basename (gulpHelpers.getModules()), so publishers must build with --modules=daaAdChoices; --modules=adChoices is rejected as an invalid module even though this module registers and documents its config under adChoices. Please rename the file/module or align the public name so the documented adChoices module can actually be included in a build.

Useful? React with 👍 / 👎.

Adds the adChoices module (modules/adChoices.ts), which reads the DAA AdChoices
Signal in the browser via the Protect My Choices (PMC) extension postMessage
protocol (ExtensionLoaded -> GetAdPreferences -> AdPreferences) and conveys it
in the OpenRTB bid stream as the community extension regs.ext.adchoices, per
Appendix 5 of the DAA AdChoices Signal Specification.

- Standalone `adChoices` config namespace; supports a static `signal` override
  (takes precedence) and an opt-in `timeout`. Non-blocking by default; when a
  positive timeout is set, the first auction is delayed up to that many ms while
  waiting for the signal (the window starts when an auction begins waiting and
  applies once, so later auctions are not re-delayed).
- Injects the value via the FPD enrichment hook, matching the consent modules.
- TypeScript public types (AdChoicesConfig); 20 unit tests.

@robertrmartinez robertrmartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I am wondering if this should be a realTimeData submodule instead of a new standalone top-level module?

it is reading data from an outside browser source, optionally delaying the auction, and then adding data into the bidstream at regs.ext.adchoices

that sounds pretty close to the RTD module shape to me:

pbjs.setConfig({
  realTimeData: {
    auctionDelay: 500,
    dataProviders: [{
      name: 'adChoices',
      waitForIt: true
    }]
  }
});

I think rtdModule already owns a lot of this lifecycle, auctionDelay, waitForIt, and provider docs expectations

could this be adChoicesRtdProvider and just enrich ortb2.regs.ext.adchoices from there?

if there is a reason this needs to be a standalone consent-style module instead, can we add that reasoning somewhere?

just wanted to ask before we add another one-off requestBids hook/config namespace

@robertrmartinez

Copy link
Copy Markdown
Collaborator

also can we please add the local module markdown file?

I see the docs PR is linked here:
prebid/prebid.github.io#6630

but we usually still need the repo-side module docs with example config and what changes in the bid request

if this becomes RTD, I think that would be modules/adChoicesRtdProvider.md, and it should show the realTimeData.dataProviders config plus the expected regs.ext.adchoices output

if it stays standalone, then prob modules/adChoices.md

please add that also!

patmmccann and others added 2 commits June 24, 2026 05:53
Adds modules/adChoices.md with an overview, example `adChoices` config
(`signal` and `timeout`), and the resulting `regs.ext.adchoices` bid-request
output, per reviewer request.
@rvidakovic

rvidakovic commented Jun 24, 2026

Copy link
Copy Markdown
Author

I am wondering if this should be a realTimeData submodule instead of a new standalone top-level module?

it is reading data from an outside browser source, optionally delaying the auction, and then adding data into the bidstream at regs.ext.adchoices

that sounds pretty close to the RTD module shape to me:

pbjs.setConfig({
  realTimeData: {
    auctionDelay: 500,
    dataProviders: [{
      name: 'adChoices',
      waitForIt: true
    }]
  }
});

I think rtdModule already owns a lot of this lifecycle, auctionDelay, waitForIt, and provider docs expectations

could this be adChoicesRtdProvider and just enrich ortb2.regs.ext.adchoices from there?

if there is a reason this needs to be a standalone consent-style module instead, can we add that reasoning somewhere?

just wanted to ask before we add another one-off requestBids hook/config namespace

Thanks, that's a fair question. I was wondering the same thing initially (standalone vs. RTD), and ultimately positioned it as a standalone consent-style module rather than an RTD submodule for a few reasons:

  1. It's a self-regulatory signal, not vendor targeting data. The value lands in regs.ext.adchoices, which is the same regs namespace as gdpr, us_privacy, and gpp. Every one of those is delivered by a standalone module, not by an RTD provider. This module enriches regs.ext.adchoices through the exact same enrichFPD.before(...) hook those consent modules use, so it's consistent with how the regs.* family is already handled rather than a new pattern.

  2. RTD providers are typically vendor data sources and are commonly gated behind consent controls. Gating a self-regulatory preference signal behind consent doesn't make much sense. The signal needs to flow regardless of those controls.

  3. This was discussed with @patmmccann prior to implementation. The recommendation there was that a consent-style module is better positioning than a RTD module, mainly for the previous reason: RTD modules are typically vendor modules and might be gated behind consent, which doesn't make sense in our case. Whereas something like gppControl_usnat is a more appropriate standalone precedent.

That said, if you still prefer this live under realTimeData as adChoicesRtdProvider, I'm happy to refactor, I just wanted to lay out the rationale. Let me know which you'd prefer and I'll align it.

I've also added the repo-side module docs (modules/adChoices.md) in the latest commit, as you requested, with the example config and the resulting regs.ext.adchoices bid request output.

@robertrmartinez robertrmartinez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ok now I get it and I agree standalone is right fit not RTD!

So I think from that standpoint this looks clean to me. couple minor notes.

@patmmccann mentioned activity controls, which we could leverage to enforce some of the user decisions in the PMC extension. Though seems like any enforcement should just be done in a future version once it is a bit more ironed out.

I think we should ship as is!

Comment thread modules/adChoices.ts
Comment thread modules/adChoices.md Outdated
- Remove the requestBids hook (and clear the installed flag) in
  resetAdChoicesData so the hook does not leak onto the global requestBids
  across test runs, and can be reinstalled after a reset. Adds a test that sets
  a timeout, resets, and asserts the hook is gone.
- Reformat modules/adChoices.md to the repo-side `# Overview` convention
  (Module Name / Module Type / Maintainer); the page_v2 frontmatter remains in
  the docs PR.
@rvidakovic

Copy link
Copy Markdown
Author

Thanks @robertrmartinez - good catches on both. Addressed in 10f6554

As for leveraging activity controls for enforcement, I agree, that's worth exploring for a future version.

@barecheck

barecheck Bot commented Jul 18, 2026

Copy link
Copy Markdown

Barecheck - Code coverage report

Total: 96.58%

Your code coverage diff: 0.00% ▴

Uncovered files and lines
FileLines
modules/adChoices.ts225-226

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants