Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"pubProvidedIdSystem",
"publinkIdSystem",
"pubmaticIdSystem",
"rediadsIdSystem",
"rewardedInterestIdSystem",
"sharedIdSystem",
"startioIdSystem",
Expand Down Expand Up @@ -141,4 +142,4 @@
"adplayerproVideoProvider"
]
}
}
}
185 changes: 185 additions & 0 deletions modules/rediadsIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/**
* This module adds Rediads ID support to the User ID module
* The {@link module:modules/userId} module is required.
* @module modules/rediadsIdSystem
* @requires module:modules/userId
*/

import { submodule } from '../src/hook.js';
import { VENDORLESS_GVLID } from '../src/consentHandler.js';
import { cyrb53Hash, generateUUID, isPlainObject, isStr } from '../src/utils.js';

/**
* @typedef {import('../modules/userId/index.js').Submodule} Submodule
* @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig
* @typedef {import('../modules/userId/index.js').ConsentData} ConsentData
* @typedef {import('../modules/userId/index.js').IdResponse} IdResponse
*/

const MODULE_NAME = 'rediadsId';
const DEFAULT_SOURCE = 'rediads.com';
const DEFAULT_ATYPE = 1;
const DEFAULT_EXPIRES_DAYS = 30;
const DEFAULT_REFRESH_SECONDS = 3600;
const GPP_OPT_OUT_SECTIONS = [7, 8, 9, 10, 11, 12];

function normalizeStoredId(storedId) {
if (isPlainObject(storedId)) {
return storedId;
}

if (isStr(storedId)) {
try {
const parsed = JSON.parse(storedId);
if (isPlainObject(parsed)) {
return parsed;
}
} catch (e) {}
}
}

function getConsentHash(consentData = {}) {
return `hash_${cyrb53Hash(JSON.stringify({
gdpr: consentData.gdpr?.consentString || null,
gpp: consentData.gpp?.gppString || null,
usp: consentData.usp || null,
coppa: consentData.coppa === true
}))}`;
}

function hasTcfPurpose1Consent(gdprConsent) {
return gdprConsent?.vendorData?.purpose?.consents?.[1] === true ||
gdprConsent?.vendorData?.purposeConsents?.[1] === true;
}

function canWriteStorage(consentData = {}) {
if (consentData.coppa === true) {
return false;
}

const gdprConsent = consentData.gdpr;
if (gdprConsent?.gdprApplies || gdprConsent?.vendorData || gdprConsent?.consentString) {
return isStr(gdprConsent?.consentString) &&
gdprConsent.consentString.length > 0 &&
hasTcfPurpose1Consent(gdprConsent);
}

return true;
}

function canShareId(consentData = {}) {
if (consentData.coppa === true) {
return false;
}

if (isStr(consentData.usp) && consentData.usp.charAt(2) === 'Y') {
return false;
}

if (Array.isArray(consentData.gpp?.applicableSections)) {
return !consentData.gpp.applicableSections.some((section) => GPP_OPT_OUT_SECTIONS.includes(section));
}

return true;
}

function ensureStorageDefaults(config) {
if (isPlainObject(config?.storage)) {
if (config.storage.expires == null) {
config.storage.expires = DEFAULT_EXPIRES_DAYS;
}
if (config.storage.refreshInSeconds == null) {
config.storage.refreshInSeconds = DEFAULT_REFRESH_SECONDS;
}
}
}

function buildStoredId(config, consentData, existingId) {
const params = config?.params || {};
const source = params.source || DEFAULT_SOURCE;
const expiresDays = config?.storage?.expires ?? DEFAULT_EXPIRES_DAYS;
const refreshInSeconds = config?.storage?.refreshInSeconds ?? DEFAULT_REFRESH_SECONDS;
const now = Date.now();

return {
id: existingId || `ruid_${generateUUID()}`,
source,
atype: DEFAULT_ATYPE,
canShare: canShareId(consentData),
consentHash: getConsentHash(consentData),
refreshAfter: now + (refreshInSeconds * 1000),
expiresAt: now + (expiresDays * 24 * 60 * 60 * 1000)
};
}

