From 4d5a43bc7dc746b1ab908aea1202b989785ce29f Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 13 Sep 2021 13:52:28 -0400 Subject: [PATCH 1/6] adds example code --- modules/jwplayerRtdProvider.js | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 8332e720ae7..a8a5c5cd361 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -15,6 +15,7 @@ import { ajaxBuilder } from '../src/ajax.js'; import { logError } from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; import { getGlobal } from '../src/prebidGlobal.js'; +import * as utils from '../src/utils' const SUBMODULE_NAME = 'jwplayer'; const segCache = {}; @@ -266,6 +267,47 @@ export function addTargetingToBid(bid, targeting) { bid.rtd = Object.assign({}, rtd, jwRtd); } +/* +{ + ..., + "user": { + "data": [ + { + "name": "a-data-provider.com", + "ext": { + "segtax": 3 + }, + "segment": [ + { "id": "1001" }, + { "id": "1002" } + ] + } + } + } +} + */ + +export function setGlobalOrtb2(segments, categories) { + try { + let oRtb = {}; + let testGlobal = getGlobal().getConfig('ortb2') || {}; + if (!utils.deepAccess(testGlobal, 'site.content.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.user.ext.data.sd_rtd, segments)) { + utils.deepSetValue(oRtb, 'user.ext.data.sd_rtd', segments || {}); + } + if (!utils.deepAccess(testGlobal, 'site.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.site.ext.data.sd_rtd, categories)) { + utils.deepSetValue(oRtb, 'site.ext.data.sd_rtd', categories || {}); + } + if (!utils.isEmpty(oRtb)) { + let ortb2 = {ortb2: utils.mergeDeep({}, testGlobal, oRtb)}; + getGlobal().setConfig(ortb2); + } + } catch (e) { + utils.logError(e) + } + + return true; +} + function getPlayer(playerID) { const jwplayer = window.jwplayer; if (!jwplayer) { From 1ae8135130d9d86bdb8178424682edd1754c9654 Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 13 Dec 2021 18:59:08 -0300 Subject: [PATCH 2/6] enriches the ortb2 object --- modules/jwplayerRtdProvider.js | 115 ++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 45 deletions(-) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index a8a5c5cd361..5d8a0071187 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -156,8 +156,10 @@ export function enrichAdUnits(adUnits) { if (!vat) { return; } + const contentId = getContentId(vat.mediaID); + const contentData = getContentData(vat.segments); const targeting = formatTargetingResponse(vat); - addTargetingToBids(adUnit.bids, targeting); + enrichBids(adUnit.bids, targeting, contentId, contentData); }; loadVat(jwTargeting, onVatResponse); }); @@ -236,6 +238,9 @@ export function getVatFromPlayer(playerID, mediaID) { }; } +/* + deprecated + */ export function formatTargetingResponse(vat) { const { segments, mediaID } = vat; const targeting = {}; @@ -244,68 +249,88 @@ export function formatTargetingResponse(vat) { } if (mediaID) { - const id = 'jw_' + mediaID; targeting.content = { - id + id: getContentId(mediaID) } } return targeting; } -function addTargetingToBids(bids, targeting) { - if (!bids || !targeting) { +function getContentId(mediaId) { + if (!mediaId) { return; } - bids.forEach(bid => addTargetingToBid(bid, targeting)); + return 'jw_' + mediaID; } -export function addTargetingToBid(bid, targeting) { - const rtd = bid.rtd || {}; - const jwRtd = {}; - jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); - bid.rtd = Object.assign({}, rtd, jwRtd); +function getContentData(segments) { + if (!segments) { + return; + } + + const formattedSegments = segments.reduce((convertedSegments, rawSegment) => { + convertedSegments.push({ + id: rawSegment, + value: rawSegment + }); + return convertedSegments; + }, []); + + return { + name: "jwplayer", + ext: { + segtax: 502 + }, + segment: formattedSegments + }; } -/* -{ - ..., - "user": { - "data": [ - { - "name": "a-data-provider.com", - "ext": { - "segtax": 3 - }, - "segment": [ - { "id": "1001" }, - { "id": "1002" } - ] - } - } +function addOrtbSiteContent(bid, contentId, contentData) { + if (!contentId && !contentData) { + return; + } + + let ortb2 = bid.ortb2 || {}; + let site = ortb2.site || {}; + let content = site.content || {}; + + if (contentId) { + content.id = content.id || contentId; + } + + if (contentData) { + const data = content.data || []; + data.push(contentData); } + + bid.ortb2 = ortb2; } - */ -export function setGlobalOrtb2(segments, categories) { - try { - let oRtb = {}; - let testGlobal = getGlobal().getConfig('ortb2') || {}; - if (!utils.deepAccess(testGlobal, 'site.content.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.user.ext.data.sd_rtd, segments)) { - utils.deepSetValue(oRtb, 'user.ext.data.sd_rtd', segments || {}); - } - if (!utils.deepAccess(testGlobal, 'site.ext.data.sd_rtd') || !utils.deepEqual(testGlobal.site.ext.data.sd_rtd, categories)) { - utils.deepSetValue(oRtb, 'site.ext.data.sd_rtd', categories || {}); - } - if (!utils.isEmpty(oRtb)) { - let ortb2 = {ortb2: utils.mergeDeep({}, testGlobal, oRtb)}; - getGlobal().setConfig(ortb2); - } - } catch (e) { - utils.logError(e) +function enrichBids(bids, targeting, contentId, contentData) { + if (!bids) { + return; } - return true; + bids.forEach(bid => { + addTargetingToBid(bid, targeting); + addOrtbSiteContent(bid, contentId, contentData); + }); +} + +/* + deprecated + */ +export function addTargetingToBid(bid, targeting) { + if (!targeting) { + return; + } + + const rtd = bid.rtd || {}; + const jwRtd = {}; + jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); + bid.rtd = Object.assign({}, rtd, jwRtd); + } function getPlayer(playerID) { From 15970c35259f80444efcb6b3ca6e3decb7a0175e Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 13 Dec 2021 19:19:08 -0300 Subject: [PATCH 3/6] resolves build errors --- modules/jwplayerRtdProvider.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 5d8a0071187..3cd4cc0c419 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -15,7 +15,6 @@ import { ajaxBuilder } from '../src/ajax.js'; import { logError } from '../src/utils.js'; import find from 'core-js-pure/features/array/find.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import * as utils from '../src/utils' const SUBMODULE_NAME = 'jwplayer'; const segCache = {}; @@ -256,8 +255,8 @@ export function formatTargetingResponse(vat) { return targeting; } -function getContentId(mediaId) { - if (!mediaId) { +function getContentId(mediaID) { + if (!mediaID) { return; } @@ -278,7 +277,7 @@ function getContentData(segments) { }, []); return { - name: "jwplayer", + name: 'jwplayer', ext: { segtax: 502 }, @@ -313,8 +312,8 @@ function enrichBids(bids, targeting, contentId, contentData) { } bids.forEach(bid => { - addTargetingToBid(bid, targeting); - addOrtbSiteContent(bid, contentId, contentData); + addTargetingToBid(bid, targeting); + addOrtbSiteContent(bid, contentId, contentData); }); } @@ -330,7 +329,6 @@ export function addTargetingToBid(bid, targeting) { const jwRtd = {}; jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); bid.rtd = Object.assign({}, rtd, jwRtd); - } function getPlayer(playerID) { From d5d027944f932a3a7dbdd139dceed013fcaec2bb Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 20 Dec 2021 10:37:21 -0500 Subject: [PATCH 4/6] tests content id and data getters --- modules/jwplayerRtdProvider.js | 8 ++-- test/spec/modules/jwplayerRtdProvider_spec.js | 48 ++++++++++++++++++- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 3cd4cc0c419..0276e8bcfc5 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -255,7 +255,7 @@ export function formatTargetingResponse(vat) { return targeting; } -function getContentId(mediaID) { +export function getContentId(mediaID) { if (!mediaID) { return; } @@ -263,8 +263,8 @@ function getContentId(mediaID) { return 'jw_' + mediaID; } -function getContentData(segments) { - if (!segments) { +export function getContentData(segments) { + if (!segments || !segments.length) { return; } @@ -285,7 +285,7 @@ function getContentData(segments) { }; } -function addOrtbSiteContent(bid, contentId, contentData) { +export function addOrtbSiteContent(bid, contentId, contentData) { if (!contentId && !contentData) { return; } diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 458e45e8ae7..48d8c94a671 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,6 +1,6 @@ import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, - fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; + fetchTargetingInformation, jwplayerSubmodule, getContentId, getContentData } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; describe('jwplayerRtdProvider', function() { @@ -412,7 +412,7 @@ describe('jwplayerRtdProvider', function() { }); }); - describe(' Extract Publisher Params', function () { + describe('Extract Publisher Params', function () { const config = { mediaID: 'test' }; it('should exclude adUnits that do not support instream video and do not specify jwTargeting', function () { @@ -480,6 +480,50 @@ describe('jwplayerRtdProvider', function() { }) }); + describe('Get content id', function() { + it('prefixes jw_ to the media id', function () { + const mediaId = 'mediaId'; + const contentId = getContentId(mediaId); + expect(contentId).to.equal('jw_mediaId'); + }); + + it('returns undefined when media id is empty', function () { + let contentId = getContentId(); + expect(contentId).to.be.undefined; + contentId = getContentId(''); + expect(contentId).to.be.undefined; + contentId = getContentId(null); + expect(contentId).to.be.undefined; + }); + }); + + describe('Get Content Data', function () { + it('returns undefined when segments are empty', function () { + let data = getContentData(null); + expect(data).to.be.undefined; + data = getContentData(undefined); + expect(data).to.be.undefined; + data = getContentData([]); + expect(data).to.be.undefined; + }); + + it('returns proper format', function () { + const segment1 = 'segment1'; + const segment2 = 'segment2'; + const segment3 = 'segment3'; + const data = getContentData([segment1, segment2, segment3]); + expect(data).to.have.property('name', 'jwplayer'); + expect(data.ext).to.have.property('segtax', 502); + expect(data.segment[0]).to.deep.equal({ id: segment1, value: segment1 }); + expect(data.segment[1]).to.deep.equal({ id: segment2, value: segment2 }); + expect(data.segment[2]).to.deep.equal({ id: segment3, value: segment3 }); + }); + }); + + /* + addOrtbSiteContent + */ + describe('Add Targeting to Bid', function () { const targeting = {foo: 'bar'}; From 0a4b2174e293cf12b141081eda8e3ebc3a5dc214 Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 20 Dec 2021 14:19:26 -0500 Subject: [PATCH 5/6] respects data integrity --- modules/jwplayerRtdProvider.js | 8 +- test/spec/modules/jwplayerRtdProvider_spec.js | 155 +++++++++++++++++- 2 files changed, 156 insertions(+), 7 deletions(-) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 0276e8bcfc5..5a7e9f13686 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -291,15 +291,15 @@ export function addOrtbSiteContent(bid, contentId, contentData) { } let ortb2 = bid.ortb2 || {}; - let site = ortb2.site || {}; - let content = site.content || {}; + let site = ortb2.site = ortb2.site || {}; + let content = site.content = site.content || {}; if (contentId) { - content.id = content.id || contentId; + content.id = contentId; } if (contentData) { - const data = content.data || []; + const data = content.data = content.data || []; data.push(contentData); } diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 48d8c94a671..db17d18864b 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -2,6 +2,7 @@ import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, fetchTargetingInformation, jwplayerSubmodule, getContentId, getContentData } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; +import {addOrtbSiteContent} from '../../../modules/jwplayerRtdProvider'; describe('jwplayerRtdProvider', function() { const testIdForSuccess = 'test_id_for_success'; @@ -520,9 +521,157 @@ describe('jwplayerRtdProvider', function() { }); }); - /* - addOrtbSiteContent - */ + describe(' Add Ortb Site Content', function () { + it('should maintain object structure when id and data params are empty', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'randomId' + }, + random: { + random_sub: 'randomSub' + } + }, + app: { + content: { + id: 'appId' + } + } + } + }; + addOrtbSiteContent(bid); + expect(bid).to.have.nested.property('ortb2.site.content.id', 'randomId'); + expect(bid).to.have.nested.property('ortb2.site.random.random_sub', 'randomSub'); + expect(bid).to.have.nested.property('ortb2.app.content.id', 'appId'); + }); + + it('should create a structure compliant with the oRTB 2 spec', function() { + const bid = {}; + const expectedId = 'expectedId'; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, expectedId, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should respect existing structure when adding adding fields', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'oldId' + }, + random: { + random_sub: 'randomSub' + } + }, + app: { + content: { + id: 'appId' + } + } + } + }; + + const expectedId = 'expectedId'; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, expectedId, expectedData); + expect(bid).to.have.nested.property('ortb2.site.random.random_sub', 'randomSub'); + expect(bid).to.have.nested.property('ortb2.app.content.id', 'appId'); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should set content id', function () { + const bid = {}; + const expectedId = 'expectedId'; + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + + it('should override content id', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'oldId' + } + } + } + }; + + const expectedId = 'expectedId'; + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + + it('should keep previous content id when not set', function () { + const previousId = 'oldId'; + const bid = { + ortb2: { + site: { + content: { + id: previousId, + data: [{ datum: 'first_datum' }] + } + } + } + }; + + addOrtbSiteContent(bid, null, { datum: 'new_datum' }); + expect(bid).to.have.nested.property('ortb2.site.content.id', previousId); + }); + + it('should set content data', function () { + const bid = {}; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, null, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(1); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should append content data', function () { + const bid = { + ortb2: { + site: { + content: { + data: [{ datum: 'first_datum' }] + } + } + } + }; + + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, null, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(2); + expect(bid.ortb2.site.content.data.pop()).to.be.deep.equal(expectedData); + }); + + it('should keep previous data when not set', function () { + const expectedId = 'expectedId'; + const expectedData = { datum: 'first_datum' }; + const bid = { + ortb2: { + site: { + content: { + data: [expectedData] + } + } + } + }; + + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(1); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + }); describe('Add Targeting to Bid', function () { const targeting = {foo: 'bar'}; From a17aed21544540f7f728307df419839af96996ee Mon Sep 17 00:00:00 2001 From: Karim Mourra Date: Mon, 20 Dec 2021 15:03:12 -0500 Subject: [PATCH 6/6] updates documentation --- modules/jwplayerRtdProvider.md | 38 ++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 7fb1bb13d74..77f65909040 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -81,20 +81,30 @@ realTimeData = { # Usage for Bid Adapters: Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. -Each bid for which targeting information was found will conform to the following object structure: +Each bid for which targeting information was found will have a ortb2 param conforming to the [oRTB v2 object structure](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf). The `ortb2` object will contain our proprietaty targeting segments in a format compliant with the [IAB's segment taxonomy structure](https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/segtax.md). + +Example: ```javascript { adUnitCode: 'xyz', bidId: 'abc', ..., - rtd: { - jwplayer: { - targeting: { - segments: ['123', '456'], - content: { - id: 'jw_abc123' - } + ortb2: { + site: { + content: { + id: 'jw_abc123', + data: [{ + name: 'jwplayer', + ext: { + segtax: 502 + }, + segment: [{ + id: '123' + }, { + id: '456' + }] + }] } } } @@ -102,9 +112,15 @@ Each bid for which targeting information was found will conform to the following ``` where: -- `segments` is an array of jwpseg targeting segments, of type string. -- `content` is an object containing metadata for the media. It may contain the following information: - - `id` is a unique identifier for the specific media asset. +- `ortb2` is an object containing first party data + - `site` is an object containing page specific information + - `content` is an object containing metadata for the media. It may contain the following information: + - `id` is a unique identifier for the specific media asset + - `data` is an array containing segment taxonomy objects that have the following parameters: + - `name` is the `jwplayer` string indicating the provider name + - `ext.segtax` whose `502` value is the unique identifier for JW Player's proprietary taxonomy + - `segment` is an array containing the segment taxonomy values as an object where: + - `id` is the string representation of the data segment value. **Example:**