From 66101f387390a823484b686a7652d873e2d2a9ae Mon Sep 17 00:00:00 2001 From: illiamilshtein Date: Tue, 3 Feb 2026 15:52:27 +0200 Subject: [PATCH 1/5] Add Start.io User ID submodule with tests and documentation --- modules/startioSystem.js | 81 +++++++++ modules/startioSystem.md | 63 +++++++ test/spec/modules/startioSystem_spec.js | 217 ++++++++++++++++++++++++ 3 files changed, 361 insertions(+) create mode 100644 modules/startioSystem.js create mode 100644 modules/startioSystem.md create mode 100644 test/spec/modules/startioSystem_spec.js diff --git a/modules/startioSystem.js b/modules/startioSystem.js new file mode 100644 index 0000000000..896d045e78 --- /dev/null +++ b/modules/startioSystem.js @@ -0,0 +1,81 @@ +import { logError } from '../src/utils.js'; +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; + +const MODULE_NAME = 'startioId'; +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); + +function storeId(id, storageConfig = {}) { + const expires = storageConfig.expires || 365; + const expirationDate = new Date(Date.now() + (expires * 24 * 60 * 60 * 1000)); + const storageType = storageConfig.type || ''; + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + const useLocalStorage = !storageType || storageType.includes(STORAGE_TYPE_LOCALSTORAGE); + + if (useCookie && storage.cookiesAreEnabled()) { + storage.setCookie(MODULE_NAME, id, expirationDate.toUTCString()); + } + + if (useLocalStorage && storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(MODULE_NAME, id); + } +} + +export const startioIdSubmodule = { + name: MODULE_NAME, + decode(value) { + return value && typeof value === 'string' + ? { 'startioId': value } + : undefined; + }, + getId(config, consentData, storedId) { + const configParams = (config && config.params) || {}; + const storageConfig = (config && config.storage) || {}; + + if (storedId) { + return { id: storedId }; + } + + if (!configParams.endpoint || typeof configParams.endpoint !== 'string') { + logError(`${MODULE_NAME} module requires an endpoint parameter.`); + return; + } + + const resp = function (callback) { + const callbacks = { + success: response => { + let responseId; + try { + const responseObj = JSON.parse(response); + if (responseObj && responseObj.id) { + responseId = responseObj.id; + storeId(responseId, storageConfig); + } else { + logError(`${MODULE_NAME}: Server response missing 'id' field`); + } + } catch (error) { + logError(`${MODULE_NAME}: Error parsing server response`, error); + } + callback(responseId); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(configParams.endpoint, callbacks, undefined, { method: 'GET' }); + }; + return { callback: resp }; + }, + + eids: { + 'startioId': { + source: 'start.io', + atype: 3 + }, + } +}; + +submodule('userId', startioIdSubmodule); diff --git a/modules/startioSystem.md b/modules/startioSystem.md new file mode 100644 index 0000000000..44173e78f9 --- /dev/null +++ b/modules/startioSystem.md @@ -0,0 +1,63 @@ +## Start.io User ID Submodule + +The Start.io User ID submodule generates and persists a unique user identifier by fetching it from a publisher-supplied endpoint. The ID is stored in both cookies and local storage for subsequent page loads and is made available to other Prebid.js modules via the standard `eids` interface. + +For integration support, contact prebid@start.io. + +### Prebid Params + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'startioId', + params: { + endpoint: 'https://your-startio-endpoint.example.com/id' + }, + storage: { + type: 'html5', //or 'cookie', or 'cookie&html5', + name: 'startioId', + expires: 365 + } + }] + } +}); +``` + +## Parameter Descriptions for the `userSync` Configuration Section + +The below parameters apply only to the Start.io User ID integration. + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module. | `"startioId"` | +| params | Required | Object | Container of all module params. | | +| params.endpoint | Required | String | The URL of the Start.io ID endpoint. Must return a JSON object with an `id` field. | `"https://id.startio.example.com/uid"` | +| storage | Optional | Object | Controls how the ID is persisted. Managed by Prebid.js core; see notes below. | | +| storage.type | Optional | String | Storage mechanism. Accepts `cookie`, `html5`, or `cookie&html5`. Defaults to both when omitted. | `"cookie&html5"` | +| storage.expires | Optional | Number | Cookie / storage TTL in days. Defaults to `365`. | `365` | + +## Server Response Format + +The endpoint specified in `params.endpoint` must return a JSON response containing an `id` field: + +``` +{ + "id": "unique-user-identifier-string" +} +``` + +If the `id` field is missing or the response cannot be parsed, the module logs an error and does not store a value. + +## How It Works + +1. On the first page load (no stored ID exists), the module sends a `GET` request to the configured `endpoint`. +2. The returned `id` is written to both cookies and local storage (respecting the `storage` configuration). +3. On subsequent loads the stored ID is returned directly — no network request is made. +4. The ID is exposed to other modules via the extended ID (`eids`) framework with source `start.io` and `atype: 3`. + +## Notes + +- The `endpoint` parameter is required. The module will log an error and return no ID if it is missing or not a string. +- Storage defaults to both cookies and local storage when no explicit `storage.type` is provided. The module checks whether each mechanism is available before writing. +- Cookie expiration is set to `storage.expires` days from the time the ID is first fetched (default 365 days). diff --git a/test/spec/modules/startioSystem_spec.js b/test/spec/modules/startioSystem_spec.js new file mode 100644 index 0000000000..c891a06a1c --- /dev/null +++ b/test/spec/modules/startioSystem_spec.js @@ -0,0 +1,217 @@ +import * as utils from '../../../src/utils.js'; +import { server } from 'test/mocks/xhr.js'; +import { startioIdSubmodule, storage } from 'modules/startioSystem.js'; + +describe('StartIO ID System', function () { + let sandbox; + + const validConfig = { + params: { + endpoint: 'https://test-endpoint.start.io/getId' + }, + storage: { + expires: 365 + } + }; + + beforeEach(function () { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logError'); + sandbox.stub(storage, 'getCookie').returns(null); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + }); + + afterEach(function () { + sandbox.restore(); + }); + + describe('module registration', function () { + it('should register the submodule', function () { + expect(startioIdSubmodule.name).to.equal('startioId'); + }); + + it('should have eids configuration', function () { + expect(startioIdSubmodule.eids).to.deep.equal({ + 'startioId': { + source: 'start.io', + atype: 3 + } + }); + }); + }); + + describe('decode', function () { + it('should return undefined if no value passed', function () { + expect(startioIdSubmodule.decode()).to.be.undefined; + }); + + it('should return undefined if invalid value passed', function () { + expect(startioIdSubmodule.decode(123)).to.be.undefined; + expect(startioIdSubmodule.decode(null)).to.be.undefined; + expect(startioIdSubmodule.decode({})).to.be.undefined; + expect(startioIdSubmodule.decode('')).to.be.undefined; + }); + + it('should return startioId object if valid string passed', function () { + const id = 'test-uuid-12345'; + const result = startioIdSubmodule.decode(id); + expect(result).to.deep.equal({ 'startioId': id }); + }); + }); + + describe('getId', function () { + it('should log an error if no endpoint configured', function () { + const config = { params: {} }; + startioIdSubmodule.getId(config); + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.args[0][0]).to.include('requires an endpoint'); + }); + + it('should log an error if endpoint is not a string', function () { + const config = { params: { endpoint: 123 } }; + startioIdSubmodule.getId(config); + expect(utils.logError.calledOnce).to.be.true; + }); + + it('should return existing storedId immediately if provided', function () { + const storedId = 'existing-id-12345'; + const result = startioIdSubmodule.getId(validConfig, {}, storedId); + expect(result).to.deep.equal({ id: storedId }); + expect(server.requests.length).to.eq(0); + }); + + it('should fetch new ID from server if no storedId provided', function () { + const result = startioIdSubmodule.getId(validConfig); + expect(result).to.have.property('callback'); + expect(typeof result.callback).to.equal('function'); + }); + + it('should invoke callback with ID from server response', function () { + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + expect(request.method).to.eq('GET'); + expect(request.url).to.eq(validConfig.params.endpoint); + + const serverId = 'new-server-id-12345'; + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverId); + }); + + it('should store new ID in both cookie and localStorage by default', function () { + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const serverId = 'new-server-id-12345'; + server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(storage.setCookie.calledOnce).to.be.true; + expect(storage.setCookie.args[0][0]).to.equal('startioId'); + expect(storage.setCookie.args[0][1]).to.equal(serverId); + expect(storage.setDataInLocalStorage.calledOnce).to.be.true; + expect(storage.setDataInLocalStorage.args[0][0]).to.equal('startioId'); + expect(storage.setDataInLocalStorage.args[0][1]).to.equal(serverId); + }); + + it('should store only in cookie when storage type is cookie', function () { + const config = { ...validConfig, storage: { ...validConfig.storage, type: 'cookie' } }; + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const serverId = 'new-server-id-12345'; + server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(storage.setCookie.calledOnce).to.be.true; + expect(storage.setDataInLocalStorage.called).to.be.false; + }); + + it('should store only in localStorage when storage type is html5', function () { + const config = { ...validConfig, storage: { ...validConfig.storage, type: 'html5' } }; + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(config).callback; + callback(callbackSpy); + + const serverId = 'new-server-id-12345'; + server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(storage.setCookie.called).to.be.false; + expect(storage.setDataInLocalStorage.calledOnce).to.be.true; + }); + + it('should log error if server response is missing id field', function () { + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ wrongField: 'value' })); + + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.args[0][0]).to.include('missing \'id\' field'); + }); + + it('should log error if server response is invalid JSON', function () { + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + request.respond(200, { 'Content-Type': 'application/json' }, 'invalid-json{'); + + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.args[0][0]).to.include('Error parsing'); + }); + + it('should log error if server request fails', function () { + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + request.error(); + + expect(utils.logError.calledOnce).to.be.true; + expect(utils.logError.args[0][0]).to.include('encountered an error'); + }); + + it('should not store in cookie if cookies are disabled', function () { + storage.cookiesAreEnabled.returns(false); + + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + const serverId = 'new-server-id-12345'; + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(storage.setCookie.called).to.be.false; + expect(storage.setDataInLocalStorage.calledOnce).to.be.true; + }); + + it('should not store in localStorage if localStorage is disabled', function () { + storage.localStorageIsEnabled.returns(false); + + const callbackSpy = sinon.spy(); + const callback = startioIdSubmodule.getId(validConfig).callback; + callback(callbackSpy); + + const request = server.requests[0]; + const serverId = 'new-server-id-12345'; + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); + + expect(storage.setCookie.calledOnce).to.be.true; + expect(storage.setDataInLocalStorage.called).to.be.false; + }); + }); +}); From 7db347257f74be96e3fc5014a6ff18d1a41643cb Mon Sep 17 00:00:00 2001 From: illiamilshtein Date: Tue, 3 Feb 2026 16:43:57 +0200 Subject: [PATCH 2/5] Update Start.io User ID module to ensure callbacks and AJAX requests fire regardless of endpoint validity --- modules/startioSystem.js | 9 +++------ test/spec/modules/startioSystem_spec.js | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/modules/startioSystem.js b/modules/startioSystem.js index 896d045e78..0a0073212b 100644 --- a/modules/startioSystem.js +++ b/modules/startioSystem.js @@ -5,6 +5,7 @@ import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } fr import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const MODULE_NAME = 'startioId'; +const DEFAULT_ENDPOINT = ''; export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); function storeId(id, storageConfig = {}) { @@ -33,16 +34,12 @@ export const startioIdSubmodule = { getId(config, consentData, storedId) { const configParams = (config && config.params) || {}; const storageConfig = (config && config.storage) || {}; + const endpoint = configParams.endpoint || DEFAULT_ENDPOINT; if (storedId) { return { id: storedId }; } - if (!configParams.endpoint || typeof configParams.endpoint !== 'string') { - logError(`${MODULE_NAME} module requires an endpoint parameter.`); - return; - } - const resp = function (callback) { const callbacks = { success: response => { @@ -65,7 +62,7 @@ export const startioIdSubmodule = { callback(); } }; - ajax(configParams.endpoint, callbacks, undefined, { method: 'GET' }); + ajax(endpoint, callbacks, undefined, { method: 'GET' }); }; return { callback: resp }; }, diff --git a/test/spec/modules/startioSystem_spec.js b/test/spec/modules/startioSystem_spec.js index c891a06a1c..2fc6bf7966 100644 --- a/test/spec/modules/startioSystem_spec.js +++ b/test/spec/modules/startioSystem_spec.js @@ -64,17 +64,26 @@ describe('StartIO ID System', function () { }); describe('getId', function () { - it('should log an error if no endpoint configured', function () { + it('should return callback and fire ajax even if no endpoint configured', function () { const config = { params: {} }; - startioIdSubmodule.getId(config); - expect(utils.logError.calledOnce).to.be.true; - expect(utils.logError.args[0][0]).to.include('requires an endpoint'); + const result = startioIdSubmodule.getId(config); + expect(result).to.have.property('callback'); + expect(typeof result.callback).to.equal('function'); + + const callbackSpy = sinon.spy(); + result.callback(callbackSpy); + expect(server.requests.length).to.equal(1); }); - it('should log an error if endpoint is not a string', function () { + it('should return callback and fire ajax even if endpoint is not a string', function () { const config = { params: { endpoint: 123 } }; - startioIdSubmodule.getId(config); - expect(utils.logError.calledOnce).to.be.true; + const result = startioIdSubmodule.getId(config); + expect(result).to.have.property('callback'); + expect(typeof result.callback).to.equal('function'); + + const callbackSpy = sinon.spy(); + result.callback(callbackSpy); + expect(server.requests.length).to.equal(1); }); it('should return existing storedId immediately if provided', function () { From 8bbf781f310ecf0251fd3c6ae8105af635217e7f Mon Sep 17 00:00:00 2001 From: illiamilshtein Date: Tue, 3 Feb 2026 18:06:19 +0200 Subject: [PATCH 3/5] Remove storage-related functionality from Start.io ID submodule and add support for EIDs. --- modules/startioSystem.js | 27 ++----- modules/startioSystem.md | 3 +- test/spec/modules/startioSystem_spec.js | 99 +++++-------------------- 3 files changed, 28 insertions(+), 101 deletions(-) diff --git a/modules/startioSystem.js b/modules/startioSystem.js index 0a0073212b..9d2ecfd812 100644 --- a/modules/startioSystem.js +++ b/modules/startioSystem.js @@ -1,28 +1,15 @@ +/** + * This module adds startio ID support to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/startioSystem + * @requires module:modules/userId + */ import { logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE } from '../src/storageManager.js'; -import { MODULE_TYPE_UID } from '../src/activities/modules.js'; const MODULE_NAME = 'startioId'; const DEFAULT_ENDPOINT = ''; -export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); - -function storeId(id, storageConfig = {}) { - const expires = storageConfig.expires || 365; - const expirationDate = new Date(Date.now() + (expires * 24 * 60 * 60 * 1000)); - const storageType = storageConfig.type || ''; - const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); - const useLocalStorage = !storageType || storageType.includes(STORAGE_TYPE_LOCALSTORAGE); - - if (useCookie && storage.cookiesAreEnabled()) { - storage.setCookie(MODULE_NAME, id, expirationDate.toUTCString()); - } - - if (useLocalStorage && storage.localStorageIsEnabled()) { - storage.setDataInLocalStorage(MODULE_NAME, id); - } -} export const startioIdSubmodule = { name: MODULE_NAME, @@ -33,7 +20,6 @@ export const startioIdSubmodule = { }, getId(config, consentData, storedId) { const configParams = (config && config.params) || {}; - const storageConfig = (config && config.storage) || {}; const endpoint = configParams.endpoint || DEFAULT_ENDPOINT; if (storedId) { @@ -48,7 +34,6 @@ export const startioIdSubmodule = { const responseObj = JSON.parse(response); if (responseObj && responseObj.id) { responseId = responseObj.id; - storeId(responseId, storageConfig); } else { logError(`${MODULE_NAME}: Server response missing 'id' field`); } diff --git a/modules/startioSystem.md b/modules/startioSystem.md index 44173e78f9..74eba142e7 100644 --- a/modules/startioSystem.md +++ b/modules/startioSystem.md @@ -34,7 +34,8 @@ The below parameters apply only to the Start.io User ID integration. | params | Required | Object | Container of all module params. | | | params.endpoint | Required | String | The URL of the Start.io ID endpoint. Must return a JSON object with an `id` field. | `"https://id.startio.example.com/uid"` | | storage | Optional | Object | Controls how the ID is persisted. Managed by Prebid.js core; see notes below. | | -| storage.type | Optional | String | Storage mechanism. Accepts `cookie`, `html5`, or `cookie&html5`. Defaults to both when omitted. | `"cookie&html5"` | +| storage.name | Required | String | The cookie or local-storage key used to persist the ID. Must match the module name. | `"startioId"` | +| storage.type | Optional | String | Storage mechanism. `"cookie"` for cookies only, `"html5"` for localStorage only. Omit to use both. | `"html5"` | | storage.expires | Optional | Number | Cookie / storage TTL in days. Defaults to `365`. | `365` | ## Server Response Format diff --git a/test/spec/modules/startioSystem_spec.js b/test/spec/modules/startioSystem_spec.js index 2fc6bf7966..ac20ef18b9 100644 --- a/test/spec/modules/startioSystem_spec.js +++ b/test/spec/modules/startioSystem_spec.js @@ -1,6 +1,7 @@ import * as utils from '../../../src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { startioIdSubmodule, storage } from 'modules/startioSystem.js'; +import { startioIdSubmodule } from 'modules/startioSystem.js'; +import { createEidsArray } from '../../../modules/userId/eids.js'; describe('StartIO ID System', function () { let sandbox; @@ -17,12 +18,6 @@ describe('StartIO ID System', function () { beforeEach(function () { sandbox = sinon.createSandbox(); sandbox.stub(utils, 'logError'); - sandbox.stub(storage, 'getCookie').returns(null); - sandbox.stub(storage, 'setCookie'); - sandbox.stub(storage, 'getDataFromLocalStorage').returns(null); - sandbox.stub(storage, 'setDataInLocalStorage'); - sandbox.stub(storage, 'cookiesAreEnabled').returns(true); - sandbox.stub(storage, 'localStorageIsEnabled').returns(true); }); afterEach(function () { @@ -63,6 +58,24 @@ describe('StartIO ID System', function () { }); }); + describe('eid', function () { + it('should generate correct EID', function () { + const TEST_UID = 'test-uid-value'; + const eids = createEidsArray(startioIdSubmodule.decode(TEST_UID), new Map(Object.entries(startioIdSubmodule.eids))); + expect(eids).to.eql([ + { + source: 'start.io', + uids: [ + { + atype: 3, + id: TEST_UID + } + ] + } + ]); + }); + }); + describe('getId', function () { it('should return callback and fire ajax even if no endpoint configured', function () { const config = { params: {} }; @@ -115,48 +128,6 @@ describe('StartIO ID System', function () { expect(callbackSpy.lastCall.lastArg).to.equal(serverId); }); - it('should store new ID in both cookie and localStorage by default', function () { - const callbackSpy = sinon.spy(); - const callback = startioIdSubmodule.getId(validConfig).callback; - callback(callbackSpy); - - const serverId = 'new-server-id-12345'; - server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); - - expect(storage.setCookie.calledOnce).to.be.true; - expect(storage.setCookie.args[0][0]).to.equal('startioId'); - expect(storage.setCookie.args[0][1]).to.equal(serverId); - expect(storage.setDataInLocalStorage.calledOnce).to.be.true; - expect(storage.setDataInLocalStorage.args[0][0]).to.equal('startioId'); - expect(storage.setDataInLocalStorage.args[0][1]).to.equal(serverId); - }); - - it('should store only in cookie when storage type is cookie', function () { - const config = { ...validConfig, storage: { ...validConfig.storage, type: 'cookie' } }; - const callbackSpy = sinon.spy(); - const callback = startioIdSubmodule.getId(config).callback; - callback(callbackSpy); - - const serverId = 'new-server-id-12345'; - server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); - - expect(storage.setCookie.calledOnce).to.be.true; - expect(storage.setDataInLocalStorage.called).to.be.false; - }); - - it('should store only in localStorage when storage type is html5', function () { - const config = { ...validConfig, storage: { ...validConfig.storage, type: 'html5' } }; - const callbackSpy = sinon.spy(); - const callback = startioIdSubmodule.getId(config).callback; - callback(callbackSpy); - - const serverId = 'new-server-id-12345'; - server.requests[0].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); - - expect(storage.setCookie.called).to.be.false; - expect(storage.setDataInLocalStorage.calledOnce).to.be.true; - }); - it('should log error if server response is missing id field', function () { const callbackSpy = sinon.spy(); const callback = startioIdSubmodule.getId(validConfig).callback; @@ -192,35 +163,5 @@ describe('StartIO ID System', function () { expect(utils.logError.calledOnce).to.be.true; expect(utils.logError.args[0][0]).to.include('encountered an error'); }); - - it('should not store in cookie if cookies are disabled', function () { - storage.cookiesAreEnabled.returns(false); - - const callbackSpy = sinon.spy(); - const callback = startioIdSubmodule.getId(validConfig).callback; - callback(callbackSpy); - - const request = server.requests[0]; - const serverId = 'new-server-id-12345'; - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); - - expect(storage.setCookie.called).to.be.false; - expect(storage.setDataInLocalStorage.calledOnce).to.be.true; - }); - - it('should not store in localStorage if localStorage is disabled', function () { - storage.localStorageIsEnabled.returns(false); - - const callbackSpy = sinon.spy(); - const callback = startioIdSubmodule.getId(validConfig).callback; - callback(callbackSpy); - - const request = server.requests[0]; - const serverId = 'new-server-id-12345'; - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ id: serverId })); - - expect(storage.setCookie.calledOnce).to.be.true; - expect(storage.setDataInLocalStorage.called).to.be.false; - }); }); }); From 4f2aee9b154f4d427a79fe7ce5906152ee930aa4 Mon Sep 17 00:00:00 2001 From: illiamilshtein Date: Thu, 5 Feb 2026 14:49:41 +0200 Subject: [PATCH 4/5] Add iframe-based user syncing to Start.io Bid Adapter with consent parameter support --- modules/startioBidAdapter.js | 21 ++++- modules/startioBidAdapter.md | 19 +++++ test/spec/modules/startioBidAdapter_spec.js | 94 +++++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) diff --git a/modules/startioBidAdapter.js b/modules/startioBidAdapter.js index 74629f2cc9..c165b30cdc 100644 --- a/modules/startioBidAdapter.js +++ b/modules/startioBidAdapter.js @@ -1,13 +1,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; -import { logError, isFn, isPlainObject } from '../src/utils.js'; +import { logError, isFn, isPlainObject, formatQS } from '../src/utils.js'; import { ortbConverter } from '../libraries/ortbConverter/converter.js' import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js'; +import { getUserSyncParams } from '../libraries/userSyncUtils/userSyncUtils.js'; const BIDDER_CODE = 'startio'; const METHOD = 'POST'; const GVLID = 1216; const ENDPOINT_URL = `https://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`; +const IFRAME_URL = 'test'; const converter = ortbConverter({ imp(buildImp, bidRequest, context) { @@ -151,6 +153,23 @@ export const spec = { }, onSetTargeting: (bid) => { }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const syncs = []; + + if (syncOptions.iframeEnabled) { + const consentParams = getUserSyncParams(gdprConsent, uspConsent, gppConsent); + const queryString = formatQS(consentParams); + const queryParam = queryString ? `?${queryString}` : ''; + + syncs.push({ + type: 'iframe', + url: `${IFRAME_URL}${queryParam}` + }); + } + + return syncs; + } }; registerBidder(spec); diff --git a/modules/startioBidAdapter.md b/modules/startioBidAdapter.md index 172af1aeb4..6404b77ba2 100644 --- a/modules/startioBidAdapter.md +++ b/modules/startioBidAdapter.md @@ -94,6 +94,25 @@ var nativeAdUnits = [ ]; ``` +# User Syncs + +The adapter supports iframe-based user syncing. When `iframeEnabled` is set to `true` in the sync options, the adapter returns a single iframe sync URL. GDPR, USP (CCPA), and GPP consent strings are automatically appended as query parameters when present. + +``` +pbjs.setConfig({ + userSync: { + iframeEnabled: true + } +}); +``` + +**Consent parameters included in the sync URL (when available):** +- `gdpr` – `1` if GDPR applies, `0` otherwise +- `gdpr_consent` – the TCF consent string +- `us_privacy` – the USP/CCPA consent string (e.g. `1YNN`) +- `gpp` – the GPP consent string +- `gpp_sections` – applicable GPP section IDs + # Additional Notes - The adapter processes requests via OpenRTB 2.5 standards. - Ensure that the `accountId` parameter is set correctly for your integration. diff --git a/test/spec/modules/startioBidAdapter_spec.js b/test/spec/modules/startioBidAdapter_spec.js index 021c11e80d..4abb0f3bd2 100644 --- a/test/spec/modules/startioBidAdapter_spec.js +++ b/test/spec/modules/startioBidAdapter_spec.js @@ -370,4 +370,98 @@ describe('Prebid Adapter: Startio', function () { }); } }); + + describe('getUserSyncs', function () { + it('should return an iframe sync when iframeEnabled is true', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.be.a('string'); + }); + + it('should return an empty array when iframeEnabled is false', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: false }, []); + + expect(syncs).to.have.lengthOf(0); + }); + + it('should return an empty array when syncOptions is empty', function () { + const syncs = spec.getUserSyncs({}, []); + + expect(syncs).to.have.lengthOf(0); + }); + + it('should append GDPR consent params to the sync URL', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' + }; + + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + }); + + it('should append gdpr=0 when gdprApplies is false', function () { + const gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent); + + expect(syncs[0].url).to.include('gdpr=0'); + }); + + it('should append USP consent param to the sync URL', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], undefined, '1YNN'); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].url).to.include('us_privacy=1YNN'); + }); + + it('should append GPP consent params to the sync URL', function () { + const gppConsent = { + gppString: 'DBABMA~BAAAAAAAAgA.QA', + applicableSections: [7, 8] + }; + + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], undefined, undefined, gppConsent); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].url).to.include('gpp=DBABMA~BAAAAAAAAgA.QA'); + expect(syncs[0].url).to.include('gpp_sid=7,8'); + }); + + it('should append all consent params together when all are provided', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'testConsent' + }; + const uspConsent = '1YNN'; + const gppConsent = { + gppString: 'testGpp', + applicableSections: [2] + }; + + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent, uspConsent, gppConsent); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].url).to.include('gdpr=1'); + expect(syncs[0].url).to.include('gdpr_consent=testConsent'); + expect(syncs[0].url).to.include('us_privacy=1YNN'); + expect(syncs[0].url).to.include('gpp=testGpp'); + expect(syncs[0].url).to.include('gpp_sid=2'); + }); + + it('should not append query string when no consent params are provided', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].url).to.not.include('?'); + }); + }); }); From d6e0c985683a574e63685bf0cc9710115de82f1c Mon Sep 17 00:00:00 2001 From: illiamilshtein Date: Thu, 5 Feb 2026 14:51:17 +0200 Subject: [PATCH 5/5] Simplify Start.io ID module by removing storage-related parameters and endpoint configuration. --- modules/startioSystem.md | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/modules/startioSystem.md b/modules/startioSystem.md index 74eba142e7..53b6e03531 100644 --- a/modules/startioSystem.md +++ b/modules/startioSystem.md @@ -10,15 +10,7 @@ For integration support, contact prebid@start.io. pbjs.setConfig({ userSync: { userIds: [{ - name: 'startioId', - params: { - endpoint: 'https://your-startio-endpoint.example.com/id' - }, - storage: { - type: 'html5', //or 'cookie', or 'cookie&html5', - name: 'startioId', - expires: 365 - } + name: 'startioId' }] } }); @@ -31,12 +23,6 @@ The below parameters apply only to the Start.io User ID integration. | Param under userSync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | The name of this module. | `"startioId"` | -| params | Required | Object | Container of all module params. | | -| params.endpoint | Required | String | The URL of the Start.io ID endpoint. Must return a JSON object with an `id` field. | `"https://id.startio.example.com/uid"` | -| storage | Optional | Object | Controls how the ID is persisted. Managed by Prebid.js core; see notes below. | | -| storage.name | Required | String | The cookie or local-storage key used to persist the ID. Must match the module name. | `"startioId"` | -| storage.type | Optional | String | Storage mechanism. `"cookie"` for cookies only, `"html5"` for localStorage only. Omit to use both. | `"html5"` | -| storage.expires | Optional | Number | Cookie / storage TTL in days. Defaults to `365`. | `365` | ## Server Response Format