Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 68 additions & 6 deletions modules/floxisBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { triggerPixel, politeTriggerPixel, mergeDeep, replaceAuctionPrice } from '../src/utils.js';
import { triggerPixel, politeTriggerPixel, mergeDeep, replaceAuctionPrice, generateUUID } from '../src/utils.js';
import { getStorageManager } from '../src/storageManager.js';

const BIDDER_CODE = 'floxis';
const GVLID = 1609;
Expand All @@ -11,6 +12,12 @@ const DEFAULT_NET_REVENUE = true;
const DEFAULT_REGION = 'us-e';
const DEFAULT_PARTNER = BIDDER_CODE;
const SYNC_PATH = '/sync';
const FLOXIS_ID_KEY = 'flx_uid';
const FLOXIS_ID_COOKIE_EXP = 2592000000; // 30 days
const UUID_LENGTH = 36;

export const storage = getStorageManager({ bidderCode: BIDDER_CODE });

// Server-echo user-sync: the /pbjs response carries seat + region in this header (on bid and no-bid
// alike), so getUserSyncs derives sync targets from serverResponses statelessly — no module state that
// could leak across concurrent auctions. Absent header (older backend) => no sync, a safe no-op.
Expand Down Expand Up @@ -88,6 +95,56 @@ function normalizeBidParams(params = {}) {
};
}

function isValidFloxisId(id) {
return typeof id === 'string' && id.length === UUID_LENGTH;
}

function getOrCreatePersistedFloxisId() {
try {
const localOk = storage.localStorageIsEnabled();
const cookieOk = storage.cookiesAreEnabled();
if (!localOk && !cookieOk) return null;

let id = localOk ? storage.getDataFromLocalStorage(FLOXIS_ID_KEY) : null;
if (!isValidFloxisId(id) && cookieOk) {
id = storage.getCookie(FLOXIS_ID_KEY);
}
const minted = !isValidFloxisId(id);
if (minted) {
id = generateUUID();
}

if (localOk) {
storage.setDataInLocalStorage(FLOXIS_ID_KEY, id);
}
if (cookieOk) {
const expires = new Date(Date.now() + FLOXIS_ID_COOKIE_EXP).toUTCString();
storage.setCookie(FLOXIS_ID_KEY, id, expires);
}

if (minted &&
storage.getDataFromLocalStorage(FLOXIS_ID_KEY) !== id &&
storage.getCookie(FLOXIS_ID_KEY) !== id) {
return null;
}
return id;
} catch (e) {
return null;
}
}

function createFloxisIdResolver() {
let resolved = false;
let id = null;
return () => {
if (!resolved) {
resolved = true;
id = getOrCreatePersistedFloxisId();
}
return id;
};
}

