Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gamera Rtd Provider: Initial release #12424

Merged
merged 3 commits into from
Nov 8, 2024
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
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');
});
});
});
});