From f659f0bd968be4f11cf81901259b95f44d957c1f Mon Sep 17 00:00:00 2001 From: eknis Date: Thu, 14 May 2026 14:10:47 +0900 Subject: [PATCH 1/3] IntimateMerger Analytics Adapter : fix cache --- modules/imAnalyticsAdapter.js | 3 +-- test/spec/modules/imAnalyticsAdapter_spec.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/modules/imAnalyticsAdapter.js b/modules/imAnalyticsAdapter.js index 4c34aa71f6d..1b63c0cd6d1 100644 --- a/modules/imAnalyticsAdapter.js +++ b/modules/imAnalyticsAdapter.js @@ -256,7 +256,6 @@ const imAnalyticsAdapter = Object.assign( auction.wonBidsTimer = null; if (auction.wonBids.length === 0) { - delete cache.auctions[auctionId]; return; } @@ -264,7 +263,7 @@ const imAnalyticsAdapter = Object.assign( const ts = auction.auctionInitTimestamp || Date.now(); const bids = auction.wonBids; const uid = auction.imUid; - delete cache.auctions[auctionId]; + auction.wonBids = []; sendToApi(buildApiUrlWithOptions(this.options, 'won', auctionId), { bids, ts, diff --git a/test/spec/modules/imAnalyticsAdapter_spec.js b/test/spec/modules/imAnalyticsAdapter_spec.js index 06060575102..241e18525aa 100644 --- a/test/spec/modules/imAnalyticsAdapter_spec.js +++ b/test/spec/modules/imAnalyticsAdapter_spec.js @@ -203,7 +203,7 @@ describe('imAnalyticsAdapter', function() { expect(requests[0].url).to.include('/won'); }); - it('should drop BID_WON for an auction whose cache entry has been cleaned up', function() { + it('should send subsequent BID_WON immediately after initial batch', function() { const clock = sandbox.useFakeTimers(); imAnalyticsAdapter.track({ @@ -222,17 +222,16 @@ describe('imAnalyticsAdapter', function() { args: { auctionId: 'auc-1' } }); - // initial batch sends and deletes cache entry clock.tick(BID_WON_TIMEOUT + 10); expect(requests.length).to.equal(1); - // BID_WON after cache cleanup is dropped + // subsequent BID_WON sent immediately via lightweight cache state imAnalyticsAdapter.track({ eventType: EVENTS.BID_WON, args: { ...bidWonArgs, requestId: 'req-2' } }); - expect(requests.length).to.equal(1); + expect(requests.length).to.equal(2); }); it('should deduplicate won bids with same requestId', function() { @@ -347,7 +346,7 @@ describe('imAnalyticsAdapter', function() { expect(requests.length).to.equal(0); }); - it('should drop BID_WON that arrives after timer fired with no bids', function() { + it('should send BID_WON immediately when it arrives after timer fired with no bids', function() { const clock = sandbox.useFakeTimers(); imAnalyticsAdapter.track({ @@ -361,17 +360,18 @@ describe('imAnalyticsAdapter', function() { args: { auctionId: 'auc-1' } }); - // timer fires with no bids, cache entry is deleted + // timer fires with no bids, lightweight cache state is kept clock.tick(BID_WON_TIMEOUT + 10); expect(requests.length).to.equal(0); - // late BID_WON is dropped after cache cleanup + // late BID_WON sent immediately via lightweight cache state imAnalyticsAdapter.track({ eventType: EVENTS.BID_WON, args: { ...bidWonArgs, requestId: 'req-1' } }); - expect(requests.length).to.equal(0); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.include('/won'); }); }); }); From 22eaa7674a4ffdca9931d8576741107f7d61c532 Mon Sep 17 00:00:00 2001 From: eknis Date: Tue, 9 Jun 2026 19:00:29 +0900 Subject: [PATCH 2/3] IntimateMerger Analytics Adapter : delete old auction --- modules/imAnalyticsAdapter.js | 24 ++++ modules/imAnalyticsAdapter.md | 5 +- test/spec/modules/imAnalyticsAdapter_spec.js | 123 +++++++++++++++++++ 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/modules/imAnalyticsAdapter.js b/modules/imAnalyticsAdapter.js index 1b63c0cd6d1..baea095c015 100644 --- a/modules/imAnalyticsAdapter.js +++ b/modules/imAnalyticsAdapter.js @@ -6,6 +6,7 @@ import { sendBeacon } from '../src/ajax.js'; const DEFAULT_BID_WON_TIMEOUT = 1500; // 1.5 second for initial batch const DEFAULT_CID = 5126; +const DEFAULT_CACHE_TTL = 30 * 1000; // 30 seconds const API_BASE_URL = 'https://b6.im-apps.net/bid'; const cache = { @@ -33,6 +34,16 @@ function getWaitTimeout(options) { : DEFAULT_BID_WON_TIMEOUT; } +/** + * Get cache TTL from adapter options + * @param {Object} options - Adapter options + * @returns {number} TTL in ms + */ +function getTtl(options) { + const ttl = options && options.cacheTtl; + return (typeof ttl === 'number' && ttl > 0) ? ttl : DEFAULT_CACHE_TTL; +} + /** * Build API URL with CID from options * @param {Object} options - Adapter options @@ -151,8 +162,21 @@ const imAnalyticsAdapter = Object.assign( * @param {Object} args - Auction arguments */ handleAuctionInit(args) { + const now = Date.now(); + const ttl = getTtl(this.options); + Object.keys(cache.auctions).forEach(id => { + const entry = cache.auctions[id]; + if (now - (entry.auctionInitTimestamp || 0) > ttl) { + clearTimer(entry.wonBidsTimer); + delete cache.auctions[id]; + } + }); + const consentData = getConsentData(); const imUid = deepAccess(args.bidderRequests, '0.bids.0.userId.imuid') ?? ''; + if (cache.auctions[args.auctionId]) { + clearTimer(cache.auctions[args.auctionId].wonBidsTimer); + } cache.auctions[args.auctionId] = { imUid, consentData, diff --git a/modules/imAnalyticsAdapter.md b/modules/imAnalyticsAdapter.md index 72cfbe19666..676ce0e47bc 100644 --- a/modules/imAnalyticsAdapter.md +++ b/modules/imAnalyticsAdapter.md @@ -26,6 +26,7 @@ By enabling this adapter, you agree to Intimate Merger's privacy policy at |-----------|-------|------|---------|-------------| | `cid` | optional | number | 5126 | The Customer ID provided by Intimate Merger. | | `waitTimeout` | optional | number | 1500 | Wait time in milliseconds before sending batched requests. (Default: 1500) | +| `cacheTtl` | optional | number | 30000 | Time in milliseconds before stale auction cache entries are removed. (Default: 30000) | #### Example Configuration @@ -36,7 +37,9 @@ pbjs.enableAnalytics({ /* Optional: Customer ID */ cid: 5126, /* Optional: Wait 2 seconds */ - waitTimeout: 2000 + waitTimeout: 2000, + /* Optional: Remove stale auction cache after 60 seconds */ + cacheTtl: 60000 } }); ``` diff --git a/test/spec/modules/imAnalyticsAdapter_spec.js b/test/spec/modules/imAnalyticsAdapter_spec.js index 241e18525aa..363879e8c73 100644 --- a/test/spec/modules/imAnalyticsAdapter_spec.js +++ b/test/spec/modules/imAnalyticsAdapter_spec.js @@ -171,6 +171,129 @@ describe('imAnalyticsAdapter', function() { expect(payload.consent.gpp).to.equal('7'); expect(payload.consent.gppStr).to.equal('gpp-string'); }); + + it('should cancel existing timer when same auctionId receives duplicate AUCTION_INIT', function() { + const clock = sandbox.useFakeTimers(); + + // 1回目の AUCTION_INIT + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-dup', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + imAnalyticsAdapter.track({ + eventType: EVENTS.BID_WON, + args: { ...bidWonArgs, auctionId: 'auc-dup', requestId: 'req-1' } + }); + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_END, + args: { auctionId: 'auc-dup' } + }); + requests = []; + + // タイマー発火前に同一 auctionId で AUCTION_INIT が再度来る + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-dup', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + requests = []; + + // 古いタイマーがキャンセルされているので won リクエストは来ない + clock.tick(BID_WON_TIMEOUT + 10); + expect(requests.length).to.equal(0); + }); + + it('should remove stale auction entries that exceed TTL on AUCTION_INIT', function() { + const clock = sandbox.useFakeTimers(); + + // 1つ目のオークションを開始 + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-stale', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + requests = []; + + // デフォルト TTL (30秒) を超過させる + clock.tick(30 * 1000 + 1); + + // 2つ目のオークションを開始 → auc-stale が削除されるはず + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-new', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + + // TTL 超過エントリ削除後に BID_WON が届いても無視される + imAnalyticsAdapter.track({ + eventType: EVENTS.BID_WON, + args: { ...bidWonArgs, auctionId: 'auc-stale', requestId: 'req-stale' } + }); + + expect(requests.length).to.equal(1); // auc-new の pv のみ + }); + + it('should keep entries within TTL on AUCTION_INIT', function() { + const clock = sandbox.useFakeTimers(); + + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-active', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + requests = []; + + // TTL 未満しか経過させない + clock.tick(10 * 1000); + + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-new2', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + + // auc-active はまだ生きているので BID_WON が処理される + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_END, + args: { auctionId: 'auc-active' } + }); + imAnalyticsAdapter.track({ + eventType: EVENTS.BID_WON, + args: { ...bidWonArgs, auctionId: 'auc-active', requestId: 'req-active' } + }); + + clock.tick(BID_WON_TIMEOUT + 10); + const wonRequests = requests.filter(r => r.url.includes('/won')); + expect(wonRequests.length).to.equal(1); + }); + + it('should use cacheTtl from options when provided', function() { + // cacheTtl を 5秒に設定 + imAnalyticsAdapter.disableAnalytics(); + imAnalyticsAdapter.enableAnalytics({ + provider: 'imAnalytics', + options: { cid: 5126, cacheTtl: 5 * 1000 } + }); + + const clock = sandbox.useFakeTimers(); + + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-custom-ttl', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + requests = []; + + // 5秒を超過させる(デフォルトの30秒は未満) + clock.tick(5 * 1000 + 1); + + imAnalyticsAdapter.track({ + eventType: EVENTS.AUCTION_INIT, + args: { auctionId: 'auc-after', timestamp: clock.now, bidderRequests: [], adUnits: [] } + }); + + // auc-custom-ttl は削除されているはず + imAnalyticsAdapter.track({ + eventType: EVENTS.BID_WON, + args: { ...bidWonArgs, auctionId: 'auc-custom-ttl', requestId: 'req-custom' } + }); + + expect(requests.length).to.equal(1); // auc-after の pv のみ + }); }); describe('BID_WON', function() { From 5b56dc4c8fc2ceb54c552fccd57b80f2a15847c2 Mon Sep 17 00:00:00 2001 From: eknis Date: Tue, 9 Jun 2026 19:09:38 +0900 Subject: [PATCH 3/3] IntimateMerger Analytics Adapter : fix comment --- test/spec/modules/imAnalyticsAdapter_spec.js | 28 ++++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/test/spec/modules/imAnalyticsAdapter_spec.js b/test/spec/modules/imAnalyticsAdapter_spec.js index 363879e8c73..91ae073e550 100644 --- a/test/spec/modules/imAnalyticsAdapter_spec.js +++ b/test/spec/modules/imAnalyticsAdapter_spec.js @@ -175,7 +175,7 @@ describe('imAnalyticsAdapter', function() { it('should cancel existing timer when same auctionId receives duplicate AUCTION_INIT', function() { const clock = sandbox.useFakeTimers(); - // 1回目の AUCTION_INIT + // first AUCTION_INIT imAnalyticsAdapter.track({ eventType: EVENTS.AUCTION_INIT, args: { auctionId: 'auc-dup', timestamp: clock.now, bidderRequests: [], adUnits: [] } @@ -190,14 +190,14 @@ describe('imAnalyticsAdapter', function() { }); requests = []; - // タイマー発火前に同一 auctionId で AUCTION_INIT が再度来る + // duplicate AUCTION_INIT for the same auctionId arrives before timer fires imAnalyticsAdapter.track({ eventType: EVENTS.AUCTION_INIT, args: { auctionId: 'auc-dup', timestamp: clock.now, bidderRequests: [], adUnits: [] } }); requests = []; - // 古いタイマーがキャンセルされているので won リクエストは来ない + // old timer should be cancelled, so no won request is sent clock.tick(BID_WON_TIMEOUT + 10); expect(requests.length).to.equal(0); }); @@ -205,29 +205,29 @@ describe('imAnalyticsAdapter', function() { it('should remove stale auction entries that exceed TTL on AUCTION_INIT', function() { const clock = sandbox.useFakeTimers(); - // 1つ目のオークションを開始 + // start the first auction imAnalyticsAdapter.track({ eventType: EVENTS.AUCTION_INIT, args: { auctionId: 'auc-stale', timestamp: clock.now, bidderRequests: [], adUnits: [] } }); requests = []; - // デフォルト TTL (30秒) を超過させる + // advance past the default TTL (30 seconds) clock.tick(30 * 1000 + 1); - // 2つ目のオークションを開始 → auc-stale が削除されるはず + // start a second auction, which should trigger removal of auc-stale imAnalyticsAdapter.track({ eventType: EVENTS.AUCTION_INIT, args: { auctionId: 'auc-new', timestamp: clock.now, bidderRequests: [], adUnits: [] } }); - // TTL 超過エントリ削除後に BID_WON が届いても無視される + // BID_WON for the removed stale entry should be ignored imAnalyticsAdapter.track({ eventType: EVENTS.BID_WON, args: { ...bidWonArgs, auctionId: 'auc-stale', requestId: 'req-stale' } }); - expect(requests.length).to.equal(1); // auc-new の pv のみ + expect(requests.length).to.equal(1); // only the pv for auc-new }); it('should keep entries within TTL on AUCTION_INIT', function() { @@ -239,7 +239,7 @@ describe('imAnalyticsAdapter', function() { }); requests = []; - // TTL 未満しか経過させない + // advance less than the TTL clock.tick(10 * 1000); imAnalyticsAdapter.track({ @@ -247,7 +247,7 @@ describe('imAnalyticsAdapter', function() { args: { auctionId: 'auc-new2', timestamp: clock.now, bidderRequests: [], adUnits: [] } }); - // auc-active はまだ生きているので BID_WON が処理される + // auc-active is still alive, so BID_WON should be processed imAnalyticsAdapter.track({ eventType: EVENTS.AUCTION_END, args: { auctionId: 'auc-active' } @@ -263,7 +263,7 @@ describe('imAnalyticsAdapter', function() { }); it('should use cacheTtl from options when provided', function() { - // cacheTtl を 5秒に設定 + // set cacheTtl to 5 seconds imAnalyticsAdapter.disableAnalytics(); imAnalyticsAdapter.enableAnalytics({ provider: 'imAnalytics', @@ -278,7 +278,7 @@ describe('imAnalyticsAdapter', function() { }); requests = []; - // 5秒を超過させる(デフォルトの30秒は未満) + // advance past 5 seconds (still within the default 30s TTL) clock.tick(5 * 1000 + 1); imAnalyticsAdapter.track({ @@ -286,13 +286,13 @@ describe('imAnalyticsAdapter', function() { args: { auctionId: 'auc-after', timestamp: clock.now, bidderRequests: [], adUnits: [] } }); - // auc-custom-ttl は削除されているはず + // auc-custom-ttl should have been removed imAnalyticsAdapter.track({ eventType: EVENTS.BID_WON, args: { ...bidWonArgs, auctionId: 'auc-custom-ttl', requestId: 'req-custom' } }); - expect(requests.length).to.equal(1); // auc-after の pv のみ + expect(requests.length).to.equal(1); // only the pv for auc-after }); });