Skip to content
15 changes: 11 additions & 4 deletions modules/terceptAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Comment thread
mdusmanalvi marked this conversation as resolved.
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, {
Expand Down
27 changes: 27 additions & 0 deletions modules/terceptAnalyticsAdapterTypes.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}
95 changes: 58 additions & 37 deletions test/spec/modules/terceptAnalyticsAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
};

Expand Down Expand Up @@ -163,46 +163,67 @@ 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 ────────────────────────────────────────────────────

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');
});

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');
Expand All @@ -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');
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
});
Expand All @@ -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);
});
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
Expand All @@ -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');
});
Expand All @@ -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);
});
});
Expand All @@ -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);
});
Expand All @@ -428,18 +449,18 @@ 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);
});

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));
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
});
Expand All @@ -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);
});

Expand All @@ -562,22 +583,22 @@ 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);

terceptAnalyticsAdapter.disableAnalytics();
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);
});
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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);
});
});
Expand Down
Loading