diff --git a/modules/overtoneRtdProvider.js b/modules/overtoneRtdProvider.js new file mode 100644 index 00000000000..5da348b16fe --- /dev/null +++ b/modules/overtoneRtdProvider.js @@ -0,0 +1,79 @@ +import { submodule } from '../src/hook.js'; +import { ajaxBuilder } from '../src/ajax.js'; +import { safeJSONParse, logMessage as _logMessage } from '../src/utils.js'; + +export const OVERTONE_URL = 'https://prebid-1.overtone.ai/contextual'; + +const logMessage = (...args) => { + _logMessage('Overtone', ...args); +}; + +export async function fetchContextData(url = window.location.href) { + const pageUrl = encodeURIComponent(url); + const requestUrl = `${OVERTONE_URL}?URL=${pageUrl}&InApp=False`; + const request = window.ajaxBuilder || ajaxBuilder(); + + return new Promise((resolve, reject) => { + logMessage('Sending request to:', requestUrl); + request(requestUrl, { + success: (response) => { + const data = safeJSONParse(response); + logMessage('Fetched data:', data); + + if (!data || typeof data.status !== 'number') { + reject(new Error('Invalid response format')); + return; + } + + switch (data.status) { + case 1: // Success + resolve({ categories: data.categories || [] }); + break; + case 3: // Fail + case 4: // Ignore + resolve({ categories: [] }); + break; + default: + reject(new Error(`Unexpected response status: ${data.status}`)); + } + }, + error: (err) => { + logMessage('Error during request:', err); + reject(err); + }, + }); + }); +} + +function init(config) { + logMessage('init', config); + return true; +} + +export const overtoneRtdProvider = { + name: 'overtone', + init: init, + getBidRequestData: function (bidReqConfig, callback) { + fetchContextData() + .then((contextData) => { + if (contextData) { + logMessage('Fetched context data', contextData); + bidReqConfig.ortb2Fragments.global.site.ext = { + ...bidReqConfig.ortb2Fragments.global.site.ext, + data: contextData, + }; + } + callback(); + }) + .catch((error) => { + logMessage('Error fetching context data', error); + callback(); + }); + }, +}; + +submodule('realTimeData', overtoneRtdProvider); + +export const overtoneModule = { + fetchContextData, +}; diff --git a/modules/overtoneRtdProvider.md b/modules/overtoneRtdProvider.md new file mode 100644 index 00000000000..aa2b3b0164a --- /dev/null +++ b/modules/overtoneRtdProvider.md @@ -0,0 +1,97 @@ +# Overtone Rtd Provider + +## Overview + +Module Name: Overtone Rtd Provider + +Module Type: Rtd Provider + +Maintainer: tech@overtone.ai + +The Overtone Real-Time Data (RTD) Module is a plug-and-play Prebid.js adapter designed to provide contextual classification results on the publisher’s page through Overtone’s contextual API. + + +## Downloading and Configuring the Overtone RTD Module + +Navigate to https://docs.prebid.org/download.html and select the box labeled Overtone Prebid Contextual Evaluation. If Prebid.js is already installed on your site, ensure other necessary modules and adapters are selected. Upon clicking the "Get Prebid.js" button, a customized Prebid.js version will be built with your selections. + +Direct link to the Overtone module in the Prebid.js repository: + +The client must provide Overtone with all the addresses using the Prebid module to whitelist those domains. Failure to whitelist addresses will result in an invalid request to the Overtone Contextual API. + + +## Functionality + +At a high level, the Overtone RTD Module makes requests to the Overtone Contextual API during page load. It fetches and categorizes content for each page, which is then available for targeting in Prebid.js. Contextual data includes content classifications, which help advertisers make informed decisions about ad placements. + + +## Available Classifications + +Content Categories: + +Key: categories + +Possible Values: Various identifiers such as ovtn_004, ovtn_104, etc. + +Description: Content Categories represent Overtone’s classification of page content based on its contextual analysis. + +Please contact tech@overtone.ai for more information about our exact categories in brand safety, type, and tone. + + +## Configuration Highlight + +The configuration for the Overtone RTD module in Prebid.js might resemble the following: + +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'overtone', + params: { + + } + }] + } +}); + + +## API Response Handling + +The Overtone RTD module processes responses from the Overtone Contextual API. A typical response might include the following: + +Status: Indicates the API request status (1 for success, 3 for fail, 4 for ignore). + +Categories: An array of classification identifiers. + +For example: + +{ + "categories": ["ovtn_004", "ovtn_104", "ovtn_309", "ovtn_202"], + "status": 1 +} + +The module ensures that these values are integrated into Prebid.js’s targeting configuration for the current page. + + +## Testing and Validation + +The functionality of the Overtone RTD module can be validated using the associated test suite provided in overtoneRtdProvider_spec.mjs. The test suite simulates different API response scenarios to verify module behavior under varied conditions. + +Example Test Cases: + +Successful Data Retrieval: + +Input: URL with valid classification data. + +Expected Output: Categories array populated with identifiers. + +Failed Request: + +Input: URL resulting in a failure. + +Expected Output: Empty categories array. + +Ignored URL: + +Input: URL to be ignored by the API. + +Expected Output: Empty categories array. diff --git a/test/spec/modules/overtoneRtdProvider_spec.mjs b/test/spec/modules/overtoneRtdProvider_spec.mjs new file mode 100644 index 00000000000..2a45efb1811 --- /dev/null +++ b/test/spec/modules/overtoneRtdProvider_spec.mjs @@ -0,0 +1,60 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; +import { overtoneModule } from '../../../modules/overtoneRtdProvider.js'; +import { logMessage } from '../../../src/utils.js'; + +const TEST_URLS = { + success: 'https://www.theguardian.com/film/2024/nov/15/duncan-cowles-silent-men-interview', + fail: 'https://www.nytimes.com', + ignore: 'https://wsj.com', +}; + +describe('Overtone RTD Submodule with Test URLs', function () { + this.timeout(120000); + + let fetchContextDataStub; + + beforeEach(function () { + fetchContextDataStub = sinon.stub(overtoneModule, 'fetchContextData').callsFake(async (url) => { + if (url === TEST_URLS.success) { + return { categories: ['ovtn_004', 'ovtn_104', 'ovtn_309', 'ovtn_202'], status: 1 }; + } + if (url === TEST_URLS.fail) { + return { categories: [], status: 3 }; + } + if (url === TEST_URLS.ignore) { + return { categories: [], status: 4 }; + } + throw new Error('Unexpected URL in test'); + }); + }); + + afterEach(function () { + fetchContextDataStub.restore(); + }); + + it('should fetch and return categories for the success URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.success); + logMessage(data); + expect(data).to.deep.equal({ + categories: ['ovtn_004', 'ovtn_104', 'ovtn_309', 'ovtn_202'], + status: 1, + }); + }); + + it('should return the expected structure for the fail URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.fail); + expect(data).to.deep.equal({ + categories: [], + status: 3, + }); + }); + + it('should return the expected structure for the ignore URL', async function () { + const data = await overtoneModule.fetchContextData(TEST_URLS.ignore); + expect(data).to.deep.equal({ + categories: [], + status: 4, + }); + }); +});