Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -18,6 +18,7 @@
"fabrickIdSystem",
"freepassIdSystem",
"ftrackIdSystem",
"gemiusIdSystem",
"gravitoIdSystem",
"growthCodeIdSystem",
"hadronIdSystem",
Expand Down
31 changes: 31 additions & 0 deletions modules/gemiusIdSystem.md
Original file line number Diff line number Diff line change
@@ -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
}
}]
}
});
```
122 changes: 122 additions & 0 deletions modules/gemiusIdSystem.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { logInfo, logError, isStr, getWindowTop, canAccessWindowTop, getWindowSelf } from '../src/utils.js';
import { submodule } from '../src/hook.js';
import { AllConsentData } from "../src/consentHandler.ts";

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: ';

const WAIT_FOR_PRIMARY_SCRIPT_MAX_TRIES = 8;
const WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS = 50;
const GEMIUS_CMD_TIMEOUT = 8000;

type SerializableId = string | Record<string, unknown>;
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();
}

return getWindowSelf();
}

function retrieveId(primaryScriptWindow: PrimaryScriptWindow, callback: (id: SerializableId) => void): void {
let resultResolved = false;
let timeoutId: number | null = null;
const setResult = function (id?: SerializableId): void {
if (resultResolved) {
return;
}

resultResolved = true;
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = null;
}
callback(id);
}

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: IdProviderSpec<typeof MODULE_NAME> = {
name: MODULE_NAME,
gvlid: GVLID,
decode(value) {
if (isStr(value?.['id'])) {
return {[MODULE_NAME]: value['id']};
}
return undefined;
},
getId(_, {gdpr: consentData}: Partial<AllConsentData> = {}) {
if (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) {
if (REQUIRED_PURPOSES.some(purposeId => !(consentData.vendorData?.purpose as any)?.consents?.[purposeId])) {
logInfo(LOG_PREFIX + 'getId, no consent');
return {id: {id: null}};
}
}

logInfo(LOG_PREFIX + 'getId');
return {
callback: function (callback) {
const win = getTopAccessibleWindow();

(function waitForPrimaryScript(tryCount = 1, nextWaitTime = WAIT_FOR_PRIMARY_SCRIPT_INITIAL_WAIT_MS) {
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(undefined);
}
})();
}
};
},
eids: {
[MODULE_NAME]: {
source: 'gemius.com',
atype: '1',
},
}
};

submodule('userId', gemiusIdSubmodule);
7 changes: 7 additions & 0 deletions modules/userId/eids.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,13 @@ userIdAsEids = [
id: 'some-random-id-value',
atype: 1
}]
},
{
source: 'gemius.com'',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
}
]
```
4 changes: 3 additions & 1 deletion modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ pbjs.setConfig({
name: '__adm__admixer',
expires: 30
}
},{
}, {
name: "gemiusId"
}, {
name: "kpuid",
params:{
accountid: 124 // example of account id
Expand Down
151 changes: 151 additions & 0 deletions test/spec/modules/gemiusIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { gemiusIdSubmodule } from 'modules/gemiusIdSystem.ts';
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;
});
});
});
Loading