From b0a32017d0964fcf22bb76ed93ebdc955091606b Mon Sep 17 00:00:00 2001 From: Zach Bowman Date: Mon, 10 Nov 2025 07:14:05 -0500 Subject: [PATCH 01/20] ConnectID Adapter: fix storage type configuration not being respected (#14018) * fix: Update ConnectID Adapter to respect storage type configuration. * refactor: Improve ConnectID storage type handling with constants and documentation. --- modules/connectIdSystem.js | 49 ++++++-- test/spec/modules/connectIdSystem_spec.js | 134 +++++++++++++++++++++- 2 files changed, 168 insertions(+), 15 deletions(-) diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 01b7e9196..006cb06d0 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -9,7 +9,7 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; -import {getStorageManager} from '../src/storageManager.js'; +import {getStorageManager, STORAGE_TYPE_COOKIES, STORAGE_TYPE_LOCALSTORAGE} from '../src/storageManager.js'; import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; @@ -45,15 +45,23 @@ const O_AND_O_DOMAINS = [ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** + * Stores the ConnectID object in browser storage according to storage configuration * @function - * @param {Object} obj + * @param {Object} obj - The ID object to store + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' */ -function storeObject(obj) { +function storeObject(obj, storageConfig = {}) { const expires = Date.now() + STORAGE_DURATION; - if (storage.cookiesAreEnabled()) { + const storageType = storageConfig.type || ''; + + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + const useLocalStorage = !storageType || storageType.includes(STORAGE_TYPE_LOCALSTORAGE); + + if (useCookie && storage.cookiesAreEnabled()) { setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname()); } - if (storage.localStorageIsEnabled()) { + if (useLocalStorage && storage.localStorageIsEnabled()) { storage.setDataInLocalStorage(MODULE_NAME, JSON.stringify(obj)); } } @@ -110,8 +118,17 @@ function getIdFromLocalStorage() { return null; } -function syncLocalStorageToCookie() { - if (!storage.cookiesAreEnabled()) { +/** + * Syncs ID from localStorage to cookie if storage configuration allows + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + */ +function syncLocalStorageToCookie(storageConfig = {}) { + const storageType = storageConfig.type || ''; + const useCookie = !storageType || storageType.includes(STORAGE_TYPE_COOKIES); + + if (!useCookie || !storage.cookiesAreEnabled()) { return; } const value = getIdFromLocalStorage(); @@ -129,12 +146,19 @@ function isStale(storedIdData) { return false; } -function getStoredId() { +/** + * Retrieves stored ConnectID from cookie or localStorage + * @function + * @param {Object} [storageConfig={}] - Storage configuration + * @param {string} [storageConfig.type] - Storage type: 'cookie', 'html5', or 'cookie&html5' + * @returns {Object|null} The stored ID object or null if not found + */ +function getStoredId(storageConfig = {}) { let storedId = getIdFromCookie(); if (!storedId) { storedId = getIdFromLocalStorage(); if (storedId && !isStale(storedId)) { - syncLocalStorageToCookie(); + syncLocalStorageToCookie(storageConfig); } } return storedId; @@ -191,13 +215,14 @@ export const connectIdSubmodule = { return; } const params = config.params || {}; + const storageConfig = config.storage || {}; if (!params || (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { logError(`${MODULE_NAME} module: configuration requires the 'pixelId'.`); return; } - const storedId = getStoredId(); + const storedId = getStoredId(storageConfig); let shouldResync = isStale(storedId); @@ -213,7 +238,7 @@ export const connectIdSubmodule = { } if (!shouldResync) { storedId.lastUsed = Date.now(); - storeObject(storedId); + storeObject(storedId, storageConfig); return {id: storedId}; } } @@ -274,7 +299,7 @@ export const connectIdSubmodule = { } responseObj.ttl = validTTLMiliseconds; } - storeObject(responseObj); + storeObject(responseObj, storageConfig); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); } diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index b65096068..48ef3a30f 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -77,10 +77,14 @@ describe('Yahoo ConnectID Submodule', () => { removeLocalStorageDataStub.restore(); }); - function invokeGetIdAPI(configParams, consentData) { - const result = connectIdSubmodule.getId({ + function invokeGetIdAPI(configParams, consentData, storageConfig) { + const config = { params: configParams - }, consentData); + }; + if (storageConfig) { + config.storage = storageConfig; + } + const result = connectIdSubmodule.getId(config, consentData); if (typeof result === 'object' && result.callback) { result.callback(sinon.stub()); } @@ -803,6 +807,130 @@ describe('Yahoo ConnectID Submodule', () => { expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); }); + + it('stores the result in localStorage only when storage type is html5', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'html5only'}; + const expectedStoredData = { + connectid: 'html5only', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.called).to.be.false; + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); + }); + + it('stores the result in cookie only when storage type is cookie', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'cookieonly'}; + const expectedStoredData = { + connectid: 'cookieonly', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + const expiryDelta = new Date(60 * 60 * 24 * 365 * 1000); + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'cookie'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.calledOnce).to.be.true; + expect(setCookieStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setCookieStub.firstCall.args[1]).to.equal(JSON.stringify(expectedStoredData)); + expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); + expect(setLocalStorageStub.called).to.be.false; + }); + + it('does not sync localStorage to cookie when storage type is html5', () => { + const localStorageData = {connectId: 'foobarbaz'}; + getLocalStorageStub.withArgs(STORAGE_KEY).returns(localStorageData); + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + + expect(setCookieStub.called).to.be.false; + }); + + it('updates existing ID with html5 storage type without writing cookie', () => { + const last13Days = Date.now() - (60 * 60 * 24 * 1000 * 13); + const cookieData = {connectId: 'foobar', he: HASHED_EMAIL, lastSynced: last13Days}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(20); + const newCookieData = Object.assign({}, cookieData, {lastUsed: 20}) + const result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData, {type: 'html5'}); + dateNowStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id'); + expect(setCookieStub.called).to.be.false; + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.equal(JSON.stringify(newCookieData)); + }); + + it('stores the result in both storages when storage type is cookie&html5', () => { + getAjaxFnStub.restore(); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'both'}; + const expectedStoredData = { + connectid: 'both', + puid: PUBLISHER_USER_ID, + lastSynced: 0, + lastUsed: 0 + }; + const expiryDelta = new Date(60 * 60 * 24 * 365 * 1000); + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData, {type: 'cookie&html5'}); + const request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + dateNowStub.restore(); + + expect(setCookieStub.calledOnce).to.be.true; + expect(setCookieStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setCookieStub.firstCall.args[1]).to.equal(JSON.stringify(expectedStoredData)); + expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(JSON.stringify(expectedStoredData)); + }); }); }); describe('userHasOptedOut()', () => { From 1440dfa92cbb93561e51421926a9e58ff9fe889c Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Wed, 12 Nov 2025 14:56:43 -0500 Subject: [PATCH 02/20] Vidazoo utils: fix screen resolution detection (#14122) * Fix Vidazoo utils screen resolution detection * remove duplication --------- Co-authored-by: Demetrio Girardi --- libraries/vidazooUtils/bidderUtils.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/libraries/vidazooUtils/bidderUtils.js b/libraries/vidazooUtils/bidderUtils.js index 084329368..fa2cea1b6 100644 --- a/libraries/vidazooUtils/bidderUtils.js +++ b/libraries/vidazooUtils/bidderUtils.js @@ -6,7 +6,8 @@ import { parseSizesInput, parseUrl, triggerPixel, - uniques + uniques, + getWinDimensions } from '../../src/utils.js'; import {chunk} from '../chunk/chunk.js'; import {CURRENCY, DEAL_ID_EXPIRY, SESSION_ID_KEY, TTL_SECONDS, UNIQUE_DEAL_ID_EXPIRY} from './constants.js'; @@ -280,7 +281,7 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder uniqueDealId: uniqueDealId, bidderVersion: bidderVersion, prebidVersion: '$prebid.version$', - res: `${screen.width}x${screen.height}`, + res: getScreenResolution(), schain: schain, mediaTypes: mediaTypes, isStorageAllowed: isStorageAllowed, @@ -374,6 +375,15 @@ export function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidder return data; } +function getScreenResolution() { + const dimensions = getWinDimensions(); + const width = dimensions?.screen?.width; + const height = dimensions?.screen?.height; + if (width != null && height != null) { + return `${width}x${height}` + } +} + export function createInterpretResponseFn(bidderCode, allowSingleRequest) { return function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { From 3d0f23a633e00a8acd9cba8a5057a352a322d4f2 Mon Sep 17 00:00:00 2001 From: UuqV Date: Thu, 13 Nov 2025 11:01:51 -0500 Subject: [PATCH 03/20] adds nvm path to setup script (#14109) --- .devcontainer/postCreate.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 7e14a2d20..257b49059 100644 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,5 +1,8 @@ echo "Post Create Starting" +export NVM_DIR="/usr/local/share/nvm" +[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + nvm install nvm use npm install gulp-cli -g From 5f3f951530efbe3e69e38e3748994a3aa74ae83c Mon Sep 17 00:00:00 2001 From: Keith Candiotti Date: Thu, 13 Nov 2025 11:40:15 -0500 Subject: [PATCH 04/20] optimeraRTD: updated scorefile fetching logic (#14101) --- modules/optimeraRtdProvider.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index 3a46184e9..153fbea29 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -56,9 +56,6 @@ export let transmitWithBidRequests = 'allow'; /** @type {Object} */ export let optimeraTargeting = {}; -/** @type {boolean} */ -export let fetchScoreFile = true; - /** @type {RtdSubmodule} */ export const optimeraSubmodule = { name: 'optimeraRTD', @@ -84,7 +81,6 @@ export function init(moduleConfig) { if (_moduleParams.transmitWithBidRequests) { transmitWithBidRequests = _moduleParams.transmitWithBidRequests; } - setScoresURL(); return true; } logError('Optimera clientID is missing in the Optimera RTD configuration.'); @@ -111,9 +107,9 @@ export function setScoresURL() { if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; - fetchScoreFile = true; + return true; } else { - fetchScoreFile = false; + return false; } } @@ -125,6 +121,12 @@ export function setScoresURL() { * @param {object} userConsent */ export function fetchScores(reqBidsConfigObj, callback, config, userConsent) { + // If setScoresURL returns false, no need to re-fetch the score file + if (!setScoresURL()) { + callback(); + return; + } + // Else, fetch the score file const ajax = ajaxBuilder(); ajax(scoresURL, { success: (res, req) => { From 8dd9b8699ec32f0c53dd5dbcfdd520f9fb697af8 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 13 Nov 2025 13:23:44 -0500 Subject: [PATCH 05/20] Core: fix spurious validation warnings on mediaType / ortb2Imp (#14099) --- src/prebid.ts | 2 +- test/spec/banner_spec.js | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/prebid.ts b/src/prebid.ts index 05a070c12..611b90d8d 100644 --- a/src/prebid.ts +++ b/src/prebid.ts @@ -161,7 +161,7 @@ export function syncOrtb2(adUnit, mediaType) { deepSetValue(adUnit, `mediaTypes.${mediaType}.${key}`, ortbFieldValue); } else if (ortbFieldValue === undefined) { deepSetValue(adUnit, `ortb2Imp.${mediaType}.${key}`, mediaTypesFieldValue); - } else { + } else if (!deepEqual(mediaTypesFieldValue, ortbFieldValue)) { logWarn(`adUnit ${adUnit.code}: specifies conflicting ortb2Imp.${mediaType}.${key} and mediaTypes.${mediaType}.${key}, the latter will be ignored`, adUnit); deepSetValue(adUnit, `mediaTypes.${mediaType}.${key}`, ortbFieldValue); } diff --git a/test/spec/banner_spec.js b/test/spec/banner_spec.js index fcf56ad6e..f60f20231 100644 --- a/test/spec/banner_spec.js +++ b/test/spec/banner_spec.js @@ -127,6 +127,23 @@ describe('banner', () => { assert.ok(logWarnSpy.calledOnce, 'expected warning was logged due to conflicting btype'); }); + it('should not warn if fields match', () => { + const adUnit = { + mediaTypes: { + banner: { + format: [{wratio: 1, hratio: 1}] + } + }, + ortb2Imp: { + banner: { + format: [{wratio: 1, hratio: 1}] + } + } + } + syncOrtb2(adUnit, 'banner'); + sinon.assert.notCalled(logWarnSpy); + }) + it('should omit sync if mediaType not present on adUnit', () => { const adUnit = { mediaTypes: { From 59fbe3ad82714ac666a1cf38808011484583bc76 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Mon, 17 Nov 2025 11:55:58 -0500 Subject: [PATCH 06/20] Permutive modules: define gvl id (#14131) * Permutive modules: centralize gvl id * Set gvlid to null in permutiveIdentityManagerIdSystem * Set gvlid to null in permutiveRtdProvider.json * Set gvlid to null for Permutive components * Fix JSON formatting by adding missing newline * Fix missing newline in permutiveIdentityManagerIdSystem.json Add missing newline at the end of the JSON file. * Update permutiveRtdProvider.json --- metadata/modules.json | 2 +- metadata/modules/permutiveIdentityManagerIdSystem.json | 2 +- metadata/modules/permutiveRtdProvider.json | 2 +- modules/permutiveIdentityManagerIdSystem.js | 2 ++ modules/permutiveRtdProvider.js | 2 ++ 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/metadata/modules.json b/metadata/modules.json index ebd317026..4aa546de0 100644 --- a/metadata/modules.json +++ b/metadata/modules.json @@ -5980,4 +5980,4 @@ "gvlid": null } ] -} \ No newline at end of file +} diff --git a/metadata/modules/permutiveIdentityManagerIdSystem.json b/metadata/modules/permutiveIdentityManagerIdSystem.json index e8fc0cd1f..ce7b56fec 100644 --- a/metadata/modules/permutiveIdentityManagerIdSystem.json +++ b/metadata/modules/permutiveIdentityManagerIdSystem.json @@ -10,4 +10,4 @@ "aliasOf": null } ] -} \ No newline at end of file +} diff --git a/metadata/modules/permutiveRtdProvider.json b/metadata/modules/permutiveRtdProvider.json index 0e675450f..691d87b6a 100644 --- a/metadata/modules/permutiveRtdProvider.json +++ b/metadata/modules/permutiveRtdProvider.json @@ -9,4 +9,4 @@ "disclosureURL": null } ] -} \ No newline at end of file +} diff --git a/modules/permutiveIdentityManagerIdSystem.js b/modules/permutiveIdentityManagerIdSystem.js index 5dc12d44e..f38496444 100644 --- a/modules/permutiveIdentityManagerIdSystem.js +++ b/modules/permutiveIdentityManagerIdSystem.js @@ -10,6 +10,7 @@ import {prefixLog, safeJSONParse} from '../src/utils.js' */ const MODULE_NAME = 'permutiveIdentityManagerId' +const PERMUTIVE_GVLID = 361 const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id' const ID5_DOMAIN = 'id5-sync.com' @@ -80,6 +81,7 @@ export const permutiveIdentityManagerIdSubmodule = { * @type {string} */ name: MODULE_NAME, + gvlid: PERMUTIVE_GVLID, /** * decode the stored id value for passing to bid requests diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index bb06d2d13..886dc8b3b 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -17,6 +17,7 @@ import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; */ const MODULE_NAME = 'permutive' +const PERMUTIVE_GVLID = 361 const logger = prefixLog('[PermutiveRTD]') @@ -466,6 +467,7 @@ let permutiveSDKInRealTime = false /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, + gvlid: PERMUTIVE_GVLID, getBidRequestData: function (reqBidsConfigObj, callback, customModuleConfig) { const completeBidRequestData = () => { logger.logInfo(`Request data updated`) From 7720f89a37c0b3513a9bf0f262b742797395d821 Mon Sep 17 00:00:00 2001 From: gregneuwo Date: Tue, 18 Nov 2025 22:57:51 +0100 Subject: [PATCH 07/20] Neuwo Rtd Module: Add url cleaning feature to Neuwo RTD module (#14089) * feat: add url cleaning functionality Add configurable URL cleaning options to strip query parameters and fragments before sending URLs to Neuwo API: - stripAllQueryParams: removes all query parameters - stripQueryParamsForDomains: removes all params for specific domains/subdomains - stripQueryParams: removes specific named parameters - stripFragments: removes URL hash fragments * test: add tests for url cleaning functionality Add test cases for cleanUrl function covering: - Query parameter stripping (all, domain-specific, selective) - Fragment stripping and combinations - Edge cases (malformed URLs, encoding, delimiters) - Domain/subdomain matching logic - Option priority and fallthrough behavior - Integration tests with getBidRequestData * docs: update documentation for url cleaning Update module documentation to include: - URL cleaning configuration options and examples - Parameter table with stripAllQueryParams, stripQueryParamsForDomains, stripQueryParams, stripFragments - `npm ci` as dependencies installation command - Linting section with eslint command - Adjust commands examples - Update test commands to use test-only and test-only-nobuild - Update example page to include URL cleaning functionality - Unified commands between the example file and the module readme - Improve input structure on the example page - Enable saving input values on the example page to facilitate manual testing - Update contact information * Neuwo RTD Module feat: add API response caching - Added `enableCache` parameter (default: `true`) to cache API responses and avoid redundant requests during the page session - Implemented `clearCache()` function for testing purposes - Updated *modules/neuwoRtdProvider.js* to store responses in `globalCachedResponse` and reuse them when caching is enabled - Added cache control UI to *integrationExamples/gpt/neuwoRtdProvider_example.html* with checkbox and state persistence - Updated *modules/neuwoRtdProvider.md* documentation with caching configuration details - Added test coverage in *test/spec/modules/neuwoRtdProvider_spec.js* for both enabled and disabled cache scenarios --------- Co-authored-by: grzgm <125459798+grzgm@users.noreply.github.com> --- .../gpt/neuwoRtdProvider_example.html | 161 +++++++++++++++++- modules/neuwoRtdProvider.js | 153 ++++++++++++++++- modules/neuwoRtdProvider.md | 117 ++++++++++--- 3 files changed, 395 insertions(+), 36 deletions(-) diff --git a/integrationExamples/gpt/neuwoRtdProvider_example.html b/integrationExamples/gpt/neuwoRtdProvider_example.html index 33cdccd0e..3d6fef989 100644 --- a/integrationExamples/gpt/neuwoRtdProvider_example.html +++ b/integrationExamples/gpt/neuwoRtdProvider_example.html @@ -121,10 +121,30 @@ const inputIabContentTaxonomyVersion = document.getElementById('iab-content-taxonomy-version'); const iabContentTaxonomyVersion = inputIabContentTaxonomyVersion ? inputIabContentTaxonomyVersion.value : undefined; + // Cache Option + const inputEnableCache = document.getElementById('enable-cache'); + const enableCache = inputEnableCache ? inputEnableCache.checked : undefined; + + // URL Stripping Options + const inputStripAllQueryParams = document.getElementById('strip-all-query-params'); + const stripAllQueryParams = inputStripAllQueryParams ? inputStripAllQueryParams.checked : undefined; + + const inputStripQueryParamsForDomains = document.getElementById('strip-query-params-for-domains'); + const stripQueryParamsForDomainsValue = inputStripQueryParamsForDomains ? inputStripQueryParamsForDomains.value.trim() : ''; + const stripQueryParamsForDomains = stripQueryParamsForDomainsValue ? stripQueryParamsForDomainsValue.split(',').map(d => d.trim()).filter(d => d) : undefined; + + const inputStripQueryParams = document.getElementById('strip-query-params'); + const stripQueryParamsValue = inputStripQueryParams ? inputStripQueryParams.value.trim() : ''; + const stripQueryParams = stripQueryParamsValue ? stripQueryParamsValue.split(',').map(p => p.trim()).filter(p => p) : undefined; + + const inputStripFragments = document.getElementById('strip-fragments'); + const stripFragments = inputStripFragments ? inputStripFragments.checked : undefined; + pbjs.que.push(function () { pbjs.setConfig({ debug: true, realTimeData: { + auctionDelay: 500, dataProviders: [ { name: "NeuwoRTDModule", @@ -133,7 +153,12 @@ neuwoApiUrl, neuwoApiToken, websiteToAnalyseUrl, - iabContentTaxonomyVersion + iabContentTaxonomyVersion, + enableCache, + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments } } ] @@ -166,11 +191,14 @@

Basic Prebid.js Example using Neuwo Rtd Provider

after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js + // Install dependencies npm ci + + // Run a local development server npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter // No tests - npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --notests + npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter // Only tests npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/neuwoRtdProvider_spec.js @@ -180,10 +208,49 @@

Basic Prebid.js Example using Neuwo Rtd Provider

Neuwo Rtd Provider Configuration

Add token and url to use for Neuwo extension configuration

- - - - +
+ +
+
+ +
+
+ +
+ +

IAB Content Taxonomy Options

+
+ +
+ +

Cache Options

+
+ +
+ +

URL Cleaning Options

+
+ +
+
+ +
+
+ +
+
+ +
+
@@ -232,4 +299,86 @@

Neuwo Data in Bid Request

if (helper) helper.style.display = location.href !== 'http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html' ? 'block' : 'none'; + + \ No newline at end of file diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 99715f0b4..3255547aa 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -23,6 +23,17 @@ import { deepSetValue, logError, logInfo, mergeDeep } from "../src/utils.js"; const MODULE_NAME = "NeuwoRTDModule"; export const DATA_PROVIDER = "www.neuwo.ai"; +// Cached API response to avoid redundant requests. +let globalCachedResponse; + +/** + * Clears the cached API response. Primarily used for testing. + * @private + */ +export function clearCache() { + globalCachedResponse = undefined; +} + // Maps the IAB Content Taxonomy version string to the corresponding segtax ID. // Based on https://github.com/InteractiveAdvertisingBureau/AdCOM/blob/main/AdCOM%20v1.0%20FINAL.md#list--category-taxonomies- const IAB_CONTENT_TAXONOMY_MAP = { @@ -36,11 +47,13 @@ const IAB_CONTENT_TAXONOMY_MAP = { /** * Validates the configuration and initialises the module. + * * @param {Object} config The module configuration. * @param {Object} userConsent The user consent object. * @returns {boolean} `true` if the module is configured correctly, otherwise `false`. */ function init(config, userConsent) { + logInfo(MODULE_NAME, "init:", config, userConsent); const params = config?.params || {}; if (!params.neuwoApiUrl) { logError(MODULE_NAME, "init:", "Missing Neuwo Edge API Endpoint URL"); @@ -55,18 +68,46 @@ function init(config, userConsent) { /** * Fetches contextual data from the Neuwo API and enriches the bid request object with IAB categories. + * Uses cached response if available to avoid redundant API calls. + * * @param {Object} reqBidsConfigObj The bid request configuration object. * @param {function} callback The callback function to continue the auction. * @param {Object} config The module configuration. + * @param {Object} config.params Configuration parameters. + * @param {string} config.params.neuwoApiUrl The Neuwo API endpoint URL. + * @param {string} config.params.neuwoApiToken The Neuwo API authentication token. + * @param {string} [config.params.websiteToAnalyseUrl] Optional URL to analyze instead of current page. + * @param {string} [config.params.iabContentTaxonomyVersion] IAB content taxonomy version (default: "3.0"). + * @param {boolean} [config.params.enableCache=true] If true, caches API responses to avoid redundant requests (default: true). + * @param {boolean} [config.params.stripAllQueryParams] If true, strips all query parameters from the URL. + * @param {string[]} [config.params.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [config.params.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [config.params.stripFragments] If true, strips URL fragments (hash). * @param {Object} userConsent The user consent object. */ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { logInfo(MODULE_NAME, "getBidRequestData:", "starting getBidRequestData", config); - const { websiteToAnalyseUrl, neuwoApiUrl, neuwoApiToken, iabContentTaxonomyVersion } = - config.params; + const { + websiteToAnalyseUrl, + neuwoApiUrl, + neuwoApiToken, + iabContentTaxonomyVersion, + enableCache = true, + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments, + } = config.params; - const pageUrl = encodeURIComponent(websiteToAnalyseUrl || getRefererInfo().page); + const rawUrl = websiteToAnalyseUrl || getRefererInfo().page; + const processedUrl = cleanUrl(rawUrl, { + stripAllQueryParams, + stripQueryParamsForDomains, + stripQueryParams, + stripFragments + }); + const pageUrl = encodeURIComponent(processedUrl); // Adjusted for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') const joiner = neuwoApiUrl.indexOf("?") < 0 ? "?" : "&"; const neuwoApiUrlFull = @@ -75,8 +116,13 @@ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsen const success = (response) => { logInfo(MODULE_NAME, "getBidRequestData:", "Neuwo API raw response:", response); try { - const responseJson = JSON.parse(response); - injectIabCategories(responseJson, reqBidsConfigObj, iabContentTaxonomyVersion); + const responseParsed = JSON.parse(response); + + if (enableCache) { + globalCachedResponse = responseParsed; + } + + injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion); } catch (ex) { logError(MODULE_NAME, "getBidRequestData:", "Error while processing Neuwo API response", ex); } @@ -88,15 +134,102 @@ export function getBidRequestData(reqBidsConfigObj, callback, config, userConsen callback(); }; - ajax(neuwoApiUrlFull, { success, error }, null); + if (enableCache && globalCachedResponse) { + logInfo(MODULE_NAME, "getBidRequestData:", "Using cached response:", globalCachedResponse); + injectIabCategories(globalCachedResponse, reqBidsConfigObj, iabContentTaxonomyVersion); + callback(); + } else { + logInfo(MODULE_NAME, "getBidRequestData:", "Calling Neuwo API Endpoint: ", neuwoApiUrlFull); + ajax(neuwoApiUrlFull, { success, error }, null); + } } // // HELPER FUNCTIONS // +/** + * Cleans a URL by stripping query parameters and/or fragments based on the provided configuration. + * + * @param {string} url The URL to clean. + * @param {Object} options Cleaning options. + * @param {boolean} [options.stripAllQueryParams] If true, strips all query parameters. + * @param {string[]} [options.stripQueryParamsForDomains] List of domains for which to strip all query params. + * @param {string[]} [options.stripQueryParams] List of specific query parameter names to strip. + * @param {boolean} [options.stripFragments] If true, strips URL fragments (hash). + * @returns {string} The cleaned URL. + */ +export function cleanUrl(url, options = {}) { + const { stripAllQueryParams, stripQueryParamsForDomains, stripQueryParams, stripFragments } = options; + + if (!url) { + logInfo(MODULE_NAME, "cleanUrl:", "Empty or null URL provided, returning as-is"); + return url; + } + + logInfo(MODULE_NAME, "cleanUrl:", "Input URL:", url, "Options:", options); + + try { + const urlObj = new URL(url); + + // Strip fragments if requested + if (stripFragments === true) { + urlObj.hash = ""; + logInfo(MODULE_NAME, "cleanUrl:", "Stripped fragment from URL"); + } + + // Option 1: Strip all query params unconditionally + if (stripAllQueryParams === true) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + + // Option 2: Strip all query params for specific domains + if (Array.isArray(stripQueryParamsForDomains) && stripQueryParamsForDomains.length > 0) { + const hostname = urlObj.hostname; + const shouldStripForDomain = stripQueryParamsForDomains.some(domain => { + // Support exact match or subdomain match + return hostname === domain || hostname.endsWith("." + domain); + }); + + if (shouldStripForDomain) { + urlObj.search = ""; + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + } + + // Option 3: Strip specific query parameters + // Caveats: + // - "?=value" is treated as query parameter with key "" and value "value" + // - "??" is treated as query parameter with key "?" and value "" + if (Array.isArray(stripQueryParams) && stripQueryParams.length > 0) { + const queryParams = urlObj.searchParams; + logInfo(MODULE_NAME, "cleanUrl:", `Query parameters to strip: ${stripQueryParams}`); + stripQueryParams.forEach(param => { + queryParams.delete(param); + }); + urlObj.search = queryParams.toString(); + const cleanedUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", cleanedUrl); + return cleanedUrl; + } + + const finalUrl = urlObj.toString(); + logInfo(MODULE_NAME, "cleanUrl:", "Output URL:", finalUrl); + return finalUrl; + } catch (e) { + logError(MODULE_NAME, "cleanUrl:", "Error cleaning URL:", e); + return url; + } +} + /** * Injects data into the OpenRTB 2.x global fragments of the bid request object. + * * @param {Object} reqBidsConfigObj The main bid request configuration object. * @param {string} path The dot-notation path where the data should be injected (e.g., 'site.content.data'). * @param {*} data The data to inject at the specified path. @@ -109,6 +242,7 @@ export function injectOrtbData(reqBidsConfigObj, path, data) { /** * Builds an IAB category data object for use in OpenRTB. + * * @param {Object} marketingCategories Marketing Categories returned by Neuwo API. * @param {string[]} tiers The tier keys to extract from marketingCategories. * @param {number} segtax The IAB taxonomy version Id. @@ -141,12 +275,13 @@ export function buildIabData(marketingCategories, tiers, segtax) { /** * Processes the Neuwo API response to build and inject IAB content and audience categories * into the bid request object. - * @param {Object} responseJson The parsed JSON response from the Neuwo API. + * + * @param {Object} responseParsed The parsed JSON response from the Neuwo API. * @param {Object} reqBidsConfigObj The bid request configuration object to be modified. * @param {string} iabContentTaxonomyVersion The version of the IAB content taxonomy to use for segtax mapping. */ -function injectIabCategories(responseJson, reqBidsConfigObj, iabContentTaxonomyVersion) { - const marketingCategories = responseJson.marketing_categories; +function injectIabCategories(responseParsed, reqBidsConfigObj, iabContentTaxonomyVersion) { + const marketingCategories = responseParsed.marketing_categories; if (!marketingCategories) { logError(MODULE_NAME, "injectIabCategories:", "No Marketing Categories in Neuwo API response."); diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index acd3f27d3..804130be1 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -63,41 +63,98 @@ ortb2: { } ``` -To get started, you can generate your API token at [https://neuwo.ai/generatetoken/](https://neuwo.ai/generatetoken/) or [contact us here](https://neuwo.ai/contact-us/). +To get started, you can generate your API token at [https://neuwo.ai/generatetoken/](https://neuwo.ai/generatetoken/), send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). ## Configuration -> **Important:** You must add the domain (origin) where Prebid.js is running to the list of allowed origins in Neuwo Edge API configuration. If you have problems, [contact us here](https://neuwo.ai/contact-us/). +> **Important:** You must add the domain (origin) where Prebid.js is running to the list of allowed origins in Neuwo Edge API configuration. If you have problems, send us an email to [neuwo-helpdesk@neuwo.ai](mailto:neuwo-helpdesk@neuwo.ai) or [contact us here](https://neuwo.ai/contact-us/). This module is configured as part of the `realTimeData.dataProviders` object. ```javascript pbjs.setConfig({ realTimeData: { - dataProviders: [{ - name: 'NeuwoRTDModule', - params: { - neuwoApiUrl: '', - neuwoApiToken: '', - iabContentTaxonomyVersion: '3.0', - } - }] - } + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + enableCache: true, // Default: true. Caches API responses to avoid redundant requests + }, + }, + ], + }, }); ``` **Parameters** -| Name | Type | Required | Default | Description | -| :--------------------------------- | :----- | :------- | :------ | :------------------------------------------------------------------------------------------------ | -| `name` | String | Yes | | The name of the module, which is `NeuwoRTDModule`. | -| `params` | Object | Yes | | Container for module-specific parameters. | -| `params.neuwoApiUrl` | String | Yes | | The endpoint URL for the Neuwo Edge API. | -| `params.neuwoApiToken` | String | Yes | | Your unique API token provided by Neuwo. | -| `params.iabContentTaxonomyVersion` | String | No | `'3.0'` | Specifies the version of the IAB Content Taxonomy to be used. Supported values: `'2.2'`, `'3.0'`. | +| Name | Type | Required | Default | Description | +| :---------------------------------- | :------- | :------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | String | Yes | | The name of the module, which is `NeuwoRTDModule`. | +| `params` | Object | Yes | | Container for module-specific parameters. | +| `params.neuwoApiUrl` | String | Yes | | The endpoint URL for the Neuwo Edge API. | +| `params.neuwoApiToken` | String | Yes | | Your unique API token provided by Neuwo. | +| `params.iabContentTaxonomyVersion` | String | No | `'3.0'` | Specifies the version of the IAB Content Taxonomy to be used. Supported values: `'2.2'`, `'3.0'`. | +| `params.enableCache` | Boolean | No | `true` | If `true`, caches API responses to avoid redundant requests for the same page during the session. Set to `false` to disable caching and make a fresh API call on every bid request. | +| `params.stripAllQueryParams` | Boolean | No | `false` | If `true`, strips all query parameters from the URL before analysis. Takes precedence over other stripping options. | +| `params.stripQueryParamsForDomains` | String[] | No | `[]` | List of domains for which to strip **all** query parameters. When a domain matches, all query params are removed for that domain and all its subdomains (e.g., `'example.com'` strips params for both `'example.com'` and `'sub.example.com'`). This option takes precedence over `stripQueryParams` for matching domains. | +| `params.stripQueryParams` | String[] | No | `[]` | List of specific query parameter names to strip from the URL (e.g., `['utm_source', 'fbclid']`). Other parameters are preserved. Only applies when the domain does not match `stripQueryParamsForDomains`. | +| `params.stripFragments` | Boolean | No | `false` | If `true`, strips URL fragments (hash, e.g., `#section`) from the URL before analysis. | + +### API Response Caching + +By default, the module caches API responses during the page session to optimise performance and reduce redundant API calls. This behaviour can be disabled by setting `enableCache: false` if needed for dynamic content scenarios. + +### URL Cleaning Options + +The module provides optional URL cleaning capabilities to strip query parameters and/or fragments from the analysed URL before sending it to the Neuwo API. This can be useful for privacy, caching, or analytics purposes. + +**Example with URL cleaning:** + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 500, // Value can be adjusted based on the needs + dataProviders: [ + { + name: "NeuwoRTDModule", + waitForIt: true, + params: { + neuwoApiUrl: "", + neuwoApiToken: "", + iabContentTaxonomyVersion: "3.0", + + // Option 1: Strip all query parameters from the URL + stripAllQueryParams: true, + + // Option 2: Strip all query parameters only for specific domains + // stripQueryParamsForDomains: ['example.com', 'another-domain.com'], + + // Option 3: Strip specific query parameters by name + // stripQueryParams: ['utm_source', 'utm_campaign', 'fbclid'], + + // Optional: Strip URL fragments (hash) + stripFragments: true, + }, + }, + ], + }, +}); +``` ## Local Development +Install the exact versions of packages specified in the lockfile: + +```bash +npm ci +``` + > **Linux** Linux might require exporting the following environment variable before running the commands below: > `export CHROME_BIN=/usr/bin/chromium` @@ -110,20 +167,38 @@ npx gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter For a faster build without tests: ```bash -npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --notests +npx gulp serve-fast --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter ``` After starting the server, you can access the example page at: [http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html](http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html) ### Add development tools if necessary + If you don't have gulp-cli installed globally, run the following command in your Prebid.js source folder: + ```bash npm i -g gulp-cli ``` +## Linting + +To lint the module: + +```bash +npx eslint 'modules/neuwoRtdProvider.js' --cache --cache-strategy content +``` + ## Testing + To run the module-specific tests: + +```bash +npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/euwoRtdProvider_spec.js +``` + +Skip building, if the project has already been built: + ```bash -npx gulp test-only --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter --file=test/spec/modules/neuwoRtdProvider_spec.js -``` \ No newline at end of file +npx gulp test-only-nobuild --file=test/spec/modules/neuwoRtdProvider_spec.js +``` From caa41e354693b1310e7845b8ac54337ee41e48c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 17:26:12 -0500 Subject: [PATCH 08/20] Bump min-document from 2.19.0 to 2.19.2 (#14162) Bumps [min-document](https://github.com/Raynos/min-document) from 2.19.0 to 2.19.2. - [Commits](https://github.com/Raynos/min-document/compare/v2.19.0...v2.19.2) --- updated-dependencies: - dependency-name: min-document dependency-version: 2.19.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1be28a259..90faafc14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15588,8 +15588,11 @@ } }, "node_modules/min-document": { - "version": "2.19.0", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", "dev": true, + "license": "MIT", "dependencies": { "dom-walk": "^0.1.0" } From 18fdfb9582898e8843798690b6b68d27ae4caea8 Mon Sep 17 00:00:00 2001 From: Patrick McCann Date: Wed, 19 Nov 2025 13:20:15 -0500 Subject: [PATCH 09/20] Core: fix schema-utils import (#14168) Co-authored-by: Demetrio Girardi --- customize/buildOptions.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/customize/buildOptions.mjs b/customize/buildOptions.mjs index 7b8013ab2..3341f35b0 100644 --- a/customize/buildOptions.mjs +++ b/customize/buildOptions.mjs @@ -1,8 +1,12 @@ import path from 'path' -import validate from 'schema-utils' +import { validate } from 'schema-utils' const boModule = path.resolve(import.meta.dirname, '../dist/src/buildOptions.mjs') +/** + * Resolve the absolute path of the default build options module. + * @returns {string} Absolute path to the generated build options module. + */ export function getBuildOptionsModule () { return boModule } @@ -25,6 +29,11 @@ const schema = { } } +/** + * Validate and load build options overrides. + * @param {object} [options] user supplied overrides + * @returns {Promise} Promise resolving to merged build options. + */ export function getBuildOptions (options = {}) { validate(schema, options, { name: 'Prebid build options', From 7cc4ba9c3c882a7aaf6b53dd28d2bdca045ee291 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:24:41 -0500 Subject: [PATCH 10/20] Bump tar-fs from 3.0.9 to 3.1.1 (#14163) Bumps [tar-fs](https://github.com/mafintosh/tar-fs) from 3.0.9 to 3.1.1. - [Commits](https://github.com/mafintosh/tar-fs/compare/v3.0.9...v3.1.1) --- updated-dependencies: - dependency-name: tar-fs dependency-version: 3.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Patrick McCann --- package-lock.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 90faafc14..08e603a18 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19439,7 +19439,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, "license": "MIT", "dependencies": { From 8488d0ca63450132c8e9cdcf24335bedcb086b8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:26:01 -0500 Subject: [PATCH 11/20] Bump js-yaml (#14164) Bumps and [js-yaml](https://github.com/nodeca/js-yaml). These dependencies needed to be updated together. Updates `js-yaml` from 3.14.1 to 3.14.2 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) Updates `js-yaml` from 4.1.0 to 4.1.1 - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 3.14.2 dependency-type: indirect - dependency-name: js-yaml dependency-version: 4.1.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08e603a18..6a87f1142 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2239,9 +2239,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -8014,10 +8014,11 @@ "dev": true }, "node_modules/cosmiconfig/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -14453,7 +14454,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "license": "MIT", "dependencies": { "argparse": "^1.0.7", @@ -15822,7 +15825,9 @@ } }, "node_modules/mocha/node_modules/js-yaml": { - "version": "4.1.0", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { From 1039fea40222203480c4eb7f60ffd3e17acf1e3c Mon Sep 17 00:00:00 2001 From: "null[bot]" <2886085+null[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2026 07:04:59 +0000 Subject: [PATCH 12/20] Prebid 10.17.0 release - reautomated --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6a87f1142..90cea2067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openads.js", - "version": "10.16.0", + "version": "10.17.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openads.js", - "version": "10.16.0", + "version": "10.17.0", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.28.4", diff --git a/package.json b/package.json index 70feb1d59..d7cae3218 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openads.js", - "version": "10.16.0", + "version": "10.17.0", "oaVersion": "1.5.0", "description": "Header Bidding Management Library", "main": "dist/src/prebid.public.ts", From bfc134b9e861897e66b63771aab8bbaad604bba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bendeg=C3=BAz=20=C3=81cs?= <30595431+acsbendi@users.noreply.github.com> Date: Mon, 10 Nov 2025 12:49:16 +0100 Subject: [PATCH 13/20] Various modules: consolidate page view ID logic (#14051) * Page view ID. * Page view ID. * Removed console.log. * Removed unused import. * Improved example. * Fixed some tests. --- .../gpt/prebidServer_example.html | 5 +- .../testBidder/testBidderBannerExample.html | 58 +++++++++++++------ libraries/adagioUtils/adagioUtils.js | 1 + .../pbsExtensions/processors/pageViewIds.js | 9 +++ libraries/pbsExtensions/processors/pbs.js | 7 ++- modules/ooloAnalyticsAdapter.js | 1 + .../openadsServerBidAdapter/ortbConverter.js | 10 +++- src/adapterManager.ts | 15 +++++ src/prebid.ts | 14 +++++ src/types/common.d.ts | 6 ++ .../modules/openadsServerBidAdapter_spec.js | 26 +++++++-- .../pbsExtensions/params_spec.js | 15 +---- .../ortbConverter/pbsExtensions/video_spec.js | 4 +- 13 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 libraries/pbsExtensions/processors/pageViewIds.js diff --git a/integrationExamples/gpt/prebidServer_example.html b/integrationExamples/gpt/prebidServer_example.html index 291e5ecd7..44fe229fa 100644 --- a/integrationExamples/gpt/prebidServer_example.html +++ b/integrationExamples/gpt/prebidServer_example.html @@ -83,9 +83,10 @@

OpenAds Test Bidder Example

+

+

Banner ad
- \ No newline at end of file + diff --git a/libraries/adagioUtils/adagioUtils.js b/libraries/adagioUtils/adagioUtils.js index c2614c45d..265a44201 100644 --- a/libraries/adagioUtils/adagioUtils.js +++ b/libraries/adagioUtils/adagioUtils.js @@ -22,6 +22,7 @@ export const _ADAGIO = (function() { const w = getBestWindowForAdagio(); w.ADAGIO = w.ADAGIO || {}; + // TODO: consider using the Prebid-generated page view ID instead of generating a custom one w.ADAGIO.pageviewId = w.ADAGIO.pageviewId || generateUUID(); w.ADAGIO.adUnits = w.ADAGIO.adUnits || {}; w.ADAGIO.pbjsAdUnits = w.ADAGIO.pbjsAdUnits || []; diff --git a/libraries/pbsExtensions/processors/pageViewIds.js b/libraries/pbsExtensions/processors/pageViewIds.js new file mode 100644 index 000000000..2de754917 --- /dev/null +++ b/libraries/pbsExtensions/processors/pageViewIds.js @@ -0,0 +1,9 @@ +import {deepSetValue} from '../../../src/utils.js'; + +export function setRequestExtPrebidPageViewIds(ortbRequest, bidderRequest) { + deepSetValue( + ortbRequest, + `ext.openads.page_view_ids.${bidderRequest.bidderCode}`, + bidderRequest.pageViewId + ); +} diff --git a/libraries/pbsExtensions/processors/pbs.js b/libraries/pbsExtensions/processors/pbs.js index 227eea901..e7df9b766 100644 --- a/libraries/pbsExtensions/processors/pbs.js +++ b/libraries/pbsExtensions/processors/pbs.js @@ -7,6 +7,7 @@ import {setImpAdUnitCode} from './adUnitCode.js'; import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js'; import {setBidResponseVideoCache} from './video.js'; import {addEventTrackers} from './eventTrackers.js'; +import {setRequestExtPrebidPageViewIds} from './pageViewIds.js'; export const PBS_PROCESSORS = { [REQUEST]: { @@ -21,7 +22,11 @@ export const PBS_PROCESSORS = { extPrebidAliases: { // sets ext.openads.aliases fn: setRequestExtPrebidAliases - } + }, + extPrebidPageViewIds: { + // sets ext.openads.page_view_ids + fn: setRequestExtPrebidPageViewIds + }, }, [IMP]: { params: { diff --git a/modules/ooloAnalyticsAdapter.js b/modules/ooloAnalyticsAdapter.js index 22b8476ef..2bcacd92d 100644 --- a/modules/ooloAnalyticsAdapter.js +++ b/modules/ooloAnalyticsAdapter.js @@ -22,6 +22,7 @@ const prebidVersion = '$prebid.version$' const analyticsType = 'endpoint' const ADAPTER_CODE = 'oolo' const AUCTION_END_SEND_TIMEOUT = 1500 +// TODO: consider using the Prebid-generated page view ID instead of generating a custom one export const PAGEVIEW_ID = +generatePageViewId() const { diff --git a/modules/openadsServerBidAdapter/ortbConverter.js b/modules/openadsServerBidAdapter/ortbConverter.js index e53b84f05..ee04d4676 100644 --- a/modules/openadsServerBidAdapter/ortbConverter.js +++ b/modules/openadsServerBidAdapter/ortbConverter.js @@ -27,10 +27,10 @@ const BIDDER_SPECIFIC_REQUEST_PROPS = new Set(['bidderCode', 'bidderRequestId', const getMinimumFloor = (() => { const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); return function(candidates) { - let min; + let min = null; for (const candidate of candidates) { if (candidate?.bidfloorcur == null || candidate?.bidfloor == null) return null; - min = min == null ? candidate : getMin(min, candidate); + min = min === null ? candidate : getMin(min, candidate); } return min; } @@ -133,7 +133,7 @@ const PBS_CONVERTER = ortbConverter({ // also, take overrides from s2sConfig.adapterOptions const adapterOptions = context.s2sBidRequest.s2sConfig.adapterOptions; for (const req of context.actualBidRequests.values()) { - setImpBidParams(imp, req, context, context); + setImpBidParams(imp, req); if (adapterOptions && adapterOptions[req.bidder]) { Object.assign(imp.ext.openads.bidder[req.bidder], adapterOptions[req.bidder]); } @@ -251,6 +251,10 @@ const PBS_CONVERTER = ortbConverter({ extPrebidAliases(orig, ortbRequest, proxyBidderRequest, context) { // override alias processing to do it for each bidder in the request context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); + }, + extPrebidPageViewIds(orig, ortbRequest, proxyBidderRequest, context) { + // override page view ID processing to do it for each bidder in the request + context.actualBidderRequests.forEach(req => orig(ortbRequest, req, context)); } }, [RESPONSE]: { diff --git a/src/adapterManager.ts b/src/adapterManager.ts index 2eb23990d..2d4283857 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -67,6 +67,7 @@ import type { AnalyticsConfig, AnalyticsProvider, AnalyticsProviderConfig, } from "../libraries/analyticsAdapter/AnalyticsAdapter.ts"; +import {getGlobal} from "./prebidGlobal.ts"; export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; @@ -168,6 +169,7 @@ export interface BaseBidderRequest { */ bidderRequestId: Identifier; auctionId: Identifier; + pageViewId: Identifier; /** * The bidder associated with this request, or null in the case of stored impressions. */ @@ -525,6 +527,15 @@ const adapterManager = { return bidderRequest as T; } + const pbjsInstance = getGlobal(); + + function getPageViewIdForBidder(bidderCode: string | null): string { + if (!pbjsInstance.pageViewIdPerBidder.has(bidderCode)) { + pbjsInstance.pageViewIdPerBidder.set(bidderCode, generateUUID()); + } + return pbjsInstance.pageViewIdPerBidder.get(bidderCode); + } + _s2sConfigs.forEach(s2sConfig => { const s2sParams = s2sActivityParams(s2sConfig); if (s2sConfig && s2sConfig.enabled && dep.isAllowed(ACTIVITY_FETCH_BIDS, s2sParams)) { @@ -536,11 +547,13 @@ const adapterManager = { (serverBidders.length === 0 && hasModuleBids ? [null] : serverBidders).forEach(bidderCode => { const tids = tidFor(extTids, bidderCode, () => ({})); const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ bidderCode, auctionId, bidderRequestId, + pageViewId, uniquePbsTid, bids: getBids({ bidderCode, @@ -584,10 +597,12 @@ const adapterManager = { clientBidders.forEach(bidderCode => { const tids = tidFor(extTids, bidderCode, () => ({})); const bidderRequestId = generateUUID(); + const pageViewId = getPageViewIdForBidder(bidderCode); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ bidderCode, auctionId, + pageViewId, bidderRequestId, bids: getBids({ bidderCode, diff --git a/src/prebid.ts b/src/prebid.ts index 611b90d8d..095bc6b85 100644 --- a/src/prebid.ts +++ b/src/prebid.ts @@ -106,6 +106,7 @@ declare module './prebidGlobal' { */ delayPrerendering?: boolean adUnits: AdUnitDefinition[]; + pageViewIdPerBidder: Map } } @@ -118,6 +119,7 @@ logInfo('OpenAds.js v$prebid.oaVersion$ based on Prebid.js v$prebid.version$ loa // create adUnit array pbjsInstance.adUnits = pbjsInstance.adUnits || []; +pbjsInstance.pageViewIdPerBidder = pbjsInstance.pageViewIdPerBidder || new Map(); function validateSizes(sizes, targLength?: number) { let cleanSizes = []; @@ -489,6 +491,7 @@ declare module './prebidGlobal' { processQueue: typeof processQueue; triggerBilling: typeof triggerBilling; generateTID: typeof generateTID; + refreshPageViewId: typeof refreshPageViewId; } } @@ -1289,4 +1292,15 @@ function generateTID () { } addApiMethod('generateTID', generateTID); +/** + * Refreshes the previously generated page view ID. Can be used to instruct bidders + * that use page view ID to consider future auctions as part of a new page load. + */ +function refreshPageViewId() { + for (const key of pbjsInstance.pageViewIdPerBidder.keys()) { + pbjsInstance.pageViewIdPerBidder.set(key, generateUUID()); + } +} +addApiMethod('refreshPageViewId', refreshPageViewId); + export default pbjsInstance; diff --git a/src/types/common.d.ts b/src/types/common.d.ts index b636a6cba..3f385ab68 100644 --- a/src/types/common.d.ts +++ b/src/types/common.d.ts @@ -14,6 +14,12 @@ export type Currency = string; export type AdUnitCode = string; export type Size = [number, number]; export type ContextIdentifiers = { + /** + * Page view ID. Unique for a page view (one load of Prebid); can also be refreshed programmatically. + * Shared across all requests and responses within the page view, for the same bidder. + * Different bidders see a different page view ID. + */ + pageViewId: Identifier; /** * Auction ID. Unique for any given auction, but shared across all requests and responses within that auction. */ diff --git a/test/spec/modules/openadsServerBidAdapter_spec.js b/test/spec/modules/openadsServerBidAdapter_spec.js index 721f91747..01508463b 100644 --- a/test/spec/modules/openadsServerBidAdapter_spec.js +++ b/test/spec/modules/openadsServerBidAdapter_spec.js @@ -752,6 +752,7 @@ describe('S2S Adapter', function () { 'auctionId': '173afb6d132ba3', 'bidderRequestId': '3d1063078dfcc8', 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', + 'pageViewId': '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', 'bids': [ { 'bidder': 'appnexus', @@ -2373,7 +2374,7 @@ describe('S2S Adapter', function () { }; adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); const payload = JSON.parse(server.requests[0].requestBody); - const permissions = payload.ext.prebid?.data?.eidpermissions; + const permissions = payload.ext.openads?.data?.eidpermissions; if (permissions) { permissions.forEach(p => { expect(p.bidders).to.be.an('array').that.is.not.empty; @@ -2499,7 +2500,7 @@ describe('S2S Adapter', function () { expect(requestBid.ext.openads.targeting.includewinners).to.equal(true); }); - it('adds extPrebid s2sConfig video.ext.openads to request for ORTB', function () { + it('adds custom property in s2sConfig.extPrebid to request for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { foo: 'bar' @@ -2561,7 +2562,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.openads properties using s2sConfig video.ext.openads values for ORTB', function () { + it('overrides request.ext.openads properties using s2sConfig.extPrebid values for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { targeting: { @@ -2594,7 +2595,7 @@ describe('S2S Adapter', function () { }); }); - it('overrides request.ext.openads properties using s2sConfig video.ext.openads values for ORTB', function () { + it('overrides request.ext.openads properties and adds custom property from s2sConfig.extPrebid for ORTB', function () { const s2sConfig = Object.assign({}, CONFIG, { extPrebid: { cache: { @@ -2897,6 +2898,21 @@ describe('S2S Adapter', function () { expect(parsedRequestBody.ext.openads.multibid).to.deep.equal(expected); }); + it('passes page view IDs per bidder in request', function () { + const clonedBidRequest = utils.deepClone(BID_REQUESTS[0]); + clonedBidRequest.bidderCode = 'some-other-bidder'; + clonedBidRequest.pageViewId = '490a1cbc-a03c-429a-b212-ba3649ca820c'; + const bidRequests = [BID_REQUESTS[0], clonedBidRequest]; + const expected = { + appnexus: '84dfd20f-0a5a-4ac6-a86b-91569066d4f4', + 'some-other-bidder': '490a1cbc-a03c-429a-b212-ba3649ca820c' + }; + + adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + expect(parsedRequestBody.ext.openads.page_view_ids).to.deep.equal(expected); + }); + it('sets and passes pbjs version in request if channel does not exist in s2sConfig', () => { const s2sBidRequest = utils.deepClone(REQUEST); const bidRequests = utils.deepClone(BID_REQUESTS); @@ -3615,7 +3631,7 @@ describe('S2S Adapter', function () { expect(response).to.have.property('cpm', 10); }); - it('handles response cache from ext.prebid.cache.vastXml', function () { + it('handles response cache from ext.openads.cache.vastXml', function () { const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' diff --git a/test/spec/ortbConverter/pbsExtensions/params_spec.js b/test/spec/ortbConverter/pbsExtensions/params_spec.js index 561a17353..b624cf84c 100644 --- a/test/spec/ortbConverter/pbsExtensions/params_spec.js +++ b/test/spec/ortbConverter/pbsExtensions/params_spec.js @@ -1,20 +1,9 @@ import {setImpBidParams} from '../../../../libraries/pbsExtensions/processors/params.js'; describe('pbjs -> ortb bid params to imp[].ext.openads.BIDDER', () => { - let bidderRegistry, index, adUnit; - beforeEach(() => { - bidderRegistry = {}; - adUnit = {code: 'mockAdUnit'}; - index = { - getAdUnit() { - return adUnit; - } - } - }); - - function setParams(bidRequest, context, deps = {}) { + function setParams(bidRequest = {}) { const imp = {}; - setImpBidParams(imp, bidRequest, context, Object.assign({bidderRegistry, index}, deps)) + setImpBidParams(imp, bidRequest) return imp; } diff --git a/test/spec/ortbConverter/pbsExtensions/video_spec.js b/test/spec/ortbConverter/pbsExtensions/video_spec.js index 95541d297..489bb37ec 100644 --- a/test/spec/ortbConverter/pbsExtensions/video_spec.js +++ b/test/spec/ortbConverter/pbsExtensions/video_spec.js @@ -26,14 +26,14 @@ describe('pbjs - ortb videoCacheKey based on ext.openads', () => { expect(resp).to.eql({mediaType: 'banner'}); }); - it('sets videoCacheKey, vastUrl from ext.prebid.cache.vastXml', () => { + it('sets videoCacheKey, vastUrl from ext.openads.cache.vastXml', () => { sinon.assert.match(setCache(EXT_PREBID_CACHE), { videoCacheKey: 'id', vastUrl: 'url' }); }); - it('sets videoCacheKey, vastUrl from ext.prebid.targeting', () => { + it('sets videoCacheKey, vastUrl from ext.openads.targeting', () => { sinon.assert.match(setCache({ ext: { openads: { From 68d5dbfad61fdeca5ac2e5c7240914f20ecf27a6 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Mon, 17 Nov 2025 11:58:06 -0500 Subject: [PATCH 14/20] CI: split tests into smaller chunks (#14126) * CI: increase karma browserNoActivityTimeout * try decreasing * increase test timeout * Fix fintezaAnalytics tests * Increase number of test chunks * Fix number of chunks * Extract save-wdir action * use matrix * simplify * fix chunk def * fix concurrency key * fix test cmd * Output concurrency * Fix concurrency key again * only save coverage if it exists * adjust max-parallel * collect coverage results * temp disable e2e * rename serialize to browserstack * Use wdir to get access to local action * fix github_output-s * set up node, fix coverage cache keys * temporarily enable coverage on nofeatures * skip browserstack step when not using browserstack * key prefix * debug output * debug * remove some debug output * script syntax * matrix output * adjust matrix output * fixes * use artifacts directly * cleanup outputs * use artifacts instead of cache * consolidate * specify path * debug * build when there is no build * include hidden files * overwrite artifacts * try save/load * adjust load * try skipping untar * more debug * Remove debug output * cleanup dependencies * Adjust timeouts * adjust overall timeout * adjust timeouts again * fix finteza tests * adjust timeouts * use build name from env * Clear browserstack sessions * Always clean up sessions * sessions cannot be terminated via api * adjust capture timeout * skip build when there is no build cmd * increase retries * adjust timeouts * add chunk no in build name * Fix coveralls * isolate browserstack tests * Revert "isolate browserstack tests" This reverts commit 2ebfa9d2b4dc4b4baf71e448149e6321ff218da6. --- .github/actions/load/action.yml | 30 +++ .github/actions/save/action.yml | 19 ++ .../actions/wait-for-browserstack/action.yml | 10 +- .github/workflows/run-tests.yml | 180 ++++++++++++++++++ .github/workflows/run-unit-tests.yml | 109 ----------- .github/workflows/test-chunk.yml | 103 ---------- .github/workflows/test.yml | 106 +++-------- gulpfile.js | 2 +- karma.conf.maker.js | 10 +- wdio.conf.js | 3 +- 10 files changed, 266 insertions(+), 306 deletions(-) create mode 100644 .github/actions/load/action.yml create mode 100644 .github/actions/save/action.yml create mode 100644 .github/workflows/run-tests.yml delete mode 100644 .github/workflows/run-unit-tests.yml delete mode 100644 .github/workflows/test-chunk.yml diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml new file mode 100644 index 000000000..85ea9009a --- /dev/null +++ b/.github/actions/load/action.yml @@ -0,0 +1,30 @@ +name: Load working directory +description: Load working directory saved with "actions/save" +inputs: + name: + description: The name used with actions/save + +runs: + using: 'composite' + steps: + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '20' + + - name: 'Clear working directory' + shell: bash + run: | + rm -r ./* + + - name: Download artifact + uses: actions/download-artifact@v5 + with: + path: '${{ runner.temp }}' + name: '${{ inputs.name }}' + + - name: 'Untar working directory' + shell: bash + run: | + tar -xf '${{ runner.temp }}/${{ inputs.name }}.tar' . + diff --git a/.github/actions/save/action.yml b/.github/actions/save/action.yml new file mode 100644 index 000000000..3dd8c1f6e --- /dev/null +++ b/.github/actions/save/action.yml @@ -0,0 +1,19 @@ +name: Save working directory +description: Save working directory, preserving permissions +inputs: + name: + description: a name to reference with actions/load + +runs: + using: 'composite' + steps: + - name: Tar working directory + shell: bash + run: | + tar -cf "${{ runner.temp }}/${{ inputs.name }}.tar" . + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + path: '${{ runner.temp }}/${{ inputs.name }}.tar' + name: ${{ inputs.name }} + overwrite: true diff --git a/.github/actions/wait-for-browserstack/action.yml b/.github/actions/wait-for-browserstack/action.yml index 63d24b87f..12ad89d70 100644 --- a/.github/actions/wait-for-browserstack/action.yml +++ b/.github/actions/wait-for-browserstack/action.yml @@ -1,18 +1,10 @@ name: Wait for browserstack sessions description: Wait until enough browserstack sessions have become available -inputs: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" runs: using: 'composite' steps: - - env: - BROWSERSTACK_USERNAME: ${{ inputs.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ inputs.BROWSERSTACK_ACCESS_KEY }} - shell: bash + - shell: bash run: | while status=$(curl -u "${BROWSERSTACK_USERNAME}:${BROWSERSTACK_ACCESS_KEY}" \ diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml new file mode 100644 index 000000000..f76ab69a0 --- /dev/null +++ b/.github/workflows/run-tests.yml @@ -0,0 +1,180 @@ +name: Run unit tests +on: + workflow_call: + inputs: + chunks: + description: Number of chunks to split tests into + required: false + type: number + default: 1 + build-cmd: + description: Build command, run once + required: false + type: string + test-cmd: + description: Test command, run once per chunk + required: true + type: string + browserstack: + description: If true, set up browserstack environment and adjust concurrency + required: false + type: boolean + timeout: + description: Timeout on test run + required: false + type: number + default: 10 + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.collect-coverage.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + chunks: ${{ steps.chunks.outputs.chunks }} + wdir: ${{ inputs.build-cmd && format('build-{0}', inputs.build-cmd) || 'source' }} + steps: + - name: Checkout + if: ${{ inputs.build-cmd }} + uses: actions/checkout@v5 + - name: Restore source + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/load + with: + name: source + + - name: Build + if: ${{ inputs.build-cmd }} + run: ${{ inputs.build-cmd }} + + - name: 'Save working directory' + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/save + with: + name: build-${{ inputs.build-cmd }} + + - name: Define chunks + id: chunks + run: | + echo 'chunks=[ '$(seq --separator=, 1 1 ${{ inputs.chunks }})' ]' >> "$GITHUB_OUTPUT" + + + + run-tests: + needs: build + strategy: + fail-fast: false + max-parallel: ${{ inputs.browserstack && 1 || inputs.chunks }} + matrix: + chunk-no: ${{ fromJSON(needs.build.outputs.chunks) }} + + name: "Test chunk ${{ matrix.chunk-no }}" + env: + BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + TEST_CHUNKS: ${{ inputs.chunks }} + TEST_CHUNK: ${{ matrix.chunk-no }} + outputs: + coverage: ${{ steps.coverage.outputs.coverage }} + concurrency: + # The following generates 'browserstack-' when inputs.browserstack is true, and a hopefully unique ID otherwise + # Ideally we'd like to serialize browserstack access across all workflows, but github's max queue length is only 1 + # (cfr. https://github.com/orgs/community/discussions/12835) + # so we add the run_id to serialize only within one push / pull request (which has the effect of queueing e2e and unit tests) + group: ${{ inputs.browserstack && 'browser' || github.run_id }}${{ inputs.browserstack && 'stac' || inputs.test-cmd }}${{ inputs.browserstack && 'k' || matrix.chunk-no }}-${{ github.run_id }} + cancel-in-progress: false + + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.wdir }} + + - name: 'BrowserStack Env Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-env@master' + with: + username: ${{ secrets.BROWSERSTACK_USER_NAME}} + access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} + build-name: Run ${{github.run_id}}, attempt ${{ github.run_attempt }}, chunk ${{ matrix.chunk-no }}, ref ${{ github.event_name == 'pull_request_target' && format('PR {0}', github.event.pull_request.number) || github.ref }}, ${{ inputs.test-cmd }} + + - name: 'BrowserStackLocal Setup' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: start + local-identifier: random + + - name: 'Wait for browserstack' + if: ${{ inputs.browserstack }} + uses: ./.github/actions/wait-for-browserstack + + - name: Run tests + uses: nick-fields/retry@v3 + with: + timeout_minutes: ${{ inputs.timeout }} + max_attempts: 3 + command: ${{ inputs.test-cmd }} + + - name: 'BrowserStackLocal Stop' + if: ${{ inputs.browserstack }} + uses: 'browserstack/github-actions/setup-local@master' + with: + local-testing: stop + + - name: 'Check for coverage' + id: 'coverage' + run: | + if [ -d "./build/coverage" ]; then + echo 'coverage=true' >> "$GITHUB_OUTPUT"; + fi + + - name: 'Save coverage result' + if: ${{ steps.coverage.outputs.coverage }} + uses: actions/upload-artifact@v4 + with: + name: coverage-partial-${{inputs.test-cmd}}-${{ matrix.chunk-no }} + path: ./build/coverage + overwrite: true + + collect-coverage: + if: ${{ needs.run-tests.outputs.coverage }} + needs: [build, run-tests] + name: 'Collect coverage results' + runs-on: ubuntu-latest + outputs: + coverage: coverage-complete-${{ inputs.test-cmd }} + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.wdir }} + + - name: Download coverage results + uses: actions/download-artifact@v5 + with: + path: ./build/coverage + pattern: coverage-partial-${{ inputs.test-cmd }}-* + merge-multiple: true + + - name: 'Save working directory' + uses: ./.github/actions/save + with: + name: coverage-complete-${{ inputs.test-cmd }} + diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml deleted file mode 100644 index 54bae4417..000000000 --- a/.github/workflows/run-unit-tests.yml +++ /dev/null @@ -1,109 +0,0 @@ -name: Run unit tests -on: - workflow_call: - inputs: - build-cmd: - description: Build command, run once - required: true - type: string - test-cmd: - description: Test command, run once per chunk - required: true - type: string - serialize: - description: If true, allow only one concurrent chunk (see note on concurrency below) - required: false - type: boolean - outputs: - wdir: - description: Cache key for the working directory after running tests - value: ${{ jobs.chunk-4.outputs.wdir }} - secrets: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" - -jobs: - build: - name: Build - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - - - name: Fetch source - uses: actions/cache/restore@v5 - with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true - - - name: Build - run: ${{ inputs.build-cmd }} - - - name: Cache build output - uses: actions/cache/save@v5 - with: - path: . - key: build-${{ inputs.build-cmd }}-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v5 - with: - path: . - key: build-${{ inputs.build-cmd }}-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true - - chunk-1: - needs: build - name: Run tests (chunk 1 of 4) - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 1 - wdir: build-${{ inputs.build-cmd }}-${{ github.run_id }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-2: - name: Run tests (chunk 2 of 4) - needs: chunk-1 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 2 - wdir: ${{ needs.chunk-1.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-3: - name: Run tests (chunk 3 of 4) - needs: chunk-2 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 3 - wdir: ${{ needs.chunk-2.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - chunk-4: - name: Run tests (chunk 4 of 4) - needs: chunk-3 - uses: ./.github/workflows/test-chunk.yml - with: - chunk-no: 4 - wdir: ${{ needs.chunk-3.outputs.wdir }} - cmd: ${{ inputs.test-cmd }} - serialize: ${{ inputs.serialize }} - secrets: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/test-chunk.yml b/.github/workflows/test-chunk.yml deleted file mode 100644 index 3c0987d91..000000000 --- a/.github/workflows/test-chunk.yml +++ /dev/null @@ -1,103 +0,0 @@ -name: Test chunk -on: - workflow_call: - inputs: - serialize: - required: false - type: boolean - cmd: - required: true - type: string - chunk-no: - required: true - type: number - wdir: - required: true - type: string - outputs: - wdir: - description: "Cache key for the working directory after running tests" - value: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - secrets: - BROWSERSTACK_USER_NAME: - description: "Browserstack user name" - BROWSERSTACK_ACCESS_KEY: - description: "Browserstack access key" - -concurrency: - # The following generates 'browserstack-' when inputs.serialize is true, and a hopefully unique ID otherwise - # Ideally we'd like to serialize browserstack access across all workflows, but github's max queue length is only 1 - # (cfr. https://github.com/orgs/community/discussions/12835) - # so we add the run_id to serialize only within one push / pull request (which has the effect of queueing e2e and unit tests) - group: ${{ inputs.serialize && 'browser' || github.run_id }}${{ inputs.serialize && 'stack' || inputs.cmd }}-${{ github.run_id }} - cancel-in-progress: false - -jobs: - test: - name: "Test chunk ${{ inputs.chunk-no }}" - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - TEST_CHUNKS: 4 - TEST_CHUNK: ${{ inputs.chunk-no }} - runs-on: ubuntu-latest - steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - - - name: Restore working directory - id: restore-dir - uses: actions/cache/restore@v5 - with: - path: . - key: ${{ inputs.wdir }} - fail-on-cache-miss: true - - - name: 'BrowserStack Env Setup' - uses: 'browserstack/github-actions/setup-env@master' - with: - username: ${{ secrets.BROWSERSTACK_USER_NAME}} - access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: 'BrowserStackLocal Setup' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: start - local-identifier: random - - - name: 'Wait for browserstack' - if: ${{ inputs.serialize }} - uses: ./.github/actions/wait-for-browserstack - with: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 8 - max_attempts: 3 - command: ${{ inputs.cmd }} - - - name: 'BrowserStackLocal Stop' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: stop - - - name: Save working directory - uses: actions/cache/save@v5 - with: - path: . - key: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v5 - with: - path: . - key: test-${{ inputs.cmd }}-${{ inputs.chunk-no }}-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true - - diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8765056f..755be4b6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,35 +59,22 @@ jobs: - name: Install dependencies run: npm ci - - name: Cache source - uses: actions/cache/save@v5 + - name: 'Save working directory' + uses: ./.github/actions/save with: - path: . - key: source-${{ github.run_id }} - - - name: Verify cache - uses: actions/cache/restore@v5 - with: - path: . - key: source-${{ github.run_id }} - lookup-only: true - fail-on-cache-miss: true + name: source lint: name: "Run linter" needs: checkout runs-on: ubuntu-latest steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' + - name: Checkout + uses: actions/checkout@v5 - name: Restore source - uses: actions/cache/restore@v5 + uses: ./.github/actions/load with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true + name: source - name: lint run: | npx eslint @@ -95,78 +82,39 @@ jobs: test-no-features: name: "Unit tests (all features disabled)" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml + uses: ./.github/workflows/run-tests.yml with: + chunks: 8 build-cmd: npx gulp precompile-all-features-disabled test-cmd: npx gulp test-all-features-disabled-nobuild - serialize: false + browserstack: false secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} test: name: "Unit tests (all features enabled + coverage)" needs: checkout - uses: ./.github/workflows/run-unit-tests.yml + uses: ./.github/workflows/run-tests.yml with: + chunks: 8 build-cmd: npx gulp precompile test-cmd: npx gulp test-only-nobuild --browserstack - serialize: true + browserstack: true secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} test-e2e: - name: "End-to-end tests" + name: End-to-end tests needs: checkout - runs-on: ubuntu-latest - concurrency: - # see test-chunk.yml for notes on concurrency groups - group: browserstack-${{ github.run_id }} - cancel-in-progress: false - env: - BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + uses: ./.github/workflows/run-tests.yml + with: + test-cmd: npx gulp e2e-test + browserstack: true + timeout: 15 + secrets: + BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - steps: - - name: Set up Node.js - uses: actions/setup-node@v6 - with: - node-version: '20' - - name: Restore source - uses: actions/cache/restore@v5 - with: - path: . - key: source-${{ github.run_id }} - fail-on-cache-miss: true - - - name: 'BrowserStack Env Setup' - uses: 'browserstack/github-actions/setup-env@master' - with: - username: ${{ secrets.BROWSERSTACK_USER_NAME}} - access-key: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: 'BrowserStackLocal Setup' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: start - local-identifier: random - - - name: 'Wait for browserstack' - uses: ./.github/actions/wait-for-browserstack - with: - BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} - BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} - - - name: Run tests - uses: nick-fields/retry@v3 - with: - timeout_minutes: 20 - max_attempts: 3 - command: npx gulp e2e-test - - - name: 'BrowserStackLocal Stop' - uses: 'browserstack/github-actions/setup-local@master' - with: - local-testing: stop coveralls: name: Update coveralls @@ -174,12 +122,14 @@ jobs: if: false #REMOVE ONCE REPO IS PUBLIC runs-on: ubuntu-latest steps: - - name: Restore working directory - uses: actions/cache/restore@v5 + - name: Checkout + uses: actions/checkout@v5 + + - name: Restore source + uses: ./.github/actions/load with: - path: . - key: ${{ needs.test.outputs.wdir }} - fail-on-cache-miss: true + name: ${{ needs.test.outputs.coverage }} + - name: Coveralls uses: coverallsapp/github-action@v2 with: diff --git a/gulpfile.js b/gulpfile.js index 4f3de3b8c..b915e24d6 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,7 +39,7 @@ const TerserPlugin = require('terser-webpack-plugin'); const {precompile, babelPrecomp} = require('./gulp.precompilation.js'); -const TEST_CHUNKS = 4; +const TEST_CHUNKS = 8; // these modules must be explicitly listed in --modules to be included in the build, won't be part of "all" modules var explicitModules = [ diff --git a/karma.conf.maker.js b/karma.conf.maker.js index 1068e9828..f825b8eac 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -88,7 +88,7 @@ function setBrowsers(karmaConf, browserstack) { karmaConf.browserStack = { username: process.env.BROWSERSTACK_USERNAME, accessKey: process.env.BROWSERSTACK_ACCESS_KEY, - build: 'Prebidjs Unit Tests ' + new Date().toLocaleString() + build: process.env.BROWSERSTACK_BUILD_NAME } if (process.env.TRAVIS) { karmaConf.browserStack.startTunnel = false; @@ -174,10 +174,10 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: !watchMode, - browserDisconnectTimeout: 1e5, // default 2000 - browserNoActivityTimeout: 1e5, // default 10000 - captureTimeout: 3e5, // default 60000, - browserDisconnectTolerance: 3, + browserDisconnectTimeout: 1e4, + browserNoActivityTimeout: 3e4, + captureTimeout: 2e4, + browserDisconnectTolerance: 5, concurrency: 5, // browserstack allows us 5 concurrent sessions plugins: plugins diff --git a/wdio.conf.js b/wdio.conf.js index d23fecd0b..53ccd216b 100644 --- a/wdio.conf.js +++ b/wdio.conf.js @@ -1,4 +1,5 @@ const shared = require('./wdio.shared.conf.js'); +const process = require('process'); const browsers = Object.fromEntries( Object.entries(require('./browsers.json')) @@ -28,7 +29,7 @@ function getCapabilities() { osVersion: browser.os_version, networkLogs: true, consoleLogs: 'verbose', - buildName: `Prebidjs E2E (${browser.browser} ${browser.browser_version}) ${new Date().toLocaleString()}` + buildName: process.env.BROWSERSTACK_BUILD_NAME }, acceptInsecureCerts: true, }); From 70014241ffea79e4b2cba6e3153eac3ff87801d4 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Wed, 19 Nov 2025 10:56:02 -0500 Subject: [PATCH 15/20] CI: reduce dependency on browserstack (#14165) * try safari testing * try macos-latest * adjust save/load * adjust again * why * adjust -C * try safarinative * use SafariNative * separate build from run-tests * browser tests * add safari * refactor * use setup step * add firefoxHeadless * add edge * try force-local * temporarily remove build logic testing * --force-local only on win * add npm install to build * run npm install on windows * use script to generate id * use --force-local on save as well * setup edge * remove run npm install option * enable debug logging * try edge headless * define EdgeHeadless * try chromium edge * remove edge launcher * disable debug logging * add build logic test step * set shell bash * move id generation to actions/save * browser_testing.json * fix coverage * add browerstack * fix bstack secrets * remove unnecessary checkout * Clean up unused input * try clearing localStorage --- .github/actions/load/action.yml | 16 ++- .github/actions/save/action.yml | 28 +++++- .github/workflows/browser-tests.yml | 116 +++++++++++++++++++++ .github/workflows/browser_testing.json | 17 ++++ .github/workflows/build.yml | 43 ++++++++ .github/workflows/run-tests.yml | 54 +++++----- .github/workflows/test.yml | 5 +- gulpfile.js | 2 +- karma.conf.maker.js | 5 +- package-lock.json | 133 ++++++++----------------- package.json | 2 + test/test_deps.js | 2 + 12 files changed, 287 insertions(+), 136 deletions(-) create mode 100644 .github/workflows/browser-tests.yml create mode 100644 .github/workflows/browser_testing.json create mode 100644 .github/workflows/build.yml diff --git a/.github/actions/load/action.yml b/.github/actions/load/action.yml index 85ea9009a..0102608db 100644 --- a/.github/actions/load/action.yml +++ b/.github/actions/load/action.yml @@ -11,11 +11,17 @@ runs: uses: actions/setup-node@v6 with: node-version: '20' - + - uses: actions/github-script@v8 + id: platform + with: + result-encoding: string + script: | + const os = require('os'); + return os.platform(); - name: 'Clear working directory' shell: bash run: | - rm -r ./* + rm -r "$(pwd)"/* - name: Download artifact uses: actions/download-artifact@v5 @@ -26,5 +32,7 @@ runs: - name: 'Untar working directory' shell: bash run: | - tar -xf '${{ runner.temp }}/${{ inputs.name }}.tar' . - + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ steps.platform.outputs.result == 'win32' && '--force-local' || '' }} -C "$parent" -xf '${{ runner.temp }}/${{ inputs.name }}.tar' "$target" diff --git a/.github/actions/save/action.yml b/.github/actions/save/action.yml index 3dd8c1f6e..3efca584c 100644 --- a/.github/actions/save/action.yml +++ b/.github/actions/save/action.yml @@ -1,19 +1,41 @@ name: Save working directory description: Save working directory, preserving permissions inputs: + prefix: + description: Prefix to use for autogenerated names + required: false name: description: a name to reference with actions/load + required: false +outputs: + name: + description: a name to reference with actions/load + value: ${{ fromJSON(steps.platform.outputs.result).name }} runs: using: 'composite' steps: + - uses: actions/github-script@v8 + id: platform + with: + script: | + const os = require('os'); + const crypto = require("crypto"); + const id = crypto.randomBytes(16).toString("hex"); + return { + name: ${{ inputs.name && format('"{0}"', inputs.name) || format('"{0}" + id', inputs.prefix || '') }}, + platform: os.platform(), + } - name: Tar working directory shell: bash run: | - tar -cf "${{ runner.temp }}/${{ inputs.name }}.tar" . + wdir="$(pwd)" + parent="$(dirname "$wdir")" + target="$(basename "$wdir")" + tar ${{ fromJSON(steps.platform.outputs.result).platform == 'win32' && '--force-local' || '' }} -C "$parent" -cf "${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar" "$target" - name: Upload artifact uses: actions/upload-artifact@v4 with: - path: '${{ runner.temp }}/${{ inputs.name }}.tar' - name: ${{ inputs.name }} + path: '${{ runner.temp }}/${{ fromJSON(steps.platform.outputs.result).name }}.tar' + name: ${{ fromJSON(steps.platform.outputs.result).name }} overwrite: true diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 000000000..662bf38d9 --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,116 @@ +name: Run unit tests on all browsers +on: + workflow_call: + inputs: + chunks: + description: Number of chunks to split tests into + required: false + type: number + default: 1 + build-cmd: + description: Build command, run once + required: false + type: string + test-cmd: + description: Test command, run once per chunk + required: true + type: string + timeout: + description: Timeout on test run + required: false + type: number + default: 10 + outputs: + coverage: + description: Artifact name for coverage results + value: ${{ jobs.browser-tests.outputs.coverage }} + secrets: + BROWSERSTACK_USER_NAME: + description: "Browserstack user name" + BROWSERSTACK_ACCESS_KEY: + description: "Browserstack access key" +jobs: + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: ${{ inputs.build-cmd }} + + setup: + needs: build + name: "Setup environment" + runs-on: ubuntu-latest + outputs: + browsers: ${{ toJSON(fromJSON(steps.define.outputs.result).browsers) }} + bstack-key: ${{ steps.bstack-save.outputs.name }} + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Restore working directory + uses: ./.github/actions/load + with: + name: ${{ needs.build.outputs.built-key }} + - name: "Define testing strategy" + uses: actions/github-script@v8 + id: define + with: + script: | + const fs = require('node:fs/promises'); + const browsers = require('./.github/workflows/browser_testing.json'); + const excludeFromBstack = Object.values(browsers).map(browser => browser.bsName); + const bstackBrowsers = Object.fromEntries( + // exlude "latest" version of browsers that we can test on GH actions + Object.entries(require('./browsers.json')) + .filter(([name, def]) => !excludeFromBstack.includes(def.browser) || def.browser_version !== 'latest') + ) + const updatedBrowsersJson = JSON.stringify(bstackBrowsers, null, 2); + console.log("Using browsers.json:", updatedBrowsersJson); + await fs.writeFile('./browsers.json', updatedBrowsersJson); + return { + hasBSBrowsers: Object.keys(bstackBrowsers).length > 0, + browsers: Object.entries(browsers).map(([name, def]) => Object.assign({name}, def)) + } + - name: "Save working directory" + id: bstack-save + if: ${{ fromJSON(steps.define.outputs.result).hasBSBrowsers }} + uses: ./.github/actions/save + with: + prefix: browserstack- + + test-build-logic: + needs: build + name: "Test build logic" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: gulp test-build-logic + + browser-tests: + needs: [setup, build] + name: "Browser: ${{ matrix.browser.name }}" + strategy: + fail-fast: false + matrix: + browser: ${{ fromJSON(needs.setup.outputs.browsers) }} + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.build.outputs.built-key }} + test-cmd: ${{ inputs.test-cmd }} --browsers ${{ matrix.browser.name }} ${{ matrix.browser.coverage && '--coverage' || '--no-coverage' }} + chunks: ${{ inputs.chunks }} + runs-on: ${{ matrix.browser.runsOn || 'ubuntu-latest' }} + + browserstack-tests: + needs: setup + if: ${{ needs.setup.outputs.bstack-key }} + name: "Browserstack tests" + uses: + ./.github/workflows/run-tests.yml + with: + built-key: ${{ needs.setup.outputs.bstack-key }} + test-cmd: ${{ inputs.test-cmd }} --browserstack --no-coverage + chunks: ${{ inputs.chunks }} + browserstack: true + secrets: + BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} + BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/.github/workflows/browser_testing.json b/.github/workflows/browser_testing.json new file mode 100644 index 000000000..aee1631ea --- /dev/null +++ b/.github/workflows/browser_testing.json @@ -0,0 +1,17 @@ +{ + "ChromeHeadless": { + "bsName": "chrome", + "coverage": true + }, + "EdgeHeadless": { + "bsName": "edge", + "runsOn": "windows-latest" + }, + "SafariNative": { + "bsName": "safari", + "runsOn": "macos-latest" + }, + "FirefoxHeadless": { + "bsName": "firefox" + } +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..6942d25b5 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Run unit tests +on: + workflow_call: + inputs: + source-key: + description: Artifact name for source directory + type: string + required: false + default: source + build-cmd: + description: Build command + required: false + type: string + outputs: + built-key: + description: Artifact name for built directory + value: ${{ jobs.build.outputs.built-key }} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + built-key: ${{ inputs.build-cmd && steps.save.outputs.name || inputs.source-key }} + steps: + - name: Checkout + if: ${{ inputs.build-cmd }} + uses: actions/checkout@v5 + - name: Restore source + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/load + with: + name: ${{ inputs.source-key }} + - name: Build + if: ${{ inputs.build-cmd }} + run: ${{ inputs.build-cmd }} + - name: 'Save working directory' + id: save + if: ${{ inputs.build-cmd }} + uses: ./.github/actions/save + with: + prefix: 'build-' diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index f76ab69a0..26e180f13 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -11,6 +11,10 @@ on: description: Build command, run once required: false type: string + built-key: + description: Artifact name for built source + required: false + type: string test-cmd: description: Test command, run once per chunk required: true @@ -19,11 +23,17 @@ on: description: If true, set up browserstack environment and adjust concurrency required: false type: boolean + default: false timeout: description: Timeout on test run required: false type: number default: 10 + runs-on: + description: Runner image + required: false + default: ubuntu-latest + type: string outputs: coverage: description: Artifact name for coverage results @@ -35,47 +45,30 @@ on: description: "Browserstack access key" jobs: - build: - name: Build + checkout: + name: "Set up environment" runs-on: ubuntu-latest - timeout-minutes: 5 outputs: chunks: ${{ steps.chunks.outputs.chunks }} - wdir: ${{ inputs.build-cmd && format('build-{0}', inputs.build-cmd) || 'source' }} steps: - - name: Checkout - if: ${{ inputs.build-cmd }} - uses: actions/checkout@v5 - - name: Restore source - if: ${{ inputs.build-cmd }} - uses: ./.github/actions/load - with: - name: source - - - name: Build - if: ${{ inputs.build-cmd }} - run: ${{ inputs.build-cmd }} - - - name: 'Save working directory' - if: ${{ inputs.build-cmd }} - uses: ./.github/actions/save - with: - name: build-${{ inputs.build-cmd }} - - name: Define chunks id: chunks run: | echo 'chunks=[ '$(seq --separator=, 1 1 ${{ inputs.chunks }})' ]' >> "$GITHUB_OUTPUT" - - + + build: + uses: ./.github/workflows/build.yml + with: + build-cmd: ${{ !inputs.built-key && inputs.build-cmd || '' }} + source-key: ${{ inputs.built-key || 'source' }} run-tests: - needs: build + needs: [checkout, build] strategy: fail-fast: false max-parallel: ${{ inputs.browserstack && 1 || inputs.chunks }} matrix: - chunk-no: ${{ fromJSON(needs.build.outputs.chunks) }} + chunk-no: ${{ fromJSON(needs.checkout.outputs.chunks) }} name: "Test chunk ${{ matrix.chunk-no }}" env: @@ -93,7 +86,7 @@ jobs: group: ${{ inputs.browserstack && 'browser' || github.run_id }}${{ inputs.browserstack && 'stac' || inputs.test-cmd }}${{ inputs.browserstack && 'k' || matrix.chunk-no }}-${{ github.run_id }} cancel-in-progress: false - runs-on: ubuntu-latest + runs-on: ${{ inputs.runs-on }} steps: - name: Checkout uses: actions/checkout@v5 @@ -101,7 +94,7 @@ jobs: - name: Restore source uses: ./.github/actions/load with: - name: ${{ needs.build.outputs.wdir }} + name: ${{ needs.build.outputs.built-key }} - name: 'BrowserStack Env Setup' if: ${{ inputs.browserstack }} @@ -137,6 +130,7 @@ jobs: - name: 'Check for coverage' id: 'coverage' + shell: bash run: | if [ -d "./build/coverage" ]; then echo 'coverage=true' >> "$GITHUB_OUTPUT"; @@ -164,7 +158,7 @@ jobs: - name: Restore source uses: ./.github/actions/load with: - name: ${{ needs.build.outputs.wdir }} + name: ${{ needs.build.outputs.built-key }} - name: Download coverage results uses: actions/download-artifact@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 755be4b6e..a639397a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -94,12 +94,11 @@ jobs: test: name: "Unit tests (all features enabled + coverage)" needs: checkout - uses: ./.github/workflows/run-tests.yml + uses: ./.github/workflows/browser-tests.yml with: chunks: 8 build-cmd: npx gulp precompile - test-cmd: npx gulp test-only-nobuild --browserstack - browserstack: true + test-cmd: npx gulp test-only-nobuild secrets: BROWSERSTACK_USER_NAME: ${{ secrets.BROWSERSTACK_USER_NAME }} BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }} diff --git a/gulpfile.js b/gulpfile.js index b915e24d6..f272e3cfc 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -521,7 +521,7 @@ gulp.task('build-bundle-verbose', gulp.series(precompile(), makeWebpackPkg(makeV // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('update-browserslist', execaTask('npx update-browserslist-db@latest')); gulp.task('test-build-logic', execaTask('npx mocha ./test/build-logic')) -gulp.task('test-only-nobuild', gulp.series('test-build-logic', testTaskMaker({coverage: true}))) +gulp.task('test-only-nobuild', gulp.series(testTaskMaker({coverage: argv.coverage ?? true}))) gulp.task('test-only', gulp.series('test-build-logic', 'precompile', test)); gulp.task('test-all-features-disabled-nobuild', testTaskMaker({disableFeatures: helpers.getTestDisableFeatures(), oneBrowser: 'chrome', watch: false})); diff --git a/karma.conf.maker.js b/karma.conf.maker.js index f825b8eac..6866b296c 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -40,6 +40,7 @@ function newWebpackConfig(codeCoverage, disableFeatures) { function newPluginsArray(browserstack) { var plugins = [ 'karma-chrome-launcher', + 'karma-safarinative-launcher', 'karma-coverage', 'karma-mocha', 'karma-chai', @@ -47,14 +48,14 @@ function newPluginsArray(browserstack) { 'karma-sourcemap-loader', 'karma-spec-reporter', 'karma-webpack', - 'karma-mocha-reporter' + 'karma-mocha-reporter', + '@chiragrupani/karma-chromium-edge-launcher', ]; if (browserstack) { plugins.push('karma-browserstack-launcher'); } plugins.push('karma-firefox-launcher'); plugins.push('karma-opera-launcher'); - plugins.push('karma-safari-launcher'); plugins.push('karma-script-launcher'); return plugins; } diff --git a/package-lock.json b/package-lock.json index 90cea2067..983fd23bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, @@ -32,6 +33,7 @@ "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", "@eslint/compat": "^1.3.1", "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", @@ -75,6 +77,7 @@ "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.1", "karma-coverage-istanbul-reporter": "^3.0.3", + "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^2.1.0", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", @@ -1610,9 +1613,14 @@ "uuid": "9.0.1" } }, + "node_modules/@chiragrupani/karma-chromium-edge-launcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@chiragrupani/karma-chromium-edge-launcher/-/karma-chromium-edge-launcher-2.4.1.tgz", + "integrity": "sha512-HwTlN4dk7dnL9m5nEonq7cI3Wa787wYfGVWeb4oWPMySIEhFpA7/BYQ8zMbpQ4YkSQxVnvY1502aWdbI3w7DeA==", + "dev": true + }, "node_modules/@colors/colors": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.1.90" @@ -3402,7 +3410,6 @@ }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", - "dev": true, "license": "MIT" }, "node_modules/@stylistic/eslint-plugin": { @@ -3471,12 +3478,10 @@ }, "node_modules/@types/cookie": { "version": "0.4.1", - "dev": true, "license": "MIT" }, "node_modules/@types/cors": { "version": "2.8.17", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -3570,7 +3575,6 @@ }, "node_modules/@types/node": { "version": "20.14.2", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -6121,7 +6125,6 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6147,7 +6150,6 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -6784,7 +6786,6 @@ }, "node_modules/balanced-match": { "version": "1.0.2", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -6875,7 +6876,6 @@ }, "node_modules/base64id": { "version": "2.0.0", - "dev": true, "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" @@ -6929,7 +6929,6 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7124,7 +7123,6 @@ "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7133,7 +7131,6 @@ }, "node_modules/braces": { "version": "3.0.3", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -7476,7 +7473,6 @@ }, "node_modules/chokidar": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -7803,7 +7799,6 @@ }, "node_modules/concat-map": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/concat-with-sourcemaps": { @@ -7816,7 +7811,6 @@ }, "node_modules/connect": { "version": "3.7.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -7838,7 +7832,6 @@ }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -7846,7 +7839,6 @@ }, "node_modules/connect/node_modules/finalhandler": { "version": "1.1.2", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -7863,12 +7855,10 @@ }, "node_modules/connect/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -7879,7 +7869,6 @@ }, "node_modules/connect/node_modules/statuses": { "version": "1.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7971,7 +7960,6 @@ }, "node_modules/cors": { "version": "2.8.5", - "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4", @@ -8459,7 +8447,6 @@ }, "node_modules/custom-event": { "version": "1.0.1", - "dev": true, "license": "MIT" }, "node_modules/d": { @@ -8532,7 +8519,6 @@ }, "node_modules/date-format": { "version": "4.0.14", - "dev": true, "license": "MIT", "engines": { "node": ">=4.0" @@ -8813,7 +8799,6 @@ }, "node_modules/di": { "version": "0.0.1", - "dev": true, "license": "MIT" }, "node_modules/diff": { @@ -8843,7 +8828,6 @@ }, "node_modules/dom-serialize": { "version": "2.2.1", - "dev": true, "license": "MIT", "dependencies": { "custom-event": "~1.0.0", @@ -8996,6 +8980,12 @@ "wcwidth": "^1.0.1" } }, + "node_modules/edge-launcher": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", + "integrity": "sha512-JcD5WBi3BHZXXVSSeEhl6sYO8g5cuynk/hifBzds2Bp4JdzCGLNMHgMCKu5DvrO1yatMgF0goFsxXRGus0yh1g==", + "dev": true + }, "node_modules/edge-paths": { "version": "3.0.5", "dev": true, @@ -9128,7 +9118,6 @@ }, "node_modules/emoji-regex": { "version": "8.0.0", - "dev": true, "license": "MIT" }, "node_modules/emojis-list": { @@ -9141,7 +9130,6 @@ }, "node_modules/encodeurl": { "version": "1.0.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -9180,7 +9168,6 @@ }, "node_modules/engine.io": { "version": "6.6.2", - "dev": true, "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", @@ -9200,7 +9187,6 @@ }, "node_modules/engine.io-parser": { "version": "5.2.3", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -9208,7 +9194,6 @@ }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9230,7 +9215,6 @@ }, "node_modules/ent": { "version": "2.2.0", - "dev": true, "license": "MIT" }, "node_modules/entities": { @@ -10597,7 +10581,6 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", - "dev": true, "license": "MIT" }, "node_modules/events": { @@ -10887,7 +10870,6 @@ }, "node_modules/extend": { "version": "3.0.2", - "dev": true, "license": "MIT" }, "node_modules/extend-shallow": { @@ -11152,7 +11134,6 @@ }, "node_modules/fill-range": { "version": "7.1.1", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -11282,12 +11263,10 @@ }, "node_modules/flatted": { "version": "3.3.1", - "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.6", - "dev": true, "funding": [ { "type": "individual", @@ -11463,7 +11442,6 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11596,7 +11574,6 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", - "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -11765,7 +11742,6 @@ }, "node_modules/glob-parent": { "version": "5.1.2", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -12848,7 +12824,6 @@ }, "node_modules/http-proxy": { "version": "1.18.1", - "dev": true, "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", @@ -13023,7 +12998,6 @@ }, "node_modules/inflight": { "version": "1.0.6", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -13195,7 +13169,6 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -13333,7 +13306,6 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13355,7 +13327,6 @@ }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13382,7 +13353,6 @@ }, "node_modules/is-glob": { "version": "4.0.3", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -13427,7 +13397,6 @@ }, "node_modules/is-number": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -13681,7 +13650,6 @@ }, "node_modules/isbinaryfile": { "version": "4.0.10", - "dev": true, "license": "MIT", "engines": { "node": ">= 8.0.0" @@ -14580,7 +14548,6 @@ "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", - "dev": true, "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", @@ -14786,6 +14753,21 @@ "semver": "bin/semver" } }, + "node_modules/karma-edge-launcher": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", + "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", + "dev": true, + "dependencies": { + "edge-launcher": "1.2.2" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, "node_modules/karma-firefox-launcher": { "version": "2.1.3", "dev": true, @@ -14864,8 +14846,17 @@ }, "node_modules/karma-safari-launcher": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/karma-safari-launcher/-/karma-safari-launcher-1.0.0.tgz", + "integrity": "sha512-qmypLWd6F2qrDJfAETvXDfxHvKDk+nyIjpH9xIeI3/hENr0U3nuqkxaftq73PfXZ4aOuOChA6SnLW4m4AxfRjQ==", "dev": true, - "license": "MIT", + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-safarinative-launcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/karma-safarinative-launcher/-/karma-safarinative-launcher-1.1.0.tgz", + "integrity": "sha512-vdMjdQDHkSUbOZc8Zq2K5bBC0yJGFEgfrKRJTqt0Um0SC1Rt8drS2wcN6UA3h4LgsL3f1pMcmRSvKucbJE8Qdg==", "peerDependencies": { "karma": ">=0.9" } @@ -14982,7 +14973,6 @@ }, "node_modules/karma/node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -14996,7 +14986,6 @@ }, "node_modules/karma/node_modules/cliui": { "version": "7.0.4", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -15006,7 +14995,6 @@ }, "node_modules/karma/node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -15017,12 +15005,10 @@ }, "node_modules/karma/node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/karma/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -15041,7 +15027,6 @@ }, "node_modules/karma/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -15052,7 +15037,6 @@ }, "node_modules/karma/node_modules/wrap-ansi": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -15068,7 +15052,6 @@ }, "node_modules/karma/node_modules/yargs": { "version": "16.2.0", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -15085,7 +15068,6 @@ }, "node_modules/karma/node_modules/yargs-parser": { "version": "20.2.9", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -15349,7 +15331,6 @@ }, "node_modules/log4js": { "version": "6.9.1", - "dev": true, "license": "Apache-2.0", "dependencies": { "date-format": "^4.0.14", @@ -15564,7 +15545,6 @@ }, "node_modules/mime": { "version": "2.6.0", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -15602,7 +15582,6 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -15613,7 +15592,6 @@ }, "node_modules/minimist": { "version": "1.2.8", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15635,7 +15613,6 @@ }, "node_modules/mkdirp": { "version": "0.5.6", - "dev": true, "license": "MIT", "dependencies": { "minimist": "^1.2.6" @@ -16436,7 +16413,6 @@ }, "node_modules/normalize-path": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16485,7 +16461,6 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16653,7 +16628,6 @@ }, "node_modules/once": { "version": "1.4.0", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -16970,7 +16944,6 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17074,7 +17047,6 @@ }, "node_modules/picomatch": { "version": "2.3.1", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -17459,7 +17431,6 @@ }, "node_modules/qjobs": { "version": "1.2.0", - "dev": true, "license": "MIT", "engines": { "node": ">=0.9" @@ -17718,7 +17689,6 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -17877,7 +17847,6 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17893,7 +17862,6 @@ }, "node_modules/requires-port": { "version": "1.0.0", - "dev": true, "license": "MIT" }, "node_modules/resolve": { @@ -17984,7 +17952,6 @@ }, "node_modules/rfdc": { "version": "1.4.1", - "dev": true, "license": "MIT" }, "node_modules/rgb2hex": { @@ -17994,7 +17961,6 @@ }, "node_modules/rimraf": { "version": "3.0.2", - "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -18008,7 +17974,6 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -18734,7 +18699,6 @@ }, "node_modules/socket.io": { "version": "4.8.0", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", @@ -18751,7 +18715,6 @@ }, "node_modules/socket.io-adapter": { "version": "2.5.5", - "dev": true, "license": "MIT", "dependencies": { "debug": "~4.3.4", @@ -18760,7 +18723,6 @@ }, "node_modules/socket.io-parser": { "version": "4.2.4", - "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", @@ -18811,7 +18773,6 @@ }, "node_modules/source-map": { "version": "0.6.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -19072,7 +19033,6 @@ }, "node_modules/streamroller": { "version": "3.1.5", - "dev": true, "license": "MIT", "dependencies": { "date-format": "^4.0.14", @@ -19085,7 +19045,6 @@ }, "node_modules/streamroller/node_modules/fs-extra": { "version": "8.1.0", - "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", @@ -19098,7 +19057,6 @@ }, "node_modules/streamroller/node_modules/jsonfile": { "version": "4.0.0", - "dev": true, "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" @@ -19106,7 +19064,6 @@ }, "node_modules/streamroller/node_modules/universalify": { "version": "0.1.2", - "dev": true, "license": "MIT", "engines": { "node": ">= 4.0.0" @@ -19146,7 +19103,6 @@ }, "node_modules/string-width": { "version": "4.2.3", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -19184,7 +19140,6 @@ }, "node_modules/string-width/node_modules/strip-ansi": { "version": "6.0.1", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -19773,7 +19728,6 @@ "version": "0.2.4", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=14.14" @@ -19794,7 +19748,6 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -20115,7 +20068,6 @@ }, "node_modules/ua-parser-js": { "version": "0.7.38", - "dev": true, "funding": [ { "type": "opencollective", @@ -20212,7 +20164,6 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -20692,7 +20643,6 @@ }, "node_modules/void-elements": { "version": "2.0.1", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21730,12 +21680,10 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.17.1", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -21762,7 +21710,6 @@ }, "node_modules/y18n": { "version": "5.0.8", - "dev": true, "license": "ISC", "engines": { "node": ">=10" diff --git a/package.json b/package.json index d7cae3218..b740aa73a 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "@babel/eslint-parser": "^7.16.5", "@babel/plugin-transform-runtime": "^7.27.4", "@babel/register": "^7.28.3", + "@chiragrupani/karma-chromium-edge-launcher": "^2.4.1", "@eslint/compat": "^1.3.1", "@types/google-publisher-tag": "^1.20250210.0", "@wdio/browserstack-service": "^9.19.1", @@ -163,6 +164,7 @@ "iab-adcom": "^1.0.6", "iab-native": "^1.0.0", "iab-openrtb": "^1.0.1", + "karma-safarinative-launcher": "^1.1.0", "klona": "^2.0.6", "live-connect-js": "^7.2.0" }, diff --git a/test/test_deps.js b/test/test_deps.js index e35e813a5..7047e775d 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -39,6 +39,8 @@ sinon.useFakeXMLHttpRequest = fakeXhr.useFakeXMLHttpRequest.bind(fakeXhr); sinon.createFakeServer = fakeServer.create.bind(fakeServer); sinon.createFakeServerWithClock = fakeServerWithClock.create.bind(fakeServerWithClock); +localStorage.clear(); + require('test/helpers/global_hooks.js'); require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); From aaacf44a8b1c66b694a46093e4061854a014dc54 Mon Sep 17 00:00:00 2001 From: Khang Vu Date: Wed, 29 Jul 2026 11:07:06 -0700 Subject: [PATCH 16/20] npm install: regenerate lockfile after #14165 cherry-pick (new karma launchers) --- package-lock.json | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 983fd23bc..c11f02183 100644 --- a/package-lock.json +++ b/package-lock.json @@ -77,7 +77,6 @@ "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.1", "karma-coverage-istanbul-reporter": "^3.0.3", - "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^2.1.0", "karma-mocha": "^2.0.1", "karma-mocha-reporter": "^2.2.5", @@ -7012,7 +7011,6 @@ "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -7035,7 +7033,6 @@ }, "node_modules/body-parser/node_modules/debug": { "version": "2.6.9", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -7045,7 +7042,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -7064,14 +7060,12 @@ }, "node_modules/body-parser/node_modules/ms": { "version": "2.0.0", - "dev": true, "license": "MIT" }, "node_modules/body-parser/node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -7087,7 +7081,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -8980,12 +8973,6 @@ "wcwidth": "^1.0.1" } }, - "node_modules/edge-launcher": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/edge-launcher/-/edge-launcher-1.2.2.tgz", - "integrity": "sha512-JcD5WBi3BHZXXVSSeEhl6sYO8g5cuynk/hifBzds2Bp4JdzCGLNMHgMCKu5DvrO1yatMgF0goFsxXRGus0yh1g==", - "dev": true - }, "node_modules/edge-paths": { "version": "3.0.5", "dev": true, @@ -14753,21 +14740,6 @@ "semver": "bin/semver" } }, - "node_modules/karma-edge-launcher": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/karma-edge-launcher/-/karma-edge-launcher-0.4.2.tgz", - "integrity": "sha512-YAJZb1fmRcxNhMIWYsjLuxwODBjh2cSHgTW/jkVmdpGguJjLbs9ZgIK/tEJsMQcBLUkO+yO4LBbqYxqgGW2HIw==", - "dev": true, - "dependencies": { - "edge-launcher": "1.2.2" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "karma": ">=0.9" - } - }, "node_modules/karma-firefox-launcher": { "version": "2.1.3", "dev": true, @@ -17478,7 +17450,6 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -17494,7 +17465,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -17515,7 +17485,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" From ed6d5dba451fc2fd74300016cc4888ba6b53fc87 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:26:36 -0500 Subject: [PATCH 17/20] Bump actions/download-artifact from 5 to 6 (#14146) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Patrick McCann --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 26e180f13..c1bb56f93 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -161,7 +161,7 @@ jobs: name: ${{ needs.build.outputs.built-key }} - name: Download coverage results - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v6 with: path: ./build/coverage pattern: coverage-partial-${{ inputs.test-cmd }}-* From b33faf555fd0cdb57f401e0403ad446e88b873a1 Mon Sep 17 00:00:00 2001 From: Khang Vu Date: Wed, 29 Jul 2026 11:42:17 -0700 Subject: [PATCH 18/20] fix: guard pageViewIdPerBidder Map init in adapterManager.ts getPageViewIdForBidder (added by cherry-picked #14051) assumed pbjsInstance.pageViewIdPerBidder was already a Map, initialized only as a side effect of src/prebid.ts loading. Isolated test bundles (and CI's per-chunk isolated webpack builds) that don't happen to also load prebid.ts crash with 'Cannot read properties of undefined (reading has)'. --- src/adapterManager.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/adapterManager.ts b/src/adapterManager.ts index 2d4283857..d627d93fc 100644 --- a/src/adapterManager.ts +++ b/src/adapterManager.ts @@ -530,6 +530,9 @@ const adapterManager = { const pbjsInstance = getGlobal(); function getPageViewIdForBidder(bidderCode: string | null): string { + if (!pbjsInstance.pageViewIdPerBidder) { + pbjsInstance.pageViewIdPerBidder = new Map(); + } if (!pbjsInstance.pageViewIdPerBidder.has(bidderCode)) { pbjsInstance.pageViewIdPerBidder.set(bidderCode, generateUUID()); } From e90ac9a2f7add177f2157bbea21a4fe5f9366729 Mon Sep 17 00:00:00 2001 From: Khang Vu Date: Wed, 29 Jul 2026 12:16:34 -0700 Subject: [PATCH 19/20] fix: use fake timers in greedySetTimeout 'can be cleared' test The test raced two real setTimeouts (0ms clear vs 5ms fire) with no guaranteed ordering, causing an intermittent failure on Safari in CI (clearTimeout losing the race under load). Switched to sinon fake timers so the clear is synchronous and deterministic before any simulated time advances. --- .../libraries/greedy/greedyPromise_spec.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/test/spec/libraries/greedy/greedyPromise_spec.js b/test/spec/libraries/greedy/greedyPromise_spec.js index c59f646ec..f5482b17f 100644 --- a/test/spec/libraries/greedy/greedyPromise_spec.js +++ b/test/spec/libraries/greedy/greedyPromise_spec.js @@ -196,18 +196,19 @@ describe('greedySetTimeout', () => { }, 10) }); - it('can be cleared', (done) => { - let cbRan = false; - const handle = greedySetTimeout(() => { - cbRan = true; - }, 5); - setTimeout(() => { + it('can be cleared', () => { + const clock = sinon.useFakeTimers(); + try { + let cbRan = false; + const handle = greedySetTimeout(() => { + cbRan = true; + }, 5); clearTimeout(handle); - setTimeout(() => { - expect(cbRan).to.be.false; - done() - }, 10) - }, 0) + clock.tick(10); + expect(cbRan).to.be.false; + } finally { + clock.restore(); + } }) }) }); From 09a56d396b6c96989b44eafd8707a576d90552bc Mon Sep 17 00:00:00 2001 From: Khang Vu Date: Wed, 29 Jul 2026 12:51:01 -0700 Subject: [PATCH 20/20] chore: ungate coveralls reporting job Matches upstream's behavior at 10.17.0, which runs coveralls unconditionally. Our fork had added 'if: false #REMOVE ONCE REPO IS PUBLIC' as a private-repo placeholder; removing it now per dev decision. --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a639397a1..c384cd29b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -118,7 +118,6 @@ jobs: coveralls: name: Update coveralls needs: [checkout, test] - if: false #REMOVE ONCE REPO IS PUBLIC runs-on: ubuntu-latest steps: - name: Checkout