-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Anonymised id system module #15492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
patmmccann
merged 4 commits into
prebid:master
from
id-ward:ANON-8303-anonymised-id-system
Aug 17, 2026
Merged
Anonymised id system module #15492
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // the augmentation in this file only applies where the spec is part of the program | ||
| import type {} from './userId/spec.js'; | ||
|
|
||
| export type AnonymisedIdSystemModuleName = 'anonymisedId'; | ||
|
|
||
| declare module './userId/spec' { | ||
| interface UserId { | ||
| anonymisedId: string; | ||
| } | ||
|
|
||
| interface ProvidersToId { | ||
| anonymisedId: 'anonymisedId'; | ||
| } | ||
|
|
||
| interface ProviderParams { | ||
| anonymisedId: never; | ||
| } | ||
| } | ||
|
|
||
| export {}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,141 @@ | ||
| /** | ||
| * This module adds the Anonymised ID to the User ID module | ||
| * The {@link module:modules/userId} module is required | ||
| * @module modules/anonymisedIdSystem | ||
| * @requires module:modules/userId | ||
| */ | ||
| import { submodule } from '../src/hook.js'; | ||
| import { getStorageManager } from '../src/storageManager.js'; | ||
| import { MODULE_TYPE_UID } from '../src/activities/modules.js'; | ||
| import { logInfo, logWarn } from '../src/utils.js'; | ||
|
|
||
| const MODULE_NAME = 'anonymisedId'; | ||
| const GVLID = 1116; | ||
| const EID_SOURCE = 'anonymised.io'; | ||
| const LOG_PREFIX = 'User ID - anonymisedId submodule: '; | ||
|
|
||
| /** | ||
| * Local storage key holding the CUID. It is written by the Anonymised Marketing Tag when the user | ||
| * signs in, and removed by it on sign-out or when consent is withdrawn. This module only reads it. | ||
| */ | ||
| export const STORAGE_KEY = 'anon-cuid'; | ||
|
|
||
| /** | ||
| * Generous upper bound on the identifier length, to keep a corrupted value from bloating every | ||
| * bid request. | ||
| */ | ||
| export const MAX_ID_LENGTH = 100; | ||
|
|
||
| export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); | ||
|
|
||
| const STORAGE_CONFIG_WARNING = `${LOG_PREFIX}no ID will be provided: this module must be configured without "storage". ` + | ||
| 'The Anonymised Marketing Tag owns this ID and removes it on sign-out and on consent withdrawal; ' + | ||
| 'a copy cached by Prebid.js would outlive that removal and keep sending the ID of a signed-out user.'; | ||
|
|
||
| /** | ||
| * A publisher who configures `storage` gets no ID at all, rather than one that Prebid.js may cache | ||
| * past the point where the Marketing Tag has removed it. Both entry points have to refuse: | ||
| * `getId` so nothing is ever written to the publisher's store, and `decode` because the User ID | ||
| * module skips `getId` entirely while a cached value is still fresh, decoding that copy instead. | ||
| * @param {Object} [config] this submodule's publisher configuration | ||
| * @returns {boolean} | ||
| */ | ||
| function usesUnsupportedStorage(config) { | ||
| if (!config?.storage) { | ||
| return false; | ||
| } | ||
| logWarn(STORAGE_CONFIG_WARNING); | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Characters that cannot occur in a raw identifier, and whose presence means the value was | ||
| * serialised rather than written as-is - a JSON object, array, or quoted scalar. Passing such a | ||
| * value on would send bidders an ID that matches nothing. | ||
| */ | ||
| const ENCODED_VALUE_CHARS = /[\s{}[\]"']/; | ||
|
|
||
| /** | ||
| * The Marketing Tag writes the CUID as a plain string. Validation is deliberately loose - it | ||
| * rejects the values that would be harmful to pass on (empty, serialised, or implausibly long) | ||
| * without pinning the identifier's format, which is owned by the tag and can change on a much | ||
| * faster release cycle than this module. | ||
| * @param {*} value | ||
| * @returns {boolean} | ||
| */ | ||
| export function isValidId(value) { | ||
| return typeof value === 'string' && | ||
| value.length > 0 && | ||
| value.length <= MAX_ID_LENGTH && | ||
| !ENCODED_VALUE_CHARS.test(value); | ||
| } | ||
|
|
||
| export const anonymisedIdSubmodule = { | ||
| /** | ||
| * used to link submodule with config | ||
| * @type {string} | ||
| */ | ||
| name: MODULE_NAME, | ||
|
|
||
| /** | ||
| * IAB Global Vendor List ID | ||
| * @type {number} | ||
| */ | ||
| gvlid: GVLID, | ||
|
|
||
| /** | ||
| * Read the CUID that the Anonymised Marketing Tag stored on this domain. This is a synchronous | ||
| * read with no network call: when the tag has not written an ID yet - because it is not installed, | ||
| * or the user is not signed in - there is simply no ID for this page view. | ||
| * @function | ||
| * @param {Object} [config] this submodule's publisher configuration | ||
| * @returns {{id: string} | undefined} | ||
| */ | ||
| getId(config) { | ||
| if (usesUnsupportedStorage(config)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const stored = storage.getDataFromLocalStorage(STORAGE_KEY); | ||
| const cuid = typeof stored === 'string' ? stored.trim() : null; | ||
|
|
||
| if (!cuid) { | ||
| // No ID is the expected state for a signed-out user, so this is not a warning: it is also | ||
| // what a reader sees when device access is denied, and it is most of the traffic. | ||
| logInfo(`${LOG_PREFIX}no ID in localStorage["${STORAGE_KEY}"] - the user is signed out, the Anonymised Marketing Tag is not installed on this page, or device access is not permitted`); | ||
| return undefined; | ||
| } | ||
|
|
||
| if (!isValidId(cuid)) { | ||
| logWarn(`${LOG_PREFIX}ignoring malformed value in localStorage["${STORAGE_KEY}"]`); | ||
| return undefined; | ||
| } | ||
|
|
||
| logInfo(`${LOG_PREFIX}ID found`); | ||
| return { id: cuid }; | ||
| }, | ||
|
|
||
| /** | ||
| * decode the stored id value for passing to bid requests | ||
| * @function | ||
| * @param {string} value | ||
| * @param {Object} [config] this submodule's publisher configuration | ||
| * @returns {{anonymisedId: string} | undefined} | ||
| */ | ||
| decode(value, config) { | ||
| if (usesUnsupportedStorage(config)) { | ||
| return undefined; | ||
| } | ||
|
|
||
| return isValidId(value) ? { [MODULE_NAME]: value } : undefined; | ||
| }, | ||
|
|
||
| eids: { | ||
| [MODULE_NAME]: { | ||
| source: EID_SOURCE, | ||
| atype: 1 | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| submodule('userId', anonymisedIdSubmodule); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # Overview | ||
|
|
||
| Module Name: anonymisedIdSystem | ||
| Module Type: UserID Module | ||
| Maintainer: support@anonymised.io | ||
|
|
||
| # Description | ||
|
|
||
| Anonymised is a data anonymization technology for privacy-preserving advertising. | ||
|
|
||
| The Anonymised User ID submodule exposes the CUID - the identifier that the | ||
| [Anonymised Marketing Tag](https://support.anonymised.io/integrate/marketing-tag?t=LPukVCXzSIcRoal5jggyeg) | ||
| assigns when a user signs in - to bid adapters as an OpenRTB Extended ID under the source | ||
| `anonymised.io`. | ||
|
|
||
| The submodule performs no network calls. It reads the identifier that the Marketing Tag has already | ||
| stored on the publisher's own domain, in `localStorage` under the key `anon-cuid`, and passes it to | ||
| the bid stream. When the Marketing Tag is not installed, or the user is not signed in, no ID is read | ||
| and no EID is added. | ||
|
|
||
| ### Prerequisite | ||
|
|
||
| The Anonymised Marketing Tag must be installed on the page. This submodule does not load it. The tag | ||
| can be installed [natively](https://support.anonymised.io/integrate/install-the-anonymised-tag-natively?t=LPukVCXzSIcRoal5jggyeg) | ||
| or through the [`anonymisedRtdProvider`](anonymisedRtdProvider.md) module's `tagConfig` parameter. | ||
|
|
||
| # Building Prebid with Anonymised ID support | ||
|
|
||
| ```bash | ||
| gulp build --modules=userId,anonymisedIdSystem | ||
| ``` | ||
|
|
||
| # Configuration | ||
|
|
||
| ```javascript | ||
| pbjs.setConfig({ | ||
| userSync: { | ||
| userIds: [{ | ||
| name: 'anonymisedId' | ||
| }] | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| | Param under userSync.userIds[] | Scope | Type | Description | Example | | ||
| | --- | --- | --- | --- | --- | | ||
| | name | Required | String | The name of this module. | `'anonymisedId'` | | ||
|
|
||
| The submodule takes no `params`. | ||
|
|
||
| ### Do not configure `storage` | ||
|
|
||
| This submodule manages the identifier itself and must be configured **without** a `storage` object. | ||
|
|
||
| The Marketing Tag is the single source of truth for the CUID: it writes the identifier on sign-in and | ||
| removes it on sign-out and on consent withdrawal. If Prebid.js were allowed to keep its own copy, that | ||
| copy would outlive the removal and the submodule would keep sending a stale identifier to bidders | ||
| until Prebid's own expiry elapsed. Reading the value fresh on every initialization makes removal take | ||
| effect immediately. | ||
|
|
||
| If a `storage` object is configured, the submodule logs a warning and provides **no** ID at all, | ||
| rather than one Prebid.js may cache beyond the Marketing Tag's removal of it. | ||
|
|
||
| ### Do not set `userSync.ppid` to `anonymised.io` | ||
|
|
||
| The Marketing Tag sets the Google Ad Manager Publisher Provided ID itself, as part of its SignalLift | ||
| feature. Pointing `userSync.ppid` at `anonymised.io` makes Prebid.js set the PPID as well, which | ||
| produces two problems: | ||
|
|
||
| - Prebid.js strips non-alphanumeric characters from an ID before setting it as the PPID, while the | ||
| Marketing Tag sends the identifier unmodified. The same user would be represented by two different | ||
| PPIDs depending on which code path ran, splitting Google Ad Manager audiences and reporting. | ||
| - The Marketing Tag applies its own logic when deciding whether a PPID should be set at all. Prebid.js | ||
| is not aware of that logic and would bypass it. | ||
|
|
||
| The division is: the Marketing Tag owns the identifier sent to **Google Ad Manager**; this submodule | ||
| owns the identifier sent to **bidders**. | ||
|
|
||
| ### Single-page applications | ||
|
|
||
| `getId` is called when the User ID module initializes and is not re-run for subsequent auctions. If a | ||
| user signs in after that point, call `pbjs.refreshUserIds({ submoduleNames: ['anonymisedId'] })` to | ||
| pick up the new identifier. Always pass `submoduleNames` - an unscoped refresh re-initializes every | ||
| configured ID submodule, including those that make network requests. | ||
|
|
||
| ### Subdomains | ||
|
|
||
| The identifier is read from `localStorage`, which is scoped to a single origin. A publisher serving | ||
| the same user from more than one subdomain will have an identifier available on each subdomain only | ||
| after the Marketing Tag has run there. | ||
|
|
||
| ### Data deletion | ||
|
|
||
| Deletion requests are handled by the Marketing Tag, which owns the user's session and every | ||
| identifier derived from it. This submodule stores nothing of its own and therefore implements no | ||
| `onDataDeletionRequest` callback. | ||
|
|
||
| ### Vendor and storage disclosure | ||
|
|
||
| The submodule declares GVL ID `1116`. Its first-party storage use is disclosed at | ||
| [https://cdn1.anonymised.io/deviceStorage.json](https://cdn1.anonymised.io/deviceStorage.json). | ||
|
|
||
| For any questions or assistance with integrating Prebid, `anonymisedIdSystem`, or the Anonymised | ||
| Marketing Tag, please contact an [Anonymised representative](mailto:support@anonymised.io). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the storageControl module is included, its default strict rule denies keyed storage reads unless module metadata has a
disclosureURLwith a matching disclosure; this submodule advertises theanon-cuidkey throughgetStorageManagerbut never setsdisclosureURLon the spec, sostorage.getDataFromLocalStorage(STORAGE_KEY)is denied and the ID is never emitted for those publishers. Please expose the device-storage disclosure URL on the submodule object so metadata can authorize the read.AGENTS.md reference: AGENTS.md:L75-L75
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
anonymisedIdSystem declares gvlid: 1116, and metadata/compileMetadata.mjs resolves the disclosure URL from the GVL when a module doesn't set one:
Vendor 1116 has deviceStorageDisclosureUrl registered as https://cdn1.anonymised.io/deviceStorage.json, which declares anon-cuid (type: "web", domains: ["*"]) — so storageControl matches it and the read is allowed. The disclosureURL property is an override for vendors without a GVL-registered URL (adplusIdSystem, jixieIdSystem); setting it here would take precedence over the GVL lookup and pin a URL the GVL is authoritative for. anonymisedRtdProvider already works this way — gvlid only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@codex op is correct, the URL is only useful in absence of a gvlid, the build process accesses the URL in the gvl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Summary
disclosureURLis intentionally omitted because metadata compilation resolves the authoritative device-storage disclosure through GVL vendor ID 1116. modules/anonymisedIdSystem.jsL80-L8535b4163(Clarify Anonymised ID disclosure resolution).make_prtool is unavailable in this environment, so a pull request could not be created.Testing
npx eslint modules/anonymisedIdSystem.js --cache --cache-strategy contentnpx gulp test --nolint --file test/spec/modules/anonymisedIdSystem_spec.js(35 tests completed in both feature configurations)git diff --checkgit status --short --branch(clean working tree)View task →