/** @type {Submodule} */
export const rediadsIdSubmodule = {
name: MODULE_NAME,
gvlid: VENDORLESS_GVLID,

decode(value) {
const storedId = normalizeStoredId(value);
if (!isStr(storedId?.id) || storedId.id.length === 0) {
return undefined;
}

return {
rediadsId: {
uid: storedId.id,
source: storedId.source || DEFAULT_SOURCE,
atype: storedId.atype || DEFAULT_ATYPE,
ext: {
canShare: storedId.canShare !== false,
consentHash: storedId.consentHash,
refreshAfter: storedId.refreshAfter
}
}
};
},

getId(config, consentData, storedId) {
ensureStorageDefaults(config);

if (!canWriteStorage(consentData)) {
return undefined;
}

const normalized = normalizeStoredId(storedId);
return {
id: buildStoredId(config, consentData, normalized?.id)
};
},

extendId(config, consentData, storedId) {
ensureStorageDefaults(config);

if (!canWriteStorage(consentData)) {
return undefined;
}

const normalized = normalizeStoredId(storedId);
if (!isStr(normalized?.id) || normalized.id.length === 0) {
return this.getId(config, consentData, storedId);
}

return {
id: buildStoredId(config, consentData, normalized.id)
};
},

eids: {
rediadsId(values) {
return values
.filter((value) => isStr(value?.uid) && value.ext?.canShare !== false)
.map((value) => ({
source: value.source || DEFAULT_SOURCE,
uids: [{
id: value.uid,
atype: value.atype || DEFAULT_ATYPE
}]
}));
}
}
};

submodule('userId', rediadsIdSubmodule);
52 changes: 52 additions & 0 deletions modules/rediadsIdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
## Rediads User ID Submodule

The Rediads User ID submodule generates a first-party identifier in the browser, stores it through Prebid's `userId` framework, and exposes it through `bidRequest.userId` and `userIdAsEids`.

The module is vendorless for TCF enforcement, so Prebid applies purpose-level storage checks without requiring a separate vendor consent entry.

### Build the Module

```bash
gulp build --modules=userId,rediadsIdSystem
```

### Example Configuration

```javascript
pbjs.setConfig({
userSync: {
userIds: [{
name: 'rediadsId',
params: {
source: 'rediads.com'
},
storage: {
type: 'html5',
name: 'rediads_id',
expires: 30,
refreshInSeconds: 3600
}
}]
}
});
```

### Parameters

| Param under `userSync.userIds[]` | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| `name` | Required | String | Name of the module. | `'rediadsId'` |
| `params` | Optional | Object | Rediads-specific configuration. | |
| `params.source` | Optional | String | EID `source` value to emit. Defaults to `'rediads.com'`. | `'rediads.com'` |
| `storage.type` | Recommended | String | Prebid-managed storage type. | `'html5'` |
| `storage.name` | Recommended | String | Storage key used by Prebid. | `'rediads_id'` |
| `storage.expires` | Optional | Number | Days before the cached ID expires. Defaults to `30`. | `30` |
| `storage.refreshInSeconds` | Optional | Number | Seconds before the cached ID is refreshed. Defaults to `3600`. | `3600` |

### Behavior Notes

- `getId()` generates a `ruid_<uuid>` value on first use.
- `extendId()` preserves the existing Rediads ID and refreshes metadata.
- `decode()` exposes the ID as `bidRequest.userId.rediadsId`.
- EIDs are suppressed when US Privacy or GPP opt-out signals indicate sharing should be blocked.
- The module returns `undefined` when COPPA applies or when GDPR applies without TCF Purpose 1 consent.
8 changes: 8 additions & 0 deletions modules/userId/eids.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,14 @@ userIdAsEids = [
}]
},

{
source: 'rediads.com',
uids: [{
id: 'ruid_7b9c1d3f-1e2b-4e7b-9e5a-acde12345678',
atype: 1
}]
},

{
source: 'britepool.com',
uids: [{
Expand Down
12 changes: 12 additions & 0 deletions modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,18 @@ pbjs.setConfig({
{
name: "mygaruId"
},
{
name: "rediadsId",
params: {
source: "rediads.com"
},
storage: {
type: "html5",
name: "rediads_id",
expires: 30,
refreshInSeconds: 3600
}
},
{
name: "startioId"
}
Expand Down
Loading
Loading