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
21 changes: 20 additions & 1 deletion modules/startioBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { registerBidder } from '../src/adapters/bidderFactory.js';
import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js';
import { logError, isFn, isPlainObject } from '../src/utils.js';
import { logError, isFn, isPlainObject, formatQS } from '../src/utils.js';
import { ortbConverter } from '../libraries/ortbConverter/converter.js'
import { ortb25Translator } from '../libraries/ortb2.5Translator/translator.js';
import { getUserSyncParams } from '../libraries/userSyncUtils/userSyncUtils.js';

const BIDDER_CODE = 'startio';
const METHOD = 'POST';
const GVLID = 1216;
const ENDPOINT_URL = `https://pbc-rtb.startappnetwork.com/1.3/2.5/getbid?account=pbc`;
const IFRAME_URL = 'test';

const converter = ortbConverter({
imp(buildImp, bidRequest, context) {
Expand Down Expand Up @@ -151,6 +153,23 @@ export const spec = {
},

onSetTargeting: (bid) => { },

getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) {
const syncs = [];

if (syncOptions.iframeEnabled) {
const consentParams = getUserSyncParams(gdprConsent, uspConsent, gppConsent);
const queryString = formatQS(consentParams);
const queryParam = queryString ? `?${queryString}` : '';

syncs.push({
type: 'iframe',
url: `${IFRAME_URL}${queryParam}`
});
}

return syncs;
}
};

registerBidder(spec);
19 changes: 19 additions & 0 deletions modules/startioBidAdapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ var nativeAdUnits = [
];
```

# User Syncs

The adapter supports iframe-based user syncing. When `iframeEnabled` is set to `true` in the sync options, the adapter returns a single iframe sync URL. GDPR, USP (CCPA), and GPP consent strings are automatically appended as query parameters when present.

```
pbjs.setConfig({
userSync: {
iframeEnabled: true
}
});
```

**Consent parameters included in the sync URL (when available):**
- `gdpr` – `1` if GDPR applies, `0` otherwise
- `gdpr_consent` – the TCF consent string
- `us_privacy` – the USP/CCPA consent string (e.g. `1YNN`)
- `gpp` – the GPP consent string
- `gpp_sections` – applicable GPP section IDs

# Additional Notes
- The adapter processes requests via OpenRTB 2.5 standards.
- Ensure that the `accountId` parameter is set correctly for your integration.
Expand Down
63 changes: 63 additions & 0 deletions modules/startioSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* This module adds startio ID support to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/startioSystem
* @requires module:modules/userId
*/
import { logError } from '../src/utils.js';
import { submodule } from '../src/hook.js';
import { ajax } from '../src/ajax.js';

const MODULE_NAME = 'startioId';
const DEFAULT_ENDPOINT = '';

export const startioIdSubmodule = {
name: MODULE_NAME,
decode(value) {
return value && typeof value === 'string'
? { 'startioId': value }
: undefined;
},
getId(config, consentData, storedId) {
Copy link
Author

Choose a reason for hiding this comment

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

Regarding to "consentData":
The standard fields on consentData are:

  • consentData.gdpr — { gdprApplies: bool, consentString: string }
  • consentData.usp — USP/CCPA privacy string
  • consentData.gpp — { gppString: string, applicableSections: [] }
  • consentData.coppa — COPPA flag

Do we need add them to our url? @matanarbel-startapp

const configParams = (config && config.params) || {};
const endpoint = configParams.endpoint || DEFAULT_ENDPOINT;

if (storedId) {
return { id: storedId };
}

const resp = function (callback) {
const callbacks = {
success: response => {
let responseId;
try {
const responseObj = JSON.parse(response);
if (responseObj && responseObj.id) {
responseId = responseObj.id;
} else {
logError(`${MODULE_NAME}: Server response missing 'id' field`);
}
} catch (error) {
logError(`${MODULE_NAME}: Error parsing server response`, error);
}
callback(responseId);
},
error: error => {
logError(`${MODULE_NAME}: ID fetch encountered an error`, error);
callback();
}
};
ajax(endpoint, callbacks, undefined, { method: 'GET' });
};
return { callback: resp };
},

eids: {
'startioId': {
source: 'start.io',
atype: 3
},
}
};

submodule('userId', startioIdSubmodule);
50 changes: 50 additions & 0 deletions modules/startioSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
## Start.io User ID Submodule

The Start.io User ID submodule generates and persists a unique user identifier by fetching it from a publisher-supplied endpoint. The ID is stored in both cookies and local storage for subsequent page loads and is made available to other Prebid.js modules via the standard `eids` interface.

For integration support, contact prebid@start.io.

### Prebid Params

```
pbjs.setConfig({
userSync: {
userIds: [{
name: 'startioId'
}]
}
});
```

## Parameter Descriptions for the `userSync` Configuration Section

The below parameters apply only to the Start.io User ID integration.

| Param under userSync.userIds[] | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| name | Required | String | The name of this module. | `"startioId"` |

## Server Response Format

The endpoint specified in `params.endpoint` must return a JSON response containing an `id` field:

```
{
"id": "unique-user-identifier-string"
}
```

If the `id` field is missing or the response cannot be parsed, the module logs an error and does not store a value.

## How It Works

1. On the first page load (no stored ID exists), the module sends a `GET` request to the configured `endpoint`.
2. The returned `id` is written to both cookies and local storage (respecting the `storage` configuration).
3. On subsequent loads the stored ID is returned directly — no network request is made.
4. The ID is exposed to other modules via the extended ID (`eids`) framework with source `start.io` and `atype: 3`.

## Notes

- The `endpoint` parameter is required. The module will log an error and return no ID if it is missing or not a string.
- Storage defaults to both cookies and local storage when no explicit `storage.type` is provided. The module checks whether each mechanism is available before writing.
- Cookie expiration is set to `storage.expires` days from the time the ID is first fetched (default 365 days).
94 changes: 94 additions & 0 deletions test/spec/modules/startioBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,98 @@ describe('Prebid Adapter: Startio', function () {
});
}
});

describe('getUserSyncs', function () {
it('should return an iframe sync when iframeEnabled is true', function () {
const syncs = spec.getUserSyncs({ iframeEnabled: true }, []);

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].type).to.equal('iframe');
expect(syncs[0].url).to.be.a('string');
});

it('should return an empty array when iframeEnabled is false', function () {
const syncs = spec.getUserSyncs({ iframeEnabled: false }, []);

expect(syncs).to.have.lengthOf(0);
});

it('should return an empty array when syncOptions is empty', function () {
const syncs = spec.getUserSyncs({}, []);

expect(syncs).to.have.lengthOf(0);
});

it('should append GDPR consent params to the sync URL', function () {
const gdprConsent = {
gdprApplies: true,
consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A=='
};

const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent);

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].url).to.include('gdpr=1');
expect(syncs[0].url).to.include('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A==');
});

it('should append gdpr=0 when gdprApplies is false', function () {
const gdprConsent = {
gdprApplies: false,
consentString: ''
};

const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent);

expect(syncs[0].url).to.include('gdpr=0');
});

it('should append USP consent param to the sync URL', function () {
const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], undefined, '1YNN');

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].url).to.include('us_privacy=1YNN');
});

it('should append GPP consent params to the sync URL', function () {
const gppConsent = {
gppString: 'DBABMA~BAAAAAAAAgA.QA',
applicableSections: [7, 8]
};

const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], undefined, undefined, gppConsent);

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].url).to.include('gpp=DBABMA~BAAAAAAAAgA.QA');
expect(syncs[0].url).to.include('gpp_sid=7,8');
});

it('should append all consent params together when all are provided', function () {
const gdprConsent = {
gdprApplies: true,
consentString: 'testConsent'
};
const uspConsent = '1YNN';
const gppConsent = {
gppString: 'testGpp',
applicableSections: [2]
};

const syncs = spec.getUserSyncs({ iframeEnabled: true }, [], gdprConsent, uspConsent, gppConsent);

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].url).to.include('gdpr=1');
expect(syncs[0].url).to.include('gdpr_consent=testConsent');
expect(syncs[0].url).to.include('us_privacy=1YNN');
expect(syncs[0].url).to.include('gpp=testGpp');
expect(syncs[0].url).to.include('gpp_sid=2');
});

it('should not append query string when no consent params are provided', function () {
const syncs = spec.getUserSyncs({ iframeEnabled: true }, []);

expect(syncs).to.have.lengthOf(1);
expect(syncs[0].url).to.not.include('?');
});
});
});
Loading
Loading