// Parse the server-echoed sync header (`seat=<seat>&region=<label>`) into a sync target. Returns null
// for an absent or malformed header so a response without it simply contributes no sync.
function parseSyncHeader(headerValue) {
Expand Down Expand Up @@ -151,6 +208,13 @@ const CONVERTER = ortbConverter({
}
}
});
if (!req.user?.ext?.floxisId) {
const floxisId = context.resolveFloxisId();
if (floxisId) {
// mergeDeep, not deepSetValue: it repairs a non-object user/user.ext, which publisher ortb2 can supply
mergeDeep(req, { user: { ext: { floxisId } } });
}
}
return req;
},
bidResponse(buildBidResponse, bid, context) {
Expand Down Expand Up @@ -178,10 +242,7 @@ export const spec = {

buildRequests(validBidRequests = [], bidderRequest = {}) {
if (!validBidRequests.length) return [];
const filteredBidRequests = validBidRequests.filter((bidRequest) => spec.isBidRequestValid(bidRequest));
if (!filteredBidRequests.length) return [];

const bidRequestsByParams = filteredBidRequests.reduce((groups, bidRequest) => {
const bidRequestsByParams = validBidRequests.reduce((groups, bidRequest) => {
const { seat, region, partner } = normalizeBidParams(bidRequest.params);
const key = `${seat}|${region}|${partner}`;
groups[key] = groups[key] || [];
Expand All @@ -198,6 +259,7 @@ export const spec = {
}, {});

const groups = Object.values(bidRequestsByParams);
const resolveFloxisId = createFloxisIdResolver();

return groups.map((groupedBidRequests) => {
const { seat, region, partner } = groupedBidRequests[0].params;
Expand All @@ -206,7 +268,7 @@ export const spec = {
return {
method: 'POST',
url,
data: CONVERTER.toORTB({ bidRequests: groupedBidRequests, bidderRequest }),
data: CONVERTER.toORTB({ bidRequests: groupedBidRequests, bidderRequest, context: { resolveFloxisId } }),
options: {
withCredentials: true,
contentType: 'text/plain'
Expand Down
19 changes: 18 additions & 1 deletion modules/floxisBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ The Floxis Bid Adapter enables integration with the Floxis programmatic advertis
- OpenRTB 2.x compliant
- Privacy signal forwarding (GDPR/TCF, USP, GPP, COPPA) via Prebid.js core
- User identity (User ID / `eids`) and supply chain (`schain`) passthrough
- First-party fallback id for cookieless browsers (`user.ext.floxisId`)
- Prebid.js Floors Module support (plus a static `bidFloor` param fallback)
- User sync (iframe and pixel cookie matching)

Expand All @@ -29,6 +30,22 @@ The Floxis Bid Adapter supports the Prebid.js [Floors Module](https://docs.prebi
## User Identity & Supply Chain
`user.ext.eids` from the Prebid [User ID module](https://docs.prebid.org/dev-docs/modules/userId.html) and `source.ext.schain` (set via `pbjs.setConfig({ schain })` or `ortb2`) are forwarded automatically through first-party-data passthrough — no adapter-specific configuration is required.

## First-Party Fallback Id
Floxis's primary identity signal, the `__fxId` cookie set on `.floxis.tech`, is a third-party cookie relative to the publisher and is blocked by Safari, Firefox and other ITP/ETP browsers. To keep an identity-of-last-resort in those browsers, the adapter mints a random v4 UUID **in the publisher's own page context** (first-party) and places it at `user.ext.floxisId` in the OpenRTB request.

- **Storage & scope**: the id is persisted via `localStorage` (preferred) and a cookie, both scoped to the *publisher's own origin* — it is per-publisher, not cross-site, and is never shared between different sites running the adapter. Cookie lifetime is ~30 days; the id is regenerated if the stored value is not a well-formed 36-character UUID.
- **Priority on the backend**: the client id is a fallback only. Floxis's backend applies `processedCookieUserId (the __fxId cookie) .orElse(clientFloxisId) .orElse(existing user.id)` — when the `__fxId` cookie is present (e.g. Chrome/Edge), behavior is unchanged and the client id is ignored.
- **Consent**: storage access goes through Prebid.js core's `storageManager`, gated by the standard `deviceAccess` config and GDPR purpose-1 consent under Floxis's registered `gvlid` (1609) — the adapter adds no bespoke consent logic. If storage access is disallowed, no id is generated or sent, and the auction is unaffected.
- **Publisher opt-in required**: since Prebid.js 7.x, bidder-level storage access is denied by default and must be explicitly granted per bidder — without it, no `floxisId` is ever generated (a safe no-op, not an error). Enable it via:
```js
// https://docs.prebid.org/dev-docs/publisher-api-reference/bidderSettings.html
pbjs.bidderSettings = {
floxis: {
storageAllowed: true
}
}
```

## Privacy
GDPR/TCF, US Privacy, GPP and COPPA signals are handled by Prebid.js core and automatically included in the OpenRTB request; consent strings are also appended to user-sync calls. Floxis is registered with IAB Europe TCF as Vendor ID **1609**, declared via the adapter's `gvlid`.

Expand Down Expand Up @@ -109,4 +126,4 @@ pbjs.setConfig({
The adapter reports client-observed auction timeouts and bidder transport errors to Floxis as cookieless operational telemetry. Each beacon is a `keepalive` fetch sent with credentials omitted (no cookies) and scheduled off the auction's critical path, so it carries only the seat, region, event type, and relevant operational dimensions (HTTP status, timeout flag, duration, auction ID, publisher domain) — no user or device identifier is included. Consent signals are forwarded as opaque pass-through parameters where available. Each beacon fires at most once per distinct seat+region pair per event, and telemetry failures are silently suppressed so they never affect the auction lifecycle.

## Testing
Unit tests are provided in `test/spec/modules/floxisBidAdapter_spec.js` and cover validation, request building (params, host-label safety, floors, FPD/consent passthrough), response interpretation and meta mapping, user syncs, billing notifications, and error/timeout telemetry callbacks.
Unit tests are provided in `test/spec/modules/floxisBidAdapter_spec.js` and cover validation, request building (params, host-label safety, floors, FPD/consent passthrough, first-party fallback id), response interpretation and meta mapping, user syncs, billing notifications, and error/timeout telemetry callbacks.
Loading
Loading