diff --git a/modules/terceptAnalyticsAdapter.js b/modules/terceptAnalyticsAdapter.js index acd99504d3..47973bc053 100644 --- a/modules/terceptAnalyticsAdapter.js +++ b/modules/terceptAnalyticsAdapter.js @@ -4,12 +4,18 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import { EVENTS } from '../src/constants.js'; +/** + * @typedef {import('./terceptAnalyticsAdapterTypes.d.ts').TerceptAnalyticsAdapterOptions} TerceptAnalyticsAdapterOptions + */ + const emptyUrl = ''; const analyticsType = 'endpoint'; -const terceptAnalyticsVersion = 'v1.0.0'; -const defaultHostName = 'us-central1-quikr-ebay.cloudfunctions.net'; +const terceptAnalyticsVersion = 'v2.0.0'; +const defaultHostName = 'b-s.tercept.com'; const defaultPathName = '/prebid-analytics'; +const DEFAULT_ANALYTICS_BATCH_TIMEOUT = 5000; +/** @type {TerceptAnalyticsAdapterOptions} */ let initOptions; // auctionId → { auctionInit, bids[], timer } — isolated per auction @@ -77,8 +83,9 @@ var terceptAnalyticsAdapter = Object.assign(adapter( } else if (eventType === EVENTS.AUCTION_END) { const auction = pendingAuctions.get(args.auctionId); if (!auction) return; - // 1.5s window to collect BID_WON, AD_RENDER_SUCCEEDED, AD_RENDER_FAILED, BIDDER_ERROR - auction.timer = setTimeout(() => flush(args.auctionId), 1500); + // configurable window (default 5s) to collect BID_WON, AD_RENDER_SUCCEEDED, AD_RENDER_FAILED, BIDDER_ERROR + const timeout = initOptions?.analyticsBatchTimeout ?? DEFAULT_ANALYTICS_BATCH_TIMEOUT; + auction.timer = setTimeout(() => flush(args.auctionId), timeout); } else if (eventType === EVENTS.BID_WON) { const { adserverAdSlot, pbAdSlot } = getAdSlotData(args.auctionId, args.adUnitCode); updateBid(args.auctionId, args.requestId, { diff --git a/modules/terceptAnalyticsAdapterTypes.d.ts b/modules/terceptAnalyticsAdapterTypes.d.ts new file mode 100644 index 0000000000..3478eee30c --- /dev/null +++ b/modules/terceptAnalyticsAdapterTypes.d.ts @@ -0,0 +1,27 @@ +export interface TerceptAnalyticsAdapterOptions { + /** + * Publisher ID assigned by Tercept. + */ + pubId: number; + + /** + * Publisher key assigned by Tercept. + */ + pubKey: number; + + /** + * Hostname of the Tercept analytics endpoint. + */ + hostName?: string; + + /** + * Path of the Tercept analytics endpoint. + */ + pathName?: string; + + /** + * Milliseconds to wait after `AUCTION_END` before flushing the batched + * event payload. + */ + analyticsBatchTimeout?: number; +} diff --git a/test/spec/modules/terceptAnalyticsAdapter_spec.js b/test/spec/modules/terceptAnalyticsAdapter_spec.js index 6d4c4b2871..8517550d83 100644 --- a/test/spec/modules/terceptAnalyticsAdapter_spec.js +++ b/test/spec/modules/terceptAnalyticsAdapter_spec.js @@ -17,8 +17,8 @@ describe('tercept analytics adapter', function () { const initOptions = { pubId: '1', - pubKey: 'ZXlKaGJHY2lPaUpJVXpJMU5pSjkuT==', - hostName: 'us-central1-quikr-ebay.cloudfunctions.net', + pubKey: 243, + hostName: 'b-s.tercept.com', pathName: '/prebid-analytics' }; @@ -163,23 +163,44 @@ describe('tercept analytics adapter', function () { // ─── Request timing ─────────────────────────────────────────────────────── describe('request timing', function () { - it('sends no request before the 1.5s timer fires', function () { + it('sends no request before the 5s timer fires', function () { emitFullAuction(); + clock.tick(4999); expect(server.requests.length).to.equal(0); }); - it('sends exactly one request per auction after 1.5s', function () { + it('sends exactly one request per auction after 5s', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(1); }); it('sends one request per auction when two auctions run concurrently', function () { emitFullAuction('auction-a'); emitFullAuction('auction-b'); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(2); }); + + it('respects a custom analyticsBatchTimeout passed in initOptions', function () { + terceptAnalyticsAdapter.disableAnalytics(); + adapterManager.enableAnalytics({ provider: 'tercept', options: { ...initOptions, analyticsBatchTimeout: 2000 } }); + + emitFullAuction(); + clock.tick(1999); + expect(server.requests.length).to.equal(0); + clock.tick(1); + expect(server.requests.length).to.equal(1); + }); + + it('treats analyticsBatchTimeout: 0 as immediate flush, not as missing', function () { + terceptAnalyticsAdapter.disableAnalytics(); + adapterManager.enableAnalytics({ provider: 'tercept', options: { ...initOptions, analyticsBatchTimeout: 0 } }); + + emitFullAuction(); + clock.tick(0); + expect(server.requests.length).to.equal(1); + }); }); // ─── Payload structure ──────────────────────────────────────────────────── @@ -187,14 +208,14 @@ describe('tercept analytics adapter', function () { describe('payload structure', function () { it('has auctionInit, bids and initOptions at top level', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const payload = JSON.parse(server.requests[0].requestBody); expect(payload).to.have.all.keys(['auctionInit', 'bids', 'initOptions']); }); it('limits auctionInit.bidderRequests to first entry only', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const { auctionInit: ai } = JSON.parse(server.requests[0].requestBody); expect(ai.bidderRequests).to.have.length(1); expect(ai.bidderRequests[0].bidderCode).to.equal('appnexus'); @@ -202,7 +223,7 @@ describe('tercept analytics adapter', function () { it('attaches host, path and search to auctionInit at send time', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const { auctionInit: ai } = JSON.parse(server.requests[0].requestBody); expect(ai.host).to.be.a('string'); expect(ai.path).to.be.a('string'); @@ -211,14 +232,14 @@ describe('tercept analytics adapter', function () { it('includes initOptions in the payload', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const payload = JSON.parse(server.requests[0].requestBody); expect(payload.initOptions).to.deep.equal(initOptions); }); it('does not include ad, native or adUrl fields in bids', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); bids.forEach(bid => { expect(bid).not.to.have.property('ad'); @@ -235,7 +256,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AUCTION_INIT, auctionInit); events.emit(EVENTS.BID_REQUESTED, bidRequested); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].bidId).to.equal(BID_ID); expect(bids[0].renderStatus).to.equal(1); @@ -250,7 +271,7 @@ describe('tercept analytics adapter', function () { describe('BID_RESPONSE', function () { it('updates bid to renderStatus 2 with response fields', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(2); expect(bids[0].cpm).to.equal(0.5); @@ -268,7 +289,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.BID_REQUESTED, bidRequested); events.emit(EVENTS.BID_TIMEOUT, [{ auctionId: AUCTION_ID, bidId: BID_ID, adUnitCode: AD_UNIT_CODE }]); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(3); }); @@ -282,7 +303,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.BID_REQUESTED, bidRequested); events.emit(EVENTS.NO_BID, { auctionId: AUCTION_ID, bidId: BID_ID, adUnitCode: AD_UNIT_CODE, bidder: 'appnexus' }); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(5); }); @@ -294,7 +315,7 @@ describe('tercept analytics adapter', function () { it('updates bid to renderStatus 4 with win fields when fired before timer', function () { emitFullAuction(); events.emit(EVENTS.BID_WON, bidWon); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(4); expect(bids[0].renderedSize).to.equal('300x250'); @@ -305,7 +326,7 @@ describe('tercept analytics adapter', function () { it('maps adserverAdSlot and pbAdSlot from adUnitMap', function () { emitFullAuction(); events.emit(EVENTS.BID_WON, bidWon); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].adserverAdSlot).to.equal('/1234567/homepage-banner'); expect(bids[0].pbAdSlot).to.equal('homepage-banner-pbadslot'); @@ -320,7 +341,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AD_RENDER_SUCCEEDED, { bid: { requestId: BID_ID, auctionId: AUCTION_ID, adUnitCode: AD_UNIT_CODE, size: '300x250' } }); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(7); expect(bids[0].renderTimestamp).to.be.a('number'); @@ -334,7 +355,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AD_RENDER_SUCCEEDED, { bid: { requestId: BID_ID, auctionId: AUCTION_ID, adUnitCode: AD_UNIT_CODE, size: '300x250' } }); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].adserverAdSlot).to.equal('/1234567/homepage-banner'); expect(bids[0].pbAdSlot).to.equal('homepage-banner-pbadslot'); @@ -351,7 +372,7 @@ describe('tercept analytics adapter', function () { reason: 'exception', message: 'Cannot read property of undefined' }); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(8); expect(bids[0].reason).to.equal('exception'); @@ -375,7 +396,7 @@ describe('tercept analytics adapter', function () { error: { message: 'Network error' } }); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].renderStatus).to.equal(6); expect(bids[0].status).to.equal('bidError'); @@ -394,7 +415,7 @@ describe('tercept analytics adapter', function () { error: 'timeout' }); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].error).to.equal('timeout'); }); @@ -403,7 +424,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AUCTION_INIT, auctionInit); events.emit(EVENTS.BIDDER_ERROR, { bidderRequest: null, error: 'err' }); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(1); }); }); @@ -413,7 +434,7 @@ describe('tercept analytics adapter', function () { describe('is_pl flag', function () { it('sets is_pl true only on the first bid of the first auction', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].is_pl).to.equal(true); }); @@ -428,7 +449,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.BID_REQUESTED, bidRequested); events.emit(EVENTS.BID_REQUESTED, bidRequested2); events.emit(EVENTS.AUCTION_END, auctionEnd); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].is_pl).to.equal(true); expect(bids[1].is_pl).to.equal(false); @@ -436,10 +457,10 @@ describe('tercept analytics adapter', function () { it('sets is_pl false on all bids of subsequent auctions', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); emitFullAuction('auction-2'); - clock.tick(1500); + clock.tick(5000); const p2 = JSON.parse(server.requests[1].requestBody); p2.bids.forEach(bid => expect(bid.is_pl).to.equal(false)); @@ -467,7 +488,7 @@ describe('tercept analytics adapter', function () { }); events.emit(EVENTS.AUCTION_END, { auctionId: id1 }); events.emit(EVENTS.AUCTION_END, { auctionId: id2 }); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(2); const p1 = JSON.parse(server.requests[0].requestBody); @@ -497,7 +518,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.BID_WON, { ...bidWon, auctionId: id1, requestId: 'bid-x' }); events.emit(EVENTS.AUCTION_END, { auctionId: id1 }); events.emit(EVENTS.AUCTION_END, { auctionId: id2 }); - clock.tick(1500); + clock.tick(5000); const p1 = JSON.parse(server.requests[0].requestBody); const p2 = JSON.parse(server.requests[1].requestBody); @@ -539,7 +560,7 @@ describe('tercept analytics adapter', function () { Object.defineProperty(document, 'visibilityState', { get: () => 'hidden', configurable: true }); document.dispatchEvent(new Event('visibilitychange')); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(0); }); }); @@ -551,7 +572,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.AUCTION_INIT, auctionInit); events.emit(EVENTS.AUCTION_END, auctionEnd); terceptAnalyticsAdapter.disableAnalytics(); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(0); }); @@ -562,14 +583,14 @@ describe('tercept analytics adapter', function () { adapterManager.enableAnalytics({ provider: 'tercept', options: initOptions }); emitFullAuction(); - clock.tick(1500); + clock.tick(5000); // only one request — from the fresh enable, not from before disable expect(server.requests.length).to.equal(1); }); it('resets firstSent so re-enabled adapter marks first auction as page load', function () { emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const p1 = JSON.parse(server.requests[0].requestBody); expect(p1.bids[0].is_pl).to.equal(true); @@ -577,7 +598,7 @@ describe('tercept analytics adapter', function () { adapterManager.enableAnalytics({ provider: 'tercept', options: initOptions }); emitFullAuction(); - clock.tick(1500); + clock.tick(5000); const p2 = JSON.parse(server.requests[1].requestBody); expect(p2.bids[0].is_pl).to.equal(true); }); @@ -599,7 +620,7 @@ describe('tercept analytics adapter', function () { bids: [{ ...bidRequested.bids[0], auctionId: 'no-ortb2' }] }); events.emit(EVENTS.AUCTION_END, { auctionId: 'no-ortb2' }); - clock.tick(1500); + clock.tick(5000); const { bids } = JSON.parse(server.requests[0].requestBody); expect(bids[0].adserverAdSlot).to.be.undefined; expect(bids[0].pbAdSlot).to.be.undefined; @@ -617,7 +638,7 @@ describe('tercept analytics adapter', function () { events.emit(EVENTS.BID_WON, { ...bidWon, auctionId: id2, requestId: 'r2' }); events.emit(EVENTS.AUCTION_END, { auctionId: id1 }); events.emit(EVENTS.AUCTION_END, { auctionId: id2 }); - clock.tick(1500); + clock.tick(5000); // BID_WON updates are stored on pending bids; but since no BID_REQUESTED was emitted // there are no bids to update — this test confirms slot lookup is isolated per auctionId @@ -647,7 +668,7 @@ describe('tercept analytics adapter', function () { it('still flushes other auctions when one auction has unknown events applied', function () { emitFullAuction(); events.emit(EVENTS.BID_RESPONSE, { ...bidResponse, auctionId: 'ghost-auction' }); - clock.tick(1500); + clock.tick(5000); expect(server.requests.length).to.equal(1); }); });