Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"adriverIdSystem",
"adtelligentIdSystem",
"amxIdSystem",
"anonymisedIdSystem",
"ceeIdSystem",
"connectIdSystem",
"criteoIdSystem",
Expand Down
20 changes: 20 additions & 0 deletions modules/anonymisedIdSystem.d.ts
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 {};
116 changes: 116 additions & 0 deletions modules/anonymisedIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* 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 });

/**
* 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{}[\]"']/;
Comment thread
patmmccann marked this conversation as resolved.

/**
* 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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add the storage disclosure URL to the submodule

When the storageControl module is included, its default strict rule denies keyed storage reads unless module metadata has a disclosureURL with a matching disclosure; this submodule advertises the anon-cuid key through getStorageManager but never sets disclosureURL on the spec, so storage.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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

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:

if (meta.disclosureURL == null && meta.gvlid != null) {
  meta.disclosureURL = await getDisclosureUrl(meta.gvlid);
}

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.

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.

@codex op is correct, the URL is only useful in absence of a gvlid, the build process accesses the URL in the gvl

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

  • Documented that disclosureURL is intentionally omitted because metadata compilation resolves the authoritative device-storage disclosure through GVL vendor ID 1116. modules/anonymisedIdSystem.jsL80-L85
  • Committed the change as 35b4163 (Clarify Anonymised ID disclosure resolution).
  • The repository has no configured Git remote, and the required make_pr tool is unavailable in this environment, so a pull request could not be created.

Testing

  • npx eslint modules/anonymisedIdSystem.js --cache --cache-strategy content
  • npx gulp test --nolint --file test/spec/modules/anonymisedIdSystem_spec.js (35 tests completed in both feature configurations)
  • git diff --check
  • git status --short --branch (clean working tree)

View task →


/**
* 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 (config?.storage) {
logWarn(`${LOG_PREFIX}this module must be configured without "storage". The Anonymised Marketing Tag owns this ID and removes it on sign-out; a copy kept by Prebid.js would outlive that removal and keep sending the ID of a signed-out user.`);
Comment thread
patmmccann marked this conversation as resolved.
Outdated
}

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
* @returns {{anonymisedId: string} | undefined}
*/
decode(value) {
return isValidId(value) ? { [MODULE_NAME]: value } : undefined;
},

eids: {
[MODULE_NAME]: {
source: EID_SOURCE,
atype: 1
}
}
};

submodule('userId', anonymisedIdSubmodule);
101 changes: 101 additions & 0 deletions modules/anonymisedIdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 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.

### 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).
7 changes: 7 additions & 0 deletions modules/userId/eids.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ userIdAsEids = [
atype: 1
}]
},
{
source: 'anonymised.io',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
},
{
source: 'utiq.com',
uids: [{
Expand Down
3 changes: 3 additions & 0 deletions modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ pbjs.setConfig({
expires: 1,
refreshInSeconds: 86400
}
}, {
// the Anonymised Marketing Tag owns this ID; it must be configured without `storage`
name: "anonymisedId"
}, {
name: "pubCommonId",
storage: {
Expand Down
Loading
Loading