diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 150e9d3c5c2..1367c4e546c 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,5 +1,5 @@ import {config} from '../src/config.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {deepAccess, generateUUID, logError, isArray} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,8 +7,8 @@ import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'insticator'; const ENDPOINT = 'https://ex.ingage.tech/v1/openrtb'; // production endpoint -const USER_ID_KEY = 'hb_insticator_uid'; -const USER_ID_COOKIE_EXP = 2592000000; // 30 days +const USER_ID_KEY = 'instUid'; +const USER_ID_COOKIE_EXP = 7776000000; // 90 days const BID_TTL = 300; // 5 minutes const GVLID = 910; @@ -22,35 +22,77 @@ config.setDefaults({ }); function getUserId() { - let uid; - - if (storage.localStorageIsEnabled()) { - uid = storage.getDataFromLocalStorage(USER_ID_KEY); - } else { - uid = storage.getCookie(USER_ID_KEY); + let uid = storage.getCookie(USER_ID_KEY); + if (uid && isUserIdValid(uid)) { + const expireIn = new Date(Date.now() + USER_ID_COOKIE_EXP).toUTCString(); + const domain = window.location.hostname.match(/[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$/mg); + storage.setCookie(USER_ID_KEY, uid, expireIn, 'none', `.${domain}`); + return uid; } - if (uid && uid.length !== 36) { - uid = undefined; + return generateUserId() +} + +function isUserIdValid(uid) { + return uid && uid.length === 36; +} + +function generateUserId() { + const uid = generateUUID(); + + if (isCookieEnabled()) { + const expireIn = new Date(Date.now() + USER_ID_COOKIE_EXP).toUTCString(); + const domain = window.location.hostname.match(/[^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$/mg); + storage.setCookie(USER_ID_KEY, uid, expireIn, 'none', `.${domain}`); } return uid; } -function setUserId(userId) { - if (storage.localStorageIsEnabled()) { - storage.setDataInLocalStorage(USER_ID_KEY, userId); - } +function isCookieEnabled() { + let enabled = false; - if (storage.cookiesAreEnabled()) { - const expires = new Date(Date.now() + USER_ID_COOKIE_EXP).toUTCString(); - storage.setCookie(USER_ID_KEY, userId, expires); + try { + const expireIn = new Date(Date.now() + USER_ID_COOKIE_EXP).toUTCString(); + storage.setCookie('insticator.prebid.cookieTest', 'true', expireIn); + enabled = Boolean(storage.getCookie('insticator.prebid.cookieTest')); + } catch (err) { + return false; + } finally { + if (enabled) { + storage.setCookie('insticator.prebid.cookieTest', 'true', new Date(Date.now()).toUTCString()); + } } + + return enabled; } -function buildBanner(bidRequest) { +function buildImpression(bidRequest) { const format = []; - const pos = deepAccess(bidRequest, 'mediaTypes.banner.pos'); + const ext = { + insticator: { + adUnitId: bidRequest.params.adUnitId, + adUnitName: bidRequest.params.adUnitName, + }, + } + + /** + * set impression type using header bidding wrapper's API + * this is Insticator header bidding wrapper specific + */ + // eslint-disable-next-line no-undef + if (bidRequest.adUnitCode && Insticator.getAdUnitStates) { + try { + // eslint-disable-next-line no-undef + const adUnits = Insticator.getAdUnitStates(); + const adUnit = adUnits[bidRequest.adUnitCode] + // eslint-disable-next-line no-undef + if (adUnit) ext.insticator.impressionType = adUnit.timesRefreshed > 0 ? adUnit.refreshType : 'il'; + } catch (e) { + console.warn(e) + } + } + const sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes; @@ -61,49 +103,27 @@ function buildBanner(bidRequest) { }); } - return { - format, - pos, + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + + if (gpid) { + ext.gpid = gpid; } -} -function buildVideo(bidRequest) { - const w = deepAccess(bidRequest, 'mediaTypes.video.w'); - const h = deepAccess(bidRequest, 'mediaTypes.video.h'); - const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); - const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const instl = deepAccess(bidRequest, 'ortb2Imp.instl') + const secure = location.protocol === 'https:' ? 1 : 0; + const pos = deepAccess(bidRequest, 'mediaTypes.banner.pos'); return { - placement, - mimes, - w, - h, - } -} - -function buildImpression(bidRequest) { - const imp = { id: bidRequest.bidId, tagid: bidRequest.adUnitCode, - instl: deepAccess(bidRequest, 'ortb2Imp.instl'), - secure: location.protocol === 'https:' ? 1 : 0, - ext: { - gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid'), - insticator: { - adUnitId: bidRequest.params.adUnitId, - }, + instl, + secure, + banner: { + format, + pos, }, - } - - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - imp.banner = buildBanner(bidRequest); - } - - if (deepAccess(bidRequest, 'mediaTypes.video')) { - imp.video = buildVideo(bidRequest); - } - - return imp; + ext, + }; } function buildDevice() { @@ -112,10 +132,7 @@ function buildDevice() { w: window.innerWidth, h: window.innerHeight, js: true, - ext: { - localStorage: storage.localStorageIsEnabled(), - cookies: storage.cookiesAreEnabled(), - }, + ext: {}, }; if (typeof deviceConfig === 'object') { @@ -139,12 +156,10 @@ function buildRegs(bidderRequest) { } function buildUser(bid) { - const userId = getUserId() || generateUUID(); + const userId = getUserId(); const yob = deepAccess(bid, 'params.user.yob') const gender = deepAccess(bid, 'params.user.gender') - setUserId(userId); - return { id: userId, yob, @@ -179,10 +194,9 @@ function buildRequest(validBidRequests, bidderRequest) { tid: bidderRequest.auctionId, }, site: { - // TODO: are these the right refererInfo values? - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, + domain: location.hostname, + page: location.href, + ref: bidderRequest.refererInfo.referer, }, device: buildDevice(), regs: buildRegs(bidderRequest), @@ -270,96 +284,40 @@ function validateSizes(sizes) { ); } -function validateAdUnitId(bid) { - if (!bid.params.adUnitId) { - logError('insticator: missing adUnitId bid parameter'); - return false; - } - - return true; -} - -function validateMediaType(bid) { - if (!(BANNER in bid.mediaTypes || VIDEO in bid.mediaTypes)) { - logError('insticator: expected banner or video in mediaTypes'); - return false; - } - - return true; -} - -function validateBanner(bid) { - const banner = deepAccess(bid, 'mediaTypes.banner'); - - if (banner === undefined) { - return true; - } - - if ( - !validateSizes(bid.sizes) && - !validateSizes(bid.mediaTypes.banner.sizes) - ) { - logError('insticator: banner sizes not specified or invalid'); - return false; - } - - return true; -} - -function validateVideo(bid) { - const video = deepAccess(bid, 'mediaTypes.video'); - - if (video === undefined) { - return true; - } - - const videoSize = [ - deepAccess(bid, 'mediaTypes.video.w'), - deepAccess(bid, 'mediaTypes.video.h'), - ]; - - if ( - !validateSize(videoSize) - ) { - logError('insticator: video size not specified or invalid'); - return false; - } - - const mimes = deepAccess(bid, 'mediaTypes.video.mimes'); - - if (!Array.isArray(mimes) || mimes.length === 0) { - logError('insticator: mimes not specified'); - return false; - } - - const placement = deepAccess(bid, 'mediaTypes.video.placement'); - - if (typeof placement !== 'undefined' && typeof placement !== 'number') { - logError('insticator: video placement is not a number'); - return false; - } - - return true; -} - export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [ BANNER, VIDEO ], + supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { - return ( - validateAdUnitId(bid) && - validateMediaType(bid) && - validateBanner(bid) && - validateVideo(bid) - ); + if (!bid.params.adUnitId) { + logError('insticator: missing adUnitId bid parameter'); + return false; + } + + if (!(BANNER in bid.mediaTypes)) { + logError('insticator: expected banner in mediaTypes'); + return false; + } + + if ( + !validateSizes(bid.sizes) && + !validateSizes(bid.mediaTypes.banner.sizes) + ) { + logError('insticator: banner sizes not specified or invalid'); + return false; + } + + return true; }, buildRequests: function (validBidRequests, bidderRequest) { const requests = []; let endpointUrl = config.getConfig('insticator.endpointUrl') || ENDPOINT; - endpointUrl = endpointUrl.replace(/^http:/, 'https:'); + + if (endpointUrl.indexOf('localhost') === -1) { + endpointUrl = endpointUrl.replace(/^http:/, 'https:'); + } if (validBidRequests.length > 0) { requests.push({ diff --git a/modules/insticatorV2AnalyticsAdapter.js b/modules/insticatorV2AnalyticsAdapter.js new file mode 100644 index 00000000000..8b18c1aa19f --- /dev/null +++ b/modules/insticatorV2AnalyticsAdapter.js @@ -0,0 +1,319 @@ +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import CONSTANTS from '../src/constants.json' +import * as utils from '../src/utils.js' +import { ajax } from '../src/ajax.js' + +const baseUrl = 'https://analysis.ingage.tech/' + +const ENDPOINTS = { + AD_RENDER_FAILED: baseUrl + 'com.snowplowanalytics.iglu/v1?schema=iglu%3Acom.insticator%2Frender_failed%2Fjsonschema%2F1-0-0', + AD_RENDER_SUCCEEDED: baseUrl + 'com.snowplowanalytics.iglu/v1?schema=iglu%3Acom.insticator%2Frender_succeeded%2Fjsonschema%2F2-0-0', + BID_WON: baseUrl + 'com.snowplowanalytics.iglu/v1?schema=iglu%3Acom.insticator%2Fbid_won%2Fjsonschema%2F2-0-0', + AUCTION_END: baseUrl + 'com.snowplowanalytics.iglu/v1?schema=iglu%3Acom.insticator%2Fauction_end%2Fjsonschema%2F1-0-0' +} + +const analyticsType = 'endpoint' +const ADAPTER_CODE = 'insticatorV2' + +const { + AUCTION_INIT, + BID_REQUESTED, + BID_RESPONSE, + BID_WON, + AUCTION_END, + AD_RENDER_FAILED, + AD_RENDER_SUCCEEDED +} = CONSTANTS.EVENTS + +const SERVER_EVENTS = { + AD_RENDER_FAILED: 'adRenderFailed', + AD_RENDER_SUCCEEDED: 'adRenderSucceeded', + WON: 'bidWon', + AUCTION_END: 'auctionEnd' +} + +const SERVER_BID_STATUS = { + BID_REQUESTED: 'bidRequested', + BID_RECEIVED: 'bidReceived', + BID_WON: 'bidWon', + AUCTION_END: 'auctionEnd' +} + +let auctions = {} + +const onAuctionInit = (args) => { + const { auctionId, adUnits, timestamp } = args + + let auction = auctions[auctionId] = { + ...args, + adUnits: {}, + auctionStart: timestamp + } + + utils._each(adUnits, adUnit => { + auction.adUnits[adUnit.code] = { + ...adUnit, + auctionId, + adunid: adUnit.code, + bids: {}, + } + }) +} + +const onBidRequested = (args) => { + const { auctionId, bids, start, timeout } = args + const _start = start || Date.now() + const auction = auctions[auctionId] + const auctionAdUnits = auction.adUnits + + bids.forEach(bid => { + const { adUnitCode } = bid + const bidId = parseBidId(bid) + + auctionAdUnits[adUnitCode].bids[bidId] = { + ...bid, + timeout, + start: _start, + rs: _start - auction.auctionStart, + bidStatus: SERVER_BID_STATUS.BID_REQUESTED, + } + }) +} + +const onBidResponse = (args) => { + const { auctionId, adUnitCode } = args + const auction = auctions[auctionId] + const bidId = parseBidId(args) + let bid = auction.adUnits[adUnitCode].bids[bidId] + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_RECEIVED, + end: args.responseTimestamp, + re: args.responseTimestamp - auction.auctionStart + }) +} + +const onAuctionEnd = (args) => { + const { auctionId, auctionStatus, auctionEnd } = args + let auction = auctions[args.auctionId]; + + for (const key in auction.adUnits) { + auction.adUnits[key].bids = Object.values(auction.adUnits[key].bids).map((bid) => { return mapBid(bid) }); + } + + const payload = { + auctionId, + auctionDuration: auctionEnd - auction.auctionStart, + auctionStatus, + adUnits: Object.values(auction.adUnits) + } + sendEvent(SERVER_EVENTS.AUCTION_END, payload) +} + +const onBidWon = (args) => { + const { auctionId, adUnitCode } = args + const bidId = parseBidId(args) + const bid = auctions[auctionId].adUnits[adUnitCode].bids.find((bid) => { + return bid.bidId == bidId; + }); + + Object.assign(bid, args, { + bidStatus: SERVER_BID_STATUS.BID_WON, + isW: true, + isH: true + }) + + const payload = { + auctionId, + adunid: adUnitCode, + bid: mapBid(bid, BID_WON) + } + sendEvent(SERVER_EVENTS.WON, payload) +} + +const onAdRenderFailed = (args) => { + const { bid } = args + let data = { + timestamp: Date.now() + } + + if (bid) { + data.bid = mapBid(bid, AD_RENDER_FAILED) + } + + sendEvent(SERVER_EVENTS.AD_RENDER_FAILED, data) +} + +const onAdRenderSucceeded = (args) => { + const { bid } = args + let data = { + timestamp: Date.now() + } + + if (bid) { + data.bid = mapBid(bid, AD_RENDER_SUCCEEDED) + } + + sendEvent(SERVER_EVENTS.AD_RENDER_SUCCEEDED, data) +} + +var insticatorAdapter = Object.assign( + adapter({ analyticsType }), { + track({ eventType, args }) { + handleEvent(eventType, args) + } + } +) + +function handleEvent(eventType, args) { + switch (eventType) { + case AUCTION_INIT: + onAuctionInit(args) + break + case BID_REQUESTED: + onBidRequested(args) + break + case BID_RESPONSE: + onBidResponse(args) + break + case BID_WON: + onBidWon(args) + break + case AUCTION_END: + onAuctionEnd(args) + break + case AD_RENDER_FAILED: + onAdRenderFailed(args) + break + case AD_RENDER_SUCCEEDED: + onAdRenderSucceeded(args) + break + } +} + +function sendEvent(eventType, data) { + let payload = { + eventType, + domain: window.location.hostname + } + if (data.bid) { + payload.bid = data.bid + } + if (data.timestamp) { + payload.timestamp = data.timestamp + } + if (data.auctionId) { + payload.auctionId = data.auctionId + } + if (data.adunid) { + payload.adunid = data.adunid + } + if (data.auctionDuration) { + payload.auctionDuration = data.auctionDuration + } + if (data.auctionStatus) { + payload.auctionStatus = data.auctionStatus + } + if (data.adUnits) { + payload.adUnits = data.adUnits + } + let endpoint + if (eventType === SERVER_EVENTS.AD_RENDER_FAILED) { + endpoint = ENDPOINTS.AD_RENDER_FAILED + } else if (eventType === SERVER_EVENTS.WON) { + endpoint = ENDPOINTS.BID_WON + } else if (eventType === SERVER_EVENTS.AD_RENDER_SUCCEEDED) { + endpoint = ENDPOINTS.AD_RENDER_SUCCEEDED + } else if (eventType === SERVER_EVENTS.AUCTION_END) { + endpoint = ENDPOINTS.AUCTION_END + } + if (endpoint) { + ajaxCall(endpoint, () => { }, JSON.stringify(payload), {}) + } +} + +function parseBidId(bid) { + return bid.bidId || bid.requestId +} + +function mapBid({ + bidStatus, + start, + end, + mediaType, + creativeId, + originalCpm, + originalCurrency, + source, + netRevenue, + currency, + width, + height, + timeToRespond, + responseTimestamp, + ...rest +}, eventType) { + const bidObj = { + bst: bidStatus, + s: start, + e: responseTimestamp || end, + mt: mediaType, + crId: creativeId, + oCpm: originalCpm, + oCur: originalCurrency, + src: source, + nrv: netRevenue, + cur: currency, + w: width, + h: height, + ttr: timeToRespond, + ...rest, + } + + delete bidObj['bidRequestsCount'] + delete bidObj['bidderRequestId'] + delete bidObj['bidderRequestsCount'] + delete bidObj['bidderWinsCount'] + delete bidObj['schain'] + delete bidObj['refererInfo'] + delete bidObj['statusMessage'] + delete bidObj['status'] + delete bidObj['adUrl'] + delete bidObj['ad'] + delete bidObj['usesGenericKeys'] + delete bidObj['requestTimestamp'] + delete bidObj['pbAg'] + delete bidObj['pbCg'] + delete bidObj['pbDg'] + delete bidObj['pbLg'] + delete bidObj['pbHg'] + delete bidObj['pbMg'] + delete bidObj['adserverTargeting'] + delete bidObj['ortb2Imp'] + return bidObj +} + +function ajaxCall(endpoint, callback, data, options = {}) { + options.contentType = 'application/json' + + return ajax(endpoint, callback, data, options) +} + +insticatorAdapter.originEnableAnalytics = insticatorAdapter.enableAnalytics +insticatorAdapter.enableAnalytics = function (config) { + insticatorAdapter.originEnableAnalytics(config) +}; + +insticatorAdapter.originDisableAnalytics = insticatorAdapter.disableAnalytics +insticatorAdapter.disableAnalytics = function () { + auctions = {} + insticatorAdapter.originDisableAnalytics() +} + +adapterManager.registerAnalyticsAdapter({ + adapter: insticatorAdapter, + code: ADAPTER_CODE +}) + +export default insticatorAdapter diff --git a/package.json b/package.json index 685e9bd892d..fa464f4e648 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "header bidding", "prebid" ], - "globalVarName": "pbjs", + "globalVarName": "instBid", "author": "the prebid.js contributors", "license": "Apache-2.0", "engines": { diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index fc7ed1833ac..bebf25dbcd1 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -270,8 +270,6 @@ describe('InsticatorBidAdapter', function () { expect(data.device.h).to.equal(window.innerHeight); expect(data.device.js).to.equal(true); expect(data.device.ext).to.be.an('object'); - expect(data.device.ext.localStorage).to.equal(true); - expect(data.device.ext.cookies).to.equal(false); expect(data.regs).to.be.an('object'); expect(data.regs.ext.gdpr).to.equal(1); expect(data.regs.ext.gdprConsentString).to.equal(bidderRequest.gdprConsent.consentString);