Skip to content

Commit

Permalink
Gamera Rtd Provider: Initial release (#12424)
Browse files Browse the repository at this point in the history
* - add gameraRtdProvider

* - allow all site,user and imp to be enriched

* - jsdoc external fn
- consent handling in the gamera script
  • Loading branch information
aleksatr authored Nov 8, 2024
1 parent b60d732 commit 05a1065
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 0 deletions.
107 changes: 107 additions & 0 deletions modules/gameraRtdProvider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { submodule } from '../src/hook.js';
import { getGlobal } from '../src/prebidGlobal.js';
import {
isPlainObject,
logError,
mergeDeep,
deepClone,
} from '../src/utils.js';

const MODULE_NAME = 'gamera';
const MODULE = `${MODULE_NAME}RtdProvider`;

/**
* Initialize the Gamera RTD Module.
* @param {Object} config
* @param {Object} userConsent
* @returns {boolean}
*/
function init(config, userConsent) {
return true;
}

/**
* Modify bid request data before auction
* @param {Object} reqBidsConfigObj - The bid request config object
* @param {function} callback - Callback function to execute after data handling
* @param {Object} config - Module configuration
* @param {Object} userConsent - User consent data
*/
function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) {
// Check if window.gamera.getPrebidSegments is available
if (typeof window.gamera?.getPrebidSegments !== 'function') {
window.gamera = window.gamera || {};
window.gamera.cmd = window.gamera.cmd || [];
window.gamera.cmd.push(function () {
enrichAuction(reqBidsConfigObj, callback, config, userConsent);
});
return;
}

enrichAuction(reqBidsConfigObj, callback, config, userConsent);
}

/**
* Enriches the auction with user and content segments from Gamera's on-page script
* @param {Object} reqBidsConfigObj - The bid request config object
* @param {Function} callback - Callback function to execute after data handling
* @param {Object} config - Module configuration
* @param {Object} userConsent - User consent data
*/
function enrichAuction(reqBidsConfigObj, callback, config, userConsent) {
try {
/**
* @function external:"window.gamera".getPrebidSegments
* @description Retrieves user and content segments from Gamera's on-page script
* @param {Function|null} onSegmentsUpdateCallback - Callback for segment updates (not used here)
* @param {Object} config - Module configuration
* @param {Object} userConsent - User consent data
* @returns {Object|undefined} segments - The targeting segments object containing:
* @property {Object} [user] - User-level attributes to merge into ortb2.user
* @property {Object} [site] - Site-level attributes to merge into ortb2.site
* @property {Object.<string, Object>} [adUnits] - Ad unit specific attributes, keyed by adUnitCode,
* to merge into each ad unit's ortb2Imp
*/
const segments = window.gamera.getPrebidSegments(null, deepClone(config || {}), deepClone(userConsent || {})) || {};

// Initialize ortb2Fragments and its nested objects
reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {};
reqBidsConfigObj.ortb2Fragments.global = reqBidsConfigObj.ortb2Fragments.global || {};

// Add user-level data
if (segments.user && isPlainObject(segments.user)) {
reqBidsConfigObj.ortb2Fragments.global.user = reqBidsConfigObj.ortb2Fragments.global.user || {};
mergeDeep(reqBidsConfigObj.ortb2Fragments.global.user, segments.user);
}

// Add site-level data
if (segments.site && isPlainObject(segments.site)) {
reqBidsConfigObj.ortb2Fragments.global.site = reqBidsConfigObj.ortb2Fragments.global.site || {};
mergeDeep(reqBidsConfigObj.ortb2Fragments.global.site, segments.site);
}

// Add adUnit-level data
const adUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits || [];
adUnits.forEach(adUnit => {
const gameraData = segments.adUnits && segments.adUnits[adUnit.code];
if (!gameraData || !isPlainObject(gameraData)) {
return;
}

adUnit.ortb2Imp = adUnit.ortb2Imp || {};
mergeDeep(adUnit.ortb2Imp, gameraData);
});
} catch (error) {
logError(MODULE, 'Error getting segments:', error);
}

callback();
}

export const subModuleObj = {
name: MODULE_NAME,
init: init,
getBidRequestData: getBidRequestData,
};

submodule('realTimeData', subModuleObj);
51 changes: 51 additions & 0 deletions modules/gameraRtdProvider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Overview

Module Name: Gamera Rtd Provider
Module Type: Rtd Provider
Maintainer: [email protected]

# Description

