From b58e1b3d57a81225fec798f33d1df803c28b7edd Mon Sep 17 00:00:00 2001 From: Marcin Muras Date: Thu, 31 Jul 2025 14:32:56 +0200 Subject: [PATCH 1/3] Add Gemius User ID Submodule --- modules/.submodules.json | 1 + modules/gemiusIdSystem.js | 105 ++++++++++++++++ modules/gemiusIdSystem.md | 31 +++++ modules/userId/eids.md | 7 ++ modules/userId/userId.md | 4 +- test/spec/modules/gemiusIdSystem_spec.js | 151 +++++++++++++++++++++++ 6 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 modules/gemiusIdSystem.js create mode 100644 modules/gemiusIdSystem.md create mode 100644 test/spec/modules/gemiusIdSystem_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index 5aa83c64376..a1b2971bfea 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -18,6 +18,7 @@ "fabrickIdSystem", "freepassIdSystem", "ftrackIdSystem", + "gemiusIdSystem", "gravitoIdSystem", "growthCodeIdSystem", "hadronIdSystem", diff --git a/modules/gemiusIdSystem.js b/modules/gemiusIdSystem.js new file mode 100644 index 00000000000..2d5d7c341e7 --- /dev/null +++ b/modules/gemiusIdSystem.js @@ -0,0 +1,105 @@ +import { logInfo, logError, isStr, getWindowTop, canAccessWindowTop, getWindowSelf } from '../src/utils.js'; +import { submodule } from '../src/hook.js'; + +const MODULE_NAME = 'gemiusId'; +const GVLID = 328; +const REQUIRED_PURPOSES = [1, 2, 3, 4, 7, 8, 9, 10]; +const LOG_PREFIX = 'Gemius User ID: '; + +const WAIT_FOR_PRIMARY_SCRIPT_MAX_TRIES = 8; +const WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS = 50; +const GEMIUS_CMD_TIMEOUT = 8000; + +function getPrimaryScriptWindow() { + if (canAccessWindowTop()) { + return getWindowTop(); + } + + return getWindowSelf(); +} + +function retrieveId(primaryScriptWindow, callback) { + let resultResolved = false; + let timeoutId = null; + const setResult = function (...args) { + if (resultResolved) { + return; + } + + resultResolved = true; + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + callback(...args); + } + + timeoutId = setTimeout(() => { + logError(LOG_PREFIX + 'failed to get id, timeout'); + timeoutId = null; + setResult(); + }, GEMIUS_CMD_TIMEOUT); + + try { + primaryScriptWindow.gemius_cmd('get_ruid', function (ruid, desc) { + if (desc.status === 'ok') { + setResult({id: ruid}); + } else if (desc.status === 'no-consent') { + logInfo(LOG_PREFIX + 'failed to get id, no consent'); + setResult({id: null}); + } else { + logError(LOG_PREFIX + 'failed to get id, response: ' + desc.status); + setResult(); + } + }); + } catch (e) { + logError(LOG_PREFIX + 'failed to get id, error: ' + e); + setResult(); + } +} + +export const gemiusIdSubmodule = { + name: MODULE_NAME, + gvlid: GVLID, + decode(value) { + if (isStr(value?.id)) { + return { [MODULE_NAME]: value.id }; + } + return undefined; + }, + getId(_, {gdpr: consentData} = {}) { + if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { + if (REQUIRED_PURPOSES.some(purposeId => !consentData.vendorData?.purpose?.consents?.[purposeId])) { + logInfo(LOG_PREFIX + 'getId, no consent'); + return {id: {id: null}}; + } + } + + logInfo(LOG_PREFIX + 'getId'); + return { + callback: function (callback) { + const win = getPrimaryScriptWindow(); + + (function waitForPrimaryScript(tryCount = 1, nextWaitTime = WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS) { + if (typeof win.gemius_cmd !== 'undefined') { + retrieveId(win, callback); + } + + if (tryCount < WAIT_FOR_PRIMARY_SCRIPT_MAX_TRIES) { + setTimeout(() => waitForPrimaryScript(tryCount + 1, nextWaitTime * 2), nextWaitTime); + } else { + callback(); + } + })(); + } + }; + }, + eids: { + [MODULE_NAME]: { + source: 'gemius.com', + atype: 1, + }, + } +}; + +submodule('userId', gemiusIdSubmodule); diff --git a/modules/gemiusIdSystem.md b/modules/gemiusIdSystem.md new file mode 100644 index 00000000000..e285a673db6 --- /dev/null +++ b/modules/gemiusIdSystem.md @@ -0,0 +1,31 @@ +## Gemius User ID Submodule + +This module supports [Gemius](https://gemius.com/) customers in using Real Users ID (RUID) functionality. + +## Building Prebid.js with Gemius User ID Submodule + +To build Prebid.js with the `gemiusIdSystem` module included: + +``` +gulp build --modules=userId,gemiusIdSystem +``` + +### Prebid Configuration + +You can configure this submodule in your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'gemiusId', + storage: { + name: 'pbjs_gemiusId', + type: 'cookie', + expires: 30, + refreshInSeconds: 3600 + } + }] + } +}); +``` diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 53567032175..f6f62229f53 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -335,6 +335,13 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 1 }] + }, + { + source: 'gemius.com'', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 9aea3e8d533..9cc2ce5bf5c 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -137,7 +137,9 @@ pbjs.setConfig({ name: '__adm__admixer', expires: 30 } - },{ + }, { + name: "gemiusId" + }, { name: "kpuid", params:{ accountid: 124 // example of account id diff --git a/test/spec/modules/gemiusIdSystem_spec.js b/test/spec/modules/gemiusIdSystem_spec.js new file mode 100644 index 00000000000..aefdec4a68a --- /dev/null +++ b/test/spec/modules/gemiusIdSystem_spec.js @@ -0,0 +1,151 @@ +import { gemiusIdSubmodule } from 'modules/gemiusIdSystem.js'; +import * as utils from 'src/utils.js'; + +describe('GemiusId module', function () { + let getWindowTopStub; + let mockWindow; + let clock; + + beforeEach(function () { + mockWindow = { + gemius_cmd: sinon.stub() + }; + getWindowTopStub = sinon.stub(utils, 'getWindowTop').returns(mockWindow); + }); + + afterEach(function () { + getWindowTopStub.restore(); + if (clock) { + clock.restore(); + clock = undefined; + } + }); + + describe('gemiusIdSubmodule', function () { + it('should have the correct name', function () { + expect(gemiusIdSubmodule.name).to.equal('gemiusId'); + }); + + it('should have correct eids configuration', function () { + expect(gemiusIdSubmodule.eids.gemiusId).to.deep.include({ + source: 'gemius.com', + atype: 1 + }); + }); + + it('should have correct gvlid', function () { + expect(gemiusIdSubmodule.gvlid).to.be.a('number'); + }); + }); + + describe('getId', function () { + const gdprConsentData = { + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: true, + 2: false, + 3: true, 4: true, 5: true, 6: true, 7: true, 8: true, 9: true, 10: true, 11: true + } + } + } + }; + + it('should return undefined if gemius_cmd is not available', function (done) { + clock = sinon.useFakeTimers(); + getWindowTopStub.returns({}); + + gemiusIdSubmodule.getId().callback((resultId) => { + expect(resultId).to.be.undefined; + done(); + }); + + clock.tick(6400); + }); + + it('should return null id if no consent', function () { + const result = gemiusIdSubmodule.getId({}, { + gdpr: gdprConsentData + }); + expect(result).to.deep.equal({id: {id: null}}); + }); + + it('should return callback on consent', function () { + const result = gemiusIdSubmodule.getId({}, { + gdpr: utils.deepClone(gdprConsentData).vendorData.purpose.consents["2"] = true + }); + expect(result).to.have.property('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('should return callback when gemius_cmd is available', function () { + const result = gemiusIdSubmodule.getId(); + expect(result).to.have.property('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('should call gemius_cmd with correct parameters', function (done) { + mockWindow.gemius_cmd.callsFake((command, callback) => { + expect(command).to.equal('get_ruid'); + expect(callback).to.be.a('function'); + + const testRuid = 'test-ruid-123'; + const statusOk = {status: 'ok'}; + callback(testRuid, statusOk); + }); + + gemiusIdSubmodule.getId().callback((resultId) => { + expect(resultId).to.deep.equal({id: 'test-ruid-123'}); + expect(mockWindow.gemius_cmd.calledOnce).to.be.true; + done(); + }); + }); + + it('should handle gemius_cmd throwing an error', function (done) { + mockWindow.gemius_cmd.callsFake(() => { + throw new Error(); + }); + + const result = gemiusIdSubmodule.getId(); + result.callback((resultId) => { + expect(resultId).to.be.undefined; + done(); + }); + }); + + it('should handle gemius_cmd not calling callback', function (done) { + const clock = sinon.useFakeTimers(); + + mockWindow.gemius_cmd.callsFake((command, callback) => { + // Don't call callback to simulate timeout/no response + }); + + gemiusIdSubmodule.getId().callback((resultId) => { + expect(resultId).to.be.undefined; + clock.restore(); + done(); + }); + + clock.tick(8100); + }); + }); + + describe('decode', function () { + it('should return object with gemiusId when value exists', function () { + const result = gemiusIdSubmodule.decode({id: 'test-gemius-id'}); + expect(result).to.deep.equal({ + gemiusId: 'test-gemius-id' + }); + }); + + it('should return undefined when value is falsy', function () { + expect(gemiusIdSubmodule.decode('')).to.be.undefined; + expect(gemiusIdSubmodule.decode(null)).to.be.undefined; + expect(gemiusIdSubmodule.decode(undefined)).to.be.undefined; + expect(gemiusIdSubmodule.decode(0)).to.be.undefined; + expect(gemiusIdSubmodule.decode(false)).to.be.undefined; + }); + }); +}); From 196825662ebd6eeb4154bcbf11b8614e75d53da2 Mon Sep 17 00:00:00 2001 From: Marcin Muras Date: Mon, 4 Aug 2025 12:48:21 +0200 Subject: [PATCH 2/3] Add Gemius User ID Submodule - switch to typescript --- .../{gemiusIdSystem.js => gemiusIdSystem.ts} | 40 +++++++++++-------- test/spec/modules/gemiusIdSystem_spec.js | 4 +- 2 files changed, 26 insertions(+), 18 deletions(-) rename modules/{gemiusIdSystem.js => gemiusIdSystem.ts} (65%) diff --git a/modules/gemiusIdSystem.js b/modules/gemiusIdSystem.ts similarity index 65% rename from modules/gemiusIdSystem.js rename to modules/gemiusIdSystem.ts index 2d5d7c341e7..97364dd31bb 100644 --- a/modules/gemiusIdSystem.js +++ b/modules/gemiusIdSystem.ts @@ -1,7 +1,10 @@ import { logInfo, logError, isStr, getWindowTop, canAccessWindowTop, getWindowSelf } from '../src/utils.js'; import { submodule } from '../src/hook.js'; +import { AllConsentData } from "../src/consentHandler.ts"; -const MODULE_NAME = 'gemiusId'; +import type { IdProviderSpec } from './userId/spec.ts'; + +const MODULE_NAME = 'gemiusId' as const; const GVLID = 328; const REQUIRED_PURPOSES = [1, 2, 3, 4, 7, 8, 9, 10]; const LOG_PREFIX = 'Gemius User ID: '; @@ -10,7 +13,12 @@ const WAIT_FOR_PRIMARY_SCRIPT_MAX_TRIES = 8; const WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS = 50; const GEMIUS_CMD_TIMEOUT = 8000; -function getPrimaryScriptWindow() { +type SerializableId = string | Record; +type PrimaryScriptWindow = Window & { + gemius_cmd: (action: string, callback: (ruid: string, desc: { status: string }) => void) => void; +}; + +function getTopAccessibleWindow(): Window { if (canAccessWindowTop()) { return getWindowTop(); } @@ -18,10 +26,10 @@ function getPrimaryScriptWindow() { return getWindowSelf(); } -function retrieveId(primaryScriptWindow, callback) { +function retrieveId(primaryScriptWindow: PrimaryScriptWindow, callback: (id: SerializableId) => void): void { let resultResolved = false; - let timeoutId = null; - const setResult = function (...args) { + let timeoutId: number | null = null; + const setResult = function (id?: SerializableId): void { if (resultResolved) { return; } @@ -31,7 +39,7 @@ function retrieveId(primaryScriptWindow, callback) { clearTimeout(timeoutId); timeoutId = null; } - callback(...args); + callback(id); } timeoutId = setTimeout(() => { @@ -58,18 +66,18 @@ function retrieveId(primaryScriptWindow, callback) { } } -export const gemiusIdSubmodule = { +export const gemiusIdSubmodule: IdProviderSpec = { name: MODULE_NAME, gvlid: GVLID, decode(value) { - if (isStr(value?.id)) { - return { [MODULE_NAME]: value.id }; + if (isStr(value?.['id'])) { + return {[MODULE_NAME]: value['id']}; } return undefined; }, - getId(_, {gdpr: consentData} = {}) { + getId(_, {gdpr: consentData}: Partial = {}) { if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) { - if (REQUIRED_PURPOSES.some(purposeId => !consentData.vendorData?.purpose?.consents?.[purposeId])) { + if (REQUIRED_PURPOSES.some(purposeId => !(consentData.vendorData?.purpose as any)?.consents?.[purposeId])) { logInfo(LOG_PREFIX + 'getId, no consent'); return {id: {id: null}}; } @@ -78,17 +86,17 @@ export const gemiusIdSubmodule = { logInfo(LOG_PREFIX + 'getId'); return { callback: function (callback) { - const win = getPrimaryScriptWindow(); + const win = getTopAccessibleWindow(); (function waitForPrimaryScript(tryCount = 1, nextWaitTime = WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS) { - if (typeof win.gemius_cmd !== 'undefined') { - retrieveId(win, callback); + if (typeof win['gemius_cmd'] !== 'undefined') { + retrieveId(win as PrimaryScriptWindow, callback); } if (tryCount < WAIT_FOR_PRIMARY_SCRIPT_MAX_TRIES) { setTimeout(() => waitForPrimaryScript(tryCount + 1, nextWaitTime * 2), nextWaitTime); } else { - callback(); + callback(undefined); } })(); } @@ -97,7 +105,7 @@ export const gemiusIdSubmodule = { eids: { [MODULE_NAME]: { source: 'gemius.com', - atype: 1, + atype: '1', }, } }; diff --git a/test/spec/modules/gemiusIdSystem_spec.js b/test/spec/modules/gemiusIdSystem_spec.js index aefdec4a68a..76c4549a321 100644 --- a/test/spec/modules/gemiusIdSystem_spec.js +++ b/test/spec/modules/gemiusIdSystem_spec.js @@ -1,4 +1,4 @@ -import { gemiusIdSubmodule } from 'modules/gemiusIdSystem.js'; +import { gemiusIdSubmodule } from 'modules/gemiusIdSystem.ts'; import * as utils from 'src/utils.js'; describe('GemiusId module', function () { @@ -29,7 +29,7 @@ describe('GemiusId module', function () { it('should have correct eids configuration', function () { expect(gemiusIdSubmodule.eids.gemiusId).to.deep.include({ source: 'gemius.com', - atype: 1 + atype: '1' }); }); From cd88331f20f21ad12ea79a0e892dd7f9d3e27d81 Mon Sep 17 00:00:00 2001 From: Marcin Muras Date: Mon, 4 Aug 2025 15:46:08 +0200 Subject: [PATCH 3/3] Add Gemius User ID Submodule - additional type informations --- modules/gemiusIdSystem.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/gemiusIdSystem.ts b/modules/gemiusIdSystem.ts index 97364dd31bb..e5e6d335857 100644 --- a/modules/gemiusIdSystem.ts +++ b/modules/gemiusIdSystem.ts @@ -18,6 +18,15 @@ type PrimaryScriptWindow = Window & { gemius_cmd: (action: string, callback: (ruid: string, desc: { status: string }) => void) => void; }; +declare module './userId/spec' { + interface UserId { + gemiusId: string; + } + interface ProvidersToId { + gemiusId: 'gemiusId'; + } +} + function getTopAccessibleWindow(): Window { if (canAccessWindowTop()) { return getWindowTop();