Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
130 changes: 130 additions & 0 deletions modules/haloadsBidAdapter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Overview

Module Name: Haloads Bidder Adapter
Module Type: Bidder Adapter
Maintainer: info.litelabs@gmail.com

**Bidder code:** `haloads`

# Description

The Haloads Bidder Adapter connects to Haloads exchange for bids. This adapter supports banner, video, and native ad formats and implements the OpenRTB 2.5 protocol.

**Currency**

- Haloads returns prices in **USD**. The adapter always sets `bid.currency` to `USD`

**Placement mapping**

- The adapter sets `imp.tagid` from `bid.params.placementId`. This value must match the placement identifier provided by Haloads team.
- The adapter also requires accountId as provided by Haloads team.

**Supported media types**

- Banner (`adm` → `bid.ad`)
- Video (`adm` → `bid.vastXml`; optional `nurl` → `bid.vastUrl`)
- Native (`adm` as OpenRTB Native JSON → `bid.native.ortb`)

# Bid Parameters

| Name | Scope | Type | Description |
|----------------|----------|--------|-------------|
| `placementId` | Required | String | Placement ID on Haloads |
| `accountId` | Required | String | Account ID for the publisher as configured on Haloads |

# Test Parameters

## Banner Ad Unit

```javascript
var adUnits = [
{
code: 'banner-ad-unit',
mediaTypes: {
banner: {
sizes: [[300, 250], [728, 90]]
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '29291001',
placementId: '12345'
}
}]
}
];
```

## Video Ad Unit

```javascript
var adUnits = [
{
code: 'video-ad-unit',
mediaTypes: {
video: {
context: 'instream',
playerSize: [[640, 480]],
mimes: ['video/mp4'],
protocols: [2, 3, 5, 6],
api: [2]
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '291001',
placementId: 'video-12345'
}
}]
}
];
```

## Native Ad Unit

```javascript
var adUnits = [
{
code: 'native-ad-unit',
mediaTypes: {
native: {
title: {
required: true,
len: 80
},
image: {
required: true,
sizes: [300, 250]
},
sponsoredBy: {
required: true
}
}
},
bids: [{
bidder: 'haloads',
params: {
accountId: '291001',
placementId: 'native-12345'
}
}]
}
];
```

## Configuration

### User Sync

The adapter automatically handles user syncing with privacy consent parameters (GDPR, USP, GPP).

### Event Tracking

The adapter supports the following event callbacks:
- `onBidWon`: Triggered when a bid wins the auction
- `onAdRenderSucceeded`: Triggered when an ad successfully renders
- `onBidderError`: Triggered when a bidder error occurs
- `onTimeout`: Triggered when a bid request times out