RTD provider for Gamera.ai that enriches bid requests with real-time data, by populating the [First Party Data](https://docs.prebid.org/features/firstPartyData.html) attributes.
The module integrates with Gamera's AI-powered audience segmentation system to provide enhanced bidding capabilities.
The Gamera RTD Provider works in conjunction with the Gamera script, which must be available on the page for the module to enrich bid requests. To learn more about the Gamera script, please visit the [Gamera website](https://gamera.ai/).

ORTB2 enrichments that gameraRtdProvider can provide:
* `ortb2.site`
* `ortb2.user`
* `AdUnit.ortb2Imp`

# Integration

## Build

Include the Gamera RTD module in your Prebid.js build:

```bash
gulp build --modules=rtdModule,gameraRtdProvider
```

## Configuration

Configure the module in your Prebid.js configuration:

```javascript
pbjs.setConfig({
realTimeData: {
dataProviders: [{
name: 'gamera',
params: {
// Optional configuration parameters
}
}]
}
});
```

### Configuration Parameters

The module currently supports basic initialization without required parameters. Future versions may include additional configuration options.

## Support

For more information or support, please contact [email protected].
223 changes: 223 additions & 0 deletions test/spec/modules/gameraRtdProvider_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { submodule } from 'src/hook.js';
import { getGlobal } from 'src/prebidGlobal.js';
import * as utils from 'src/utils.js';
import { subModuleObj } from 'modules/gameraRtdProvider.js';

describe('gameraRtdProvider', function () {
let logErrorSpy;

beforeEach(function () {
logErrorSpy = sinon.spy(utils, 'logError');
});

afterEach(function () {
logErrorSpy.restore();
});

describe('subModuleObj', function () {
it('should have the correct module name', function () {
expect(subModuleObj.name).to.equal('gamera');
});

it('successfully instantiates and returns true', function () {
expect(subModuleObj.init()).to.equal(true);
});
});

describe('getBidRequestData', function () {
const reqBidsConfigObj = {
adUnits: [{
code: 'test-div',
mediaTypes: {
banner: {
sizes: [[300, 250]]
}
},
ortb2Imp: {
ext: {
data: {
pbadslot: 'homepage-top-rect',
adUnitSpecificAttribute: '123',
}
}
},
bids: [{ bidder: 'test' }]
}],
ortb2Fragments: {
global: {
site: {
name: 'example',
domain: 'page.example.com',
// OpenRTB 2.5 spec / Content Taxonomy
cat: ['IAB2'],
sectioncat: ['IAB2-2'],
pagecat: ['IAB2-2'],

page: 'https://page.example.com/here.html',
ref: 'https://ref.example.com',
keywords: 'power tools, drills',
search: 'drill',
content: {
userrating: '4',
data: [{
name: 'www.dataprovider1.com', // who resolved the segments
ext: {
segtax: 7, // taxonomy used to encode the segments
cids: ['iris_c73g5jq96mwso4d8']
},
// the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3
segment: [{ id: '687' }, { id: '123' }]
}]
},
ext: {
data: { // fields that aren't part of openrtb 2.6
pageType: 'article',
category: 'repair'
}
}
},
// this is where the user data is placed
user: {
keywords: 'a,b',
data: [{
name: 'dataprovider.com',
ext: {
segtax: 4
},
segment: [{
id: '1'
}]
}],
ext: {
data: {
registered: true,
interests: ['cars']
}
}
}
}
}
};

let callback;

beforeEach(function () {
callback = sinon.spy();
window.gamera = undefined;
});

it('should queue command when gamera.getPrebidSegments is not available', function () {
subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(window.gamera).to.exist;
expect(window.gamera.cmd).to.be.an('array');
expect(window.gamera.cmd.length).to.equal(1);
expect(callback.called).to.be.false;

// our callback should be executed if command queue is flushed
window.gamera.cmd.forEach(command => command());
expect(callback.calledOnce).to.be.true;
});

it('should call enrichAuction directly when gamera.getPrebidSegments is available', function () {
window.gamera = {
getPrebidSegments: () => ({})
};

subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(callback.calledOnce).to.be.true;
});

it('should handle errors gracefully', function () {
window.gamera = {
getPrebidSegments: () => {
throw new Error('Test error');
}
};

subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(logErrorSpy.calledWith('gameraRtdProvider', 'Error getting segments:')).to.be.true;
expect(callback.calledOnce).to.be.true;
});

describe('segment enrichment', function () {
const mockSegments = {
user: {
data: [{
name: 'gamera.ai',
ext: {
segtax: 4,
},
segment: [{ id: 'user-1' }]
}]
},
site: {
keywords: 'gamera,article,keywords',
content: {
data: [{
name: 'gamera.ai',
ext: {
segtax: 7,
},
segment: [{ id: 'site-1' }]
}]
}
},
adUnits: {
'test-div': {
key: 'value',
ext: {
data: {
gameraSegment: 'ad-1',
}
}
}
}
};

beforeEach(function () {
window.gamera = {
getPrebidSegments: () => mockSegments
};
});

it('should enrich ortb2Fragments with user data', function () {
subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(reqBidsConfigObj.ortb2Fragments.global.user.data).to.deep.include(mockSegments.user.data[0]);

// check if existing attributes are not overwritten
expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].ext.segtax).to.equal(4);
expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment[0].id).to.equal('1');
expect(reqBidsConfigObj.ortb2Fragments.global.user.keywords).to.equal('a,b');
expect(reqBidsConfigObj.ortb2Fragments.global.user.ext.data.registered).to.equal(true);
});

it('should enrich ortb2Fragments with site data', function () {
subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data).to.deep.include(mockSegments.site.content.data[0]);
expect(reqBidsConfigObj.ortb2Fragments.global.site.keywords).to.equal('gamera,article,keywords');

// check if existing attributes are not overwritten
expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].ext.segtax).to.equal(7);
expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].segment[0].id).to.equal('687');
expect(reqBidsConfigObj.ortb2Fragments.global.site.ext.data.category).to.equal('repair');
expect(reqBidsConfigObj.ortb2Fragments.global.site.content.userrating).to.equal('4');
});

it('should enrich adUnits with segment data', function () {
subModuleObj.getBidRequestData(reqBidsConfigObj, callback);

expect(reqBidsConfigObj.adUnits[0].ortb2Imp.key).to.equal('value');
expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.gameraSegment).to.equal('ad-1');

// check if existing attributes are not overwritten
expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.adUnitSpecificAttribute).to.equal('123');
expect(reqBidsConfigObj.adUnits[0].ortb2Imp.ext.data.pbadslot).to.equal('homepage-top-rect');
});
});
});
});

0 comments on commit 05a1065

Please sign in to comment.