200 changes: 200 additions & 0 deletions modules/haloadsBidAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
import { BidderSpec, ExtendedResponse, registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js';
import { triggerPixel, deepAccess } from '../src/utils.js';
import { tryAppendQueryString } from '../libraries/urlUtils/urlUtils.js';
import type { BidRequest } from '../src/adapterManager.js';


Check failure on line 8 in modules/haloadsBidAdapter.ts

View workflow job for this annotation

GitHub Actions / Run linter

More than 1 blank line not allowed
const BIDDER_CODE = 'haloads';
const ENDPOINT = 'https://ads.haloads.io/bid';
const SYNC_URL = 'https://ads.haloads.io/cookie_sync';
const EVENT_TRACKING_URL = 'https://analytics.haloads.io/event';

interface HaloadsBidParams {
accountId: string;
placementId: string | number;
[key: string]: unknown;
}

declare module '../src/adUnits' {
interface BidderParams {
[BIDDER_CODE]: HaloadsBidParams;
}
}

declare module '../src/bidfactory' {
interface BannerBidProperties {
pbsWurl?: string;
nurl?: string;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what's causing the ts-expect-error error. Adding nurl means that it is no longer an error to use it.

Neither this nor mile's ts-expect-error should be where they are. The problem is that these definitions do not necessarily take effect (haloads does not have to be in every build), but affect common types.

I think this should go one of two ways:

  1. If we believe these fields are useful to the publisher or other consumers, they should go in bidfactory.ts (and likely in BaseBidResponse rather than in each type that extends it).
  2. otherwise, if you are just using these to coordinate between interpretResponse and later event handlers like onBidWon, which looks to be what mile is also doing, I'd just shut up the type checker by casting to any - or if you don't like that, by defining a type that you can use just in your adapters.

My preference is the second option because none of these fields look useful enough to be in core to me, but I could be wrong.

pbsWurl is mentioned just by

if (bid.pbsWurl) {

and

but they just look for it without setting it (they likely copied it from somewhere) so I don't think it ever has any effect.

nurl is actually mentioned by quite a few bidders - so maybe it's worth including it? It has no effect beyond what the adapters themselves do with it, and I'm not sure all of them are using it for the same purpose.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood, @dgirardi . Thank you for pointing that out. I discussed with our team and updated the requirement for this part, and removed it as the use-case is not currently required.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you removed nurl altogether, but the issue was with the type definitions - not the functionality, and it's still there for pbsWurl / ext. A bid adapter shouldn't be changing the definition of a bid.

ext?: Record<string, unknown>;
}
interface VideoBidProperties {
pbsWurl?: string;
nurl?: string;
ext?: Record<string, unknown>;
}
interface NativeBidProperties {
pbsWurl?: string;
ext?: Record<string, unknown>;
}
}

const converter = ortbConverter({
context: {
netRevenue: true,
ttl: 300
},
imp: function (buildImp, bidRequest: BidRequest<typeof BIDDER_CODE>, context) {
const imp = buildImp(bidRequest, context);
imp.tagid = bidRequest.params.placementId.toString();
return imp;
},
bidResponse: function (buildBidResponse, bid, context) {
const bidResponse = buildBidResponse(bid, context) as ReturnType<typeof buildBidResponse> & { ext?: unknown };
const bidAny = bid as unknown as Record<string, unknown>;
if (bidAny.ext) {
bidResponse.ext = bidAny.ext;
}
if (bidResponse.mediaType === VIDEO) {
const vBidResponse = bidResponse as typeof bidResponse & { vastXml?: string };
if (!vBidResponse.vastXml && bidAny.adm) {
vBidResponse.vastXml = bidAny.adm as string;
}
}
return bidResponse;
}
});

export const spec: BidderSpec<typeof BIDDER_CODE> = {
code: BIDDER_CODE,
supportedMediaTypes: [BANNER, VIDEO, NATIVE],

isBidRequestValid: function (bid) {
return !!(bid.params && bid.params.accountId && bid.params.placementId);
},

buildRequests: function (validBidRequests, bidderRequest) {
const data = converter.toORTB({ bidderRequest, bidRequests: validBidRequests as any });

const accountId = validBidRequests[0].params.accountId;
if (accountId && data.site) {
if (!data.site.publisher) {
data.site.publisher = {};
}
data.site.publisher.id = accountId.toString();
}

return {
method: 'POST',
url: ENDPOINT,
data: data,
options: {
contentType: 'application/json',

Check warning

Code scanning / CodeQL

Application/json request type in bidder Warning

application/json request type triggers preflight requests and may increase bidder timeouts
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
withCredentials: true
}
};
},

interpretResponse: function (serverResponse, bidRequest) {
if (!serverResponse || !serverResponse.body) {
return [];
}

const ortbResponse = converter.fromORTB({ response: serverResponse.body, request: bidRequest.data });
return (ortbResponse as ExtendedResponse).bids ?? [];
},

getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
const syncs: { type: 'iframe' | 'image'; url: string }[] = [];

if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) {
return syncs;
}

let query = '';
if (gdprConsent) {
query = tryAppendQueryString(query, 'gdpr', String(gdprConsent.gdprApplies ? 1 : 0));
}
if (gdprConsent && typeof gdprConsent.consentString === 'string') {
query = tryAppendQueryString(query, 'gdpr_consent', gdprConsent.consentString);
}
if (uspConsent) {
query = tryAppendQueryString(query, 'us_privacy', uspConsent as string);
}
if (gppConsent?.gppString && gppConsent?.applicableSections?.length) {
query = tryAppendQueryString(query, 'gpp', gppConsent.gppString);
query = tryAppendQueryString(query, 'gpp_sid', gppConsent.applicableSections.join(','));
}
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}

if (syncOptions.iframeEnabled) {
syncs.push({
type: 'iframe',
url: `${SYNC_URL}/sync.html${query ? '?' + query : ''}`
});
}
if (syncOptions.pixelEnabled) {
syncs.push({
type: 'image',
url: `${SYNC_URL}/sync.png${query ? '?' + query : ''}`
});
}
return syncs;
},

onBidWon: function (bid) {
if (bid.pbsWurl) {
triggerPixel(bid.pbsWurl);
}
if (bid.nurl) {
triggerPixel(bid.nurl);
}
},

onAdRenderSucceeded: function (bid) {
const impUrl = deepAccess(bid, 'ext.prebid.events.imp') || deepAccess(bid, 'meta.ext.prebid.events.imp');
if (impUrl) {
triggerPixel(impUrl);
}
},

onBidderError: function (errorData) {
let query = 'eventType=error';

if (errorData) {
if (errorData.error) {
query = tryAppendQueryString(query, 'error', String(errorData.error.status) || 'unknown');
query = tryAppendQueryString(query, 'status', String(errorData.error.status));
Comment thread
riteshghodrao marked this conversation as resolved.
Outdated
}
if (errorData.bidderRequest) {
query = tryAppendQueryString(query, 'auctionId', errorData.bidderRequest.auctionId || '');
}
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}
}

triggerPixel(`${EVENT_TRACKING_URL}?${query}`);
},

onTimeout: function (timeoutData) {
let query = 'eventType=timeout';

if (timeoutData && timeoutData.length > 0) {
const firstTimeout = timeoutData[0];
query = tryAppendQueryString(query, 'auctionId', firstTimeout.auctionId || '');
query = tryAppendQueryString(query, 'timeout', String(firstTimeout.timeout));
query = tryAppendQueryString(query, 'adUnitCode', firstTimeout.adUnitCode || '');
Comment thread
riteshghodrao marked this conversation as resolved.
Outdated
if (query.slice(-1) === '&') {
query = query.slice(0, -1);
}
}

triggerPixel(`${EVENT_TRACKING_URL}?${query}`);
}
};

registerBidder(spec);
Loading
Loading