prebid/Prebid.js

View on GitHub
libraries/ortbConverter/README.md

Summary

Maintainability
Test Coverage
# Prebid.js - ORTB conversion library

This library provides methods to convert Prebid.js bid request objects to ORTB requests, 
and ORTB responses to Prebid.js bid response objects. 

## Usage

The simplest way to use this from an adapter is:

```javascript
import {ortbConverter} from '../../libraries/ortbConverter/converter.js'

const converter = ortbConverter({     
    context: {
        // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them 
        netRevenue: true,    // or false if your adapter should set bidResponse.netRevenue = false
        ttl: 30              // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp)  
    }
});

registerBidder({
    // ... rest of your spec goes here ...    
    buildRequests(bidRequests, bidderRequest) {
        const data = converter.toORTB({bidRequests, bidderRequest})
        // you may need to adjust `data` to suit your needs - see "customization" below
        return [{
            method: METHOD,
            url: ENDPOINT_URL,
            data
        }]
    },
    interpretResponse(response, request) {
        const bids = converter.fromORTB({response: response.body, request: request.data}).bids;
        // likewise, you may need to adjust the bid response objects
        return bids;
    },
})
```

Without any customization, the library will generate complete ORTB requests, but ignores your [bid params](#params). 
If your endpoint sets `response.seatbid[].bid[].mtype` (part of the ORTB 2.6 spec), it will also parse the response into complete bidResponse objects. See [setting response mediaTypes](#response-mediaTypes) if that is not the case.

### Module-specific conversions

Prebid.js features that require a module also require it for their corresponding ORTB conversion logic. For example, `imp.bidfloor` is only populated if the `priceFloors` module is active; `request.cur` needs the `currency` module, and so on. Notably, this means that to get those fields populated from your unit tests, you must import those modules first; see [this suite](https://github.com/prebid/Prebid.js/blob/master/test/spec/modules/openxOrtbBidAdapter_spec.js) for an example.

## Customization

### Modifying return values directly

You are free to modify the objects returned by both `toORTB` and `fromORTB`:

```javascript
const data = converter.toORTB({bidRequests, bidderRequest});
deepSetValue(data.imp[0], 'ext.myCustomParam', bidRequests[0].params.myCustomParam);
```

However, there are two restrictions (to avoid them, use the [other customization options](#fine-customization)):

 - you may not change the `imp[].id` returned by `toORTB`; they ared used internally to match responses to their requests.
     ```javascript
     const data = converter.toORTB({bidRequests, bidderRequest});
     data.imp[0].id = 'custom-imp-id' // do not do this - it will cause an error later in `fromORTB`
     ```
   See also [overriding `imp.id`](#imp-id).
 - the `request` argument passed to `fromORTB` must be the same object returned by `toORTB`.
    ```javascript
    let data = converter.toORTB({bidRequests, bidderRequest});
   
    data = mergeDeep(                                              // the original object is lost
      {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, // `fromORTB` will later throw an error
      data
    ); 
    
    // do this instead:                                                                                     
    mergeDeep(                                                     
      data, 
      {ext: {myCustomParam: bidRequests[0].params.myCustomParam}}, 
      data
    )   
    ```

### <a id="fine-customization" /> Fine grained customization - imp, request, bidResponse, response

When invoked, `toORTB({bidRequests, bidderRequest})` first loops through each request in `bidRequests`, converting them into ORTB `imp` objects.
It then packages them into a single ORTB request, adding other parameters that are not imp-specific (such as for example `request.tmax`).

Likewise, `fromORTB({request, response})` first loops through each `response.seatbid[].bid[]`, converting them into Prebid bidResponses; it then packages them into 
a single return value.

You can customize each of these steps using the `ortbConverter` arguments `imp`, `request`, `bidResponse` and `response`:

### <a id="imp" /> Customizing imps: `imp(buildImp, bidRequest, context)`

Invoked once for each input `bidRequest`; should return the ORTB `imp` object to include in the request.
The arguments are:

- `buildImp`: a function taking `(bidRequest, context)` and returning an ORTB `imp` object;
- `bidRequest`: the bid request object to convert;
- `context`: a [context object](#context) that contains at least:
   - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`.
   
#### <a id="params" /> Example: attaching custom bid params

```javascript
const converter = ortbConverter({
   imp(buildImp, bidRequest, context) {
       const imp = buildImp(bidRequest, context);
       deepSetValue(imp, 'ext.params', bidRequest.params);
       return imp;
   }
})
```

#### <a id="imp-id" /> Example: overriding imp.id

```javascript
const converter = ortbConverter({
   imp(buildImp, bidRequest, context) {
       const imp = buildImp(bidRequest, context);
       imp.id = randomIdentifierStr();
       return imp;
   }
})
```

### <a id="request" /> Customizing the request: `request(buildRequest, imps, bidderRequest, context)`

Invoked once after all bidRequests have been converted into `imp`s; should return the complete ORTB request. The return value
of this function is also the return value of `toORTB`.
The arguments are:

- `buildRequest`: a function taking `(imps, bidderRequest, context)` and returning an ORTB request object;
- `imps` an array of ORTB `imp` objects that should be included in the request;
- `bidderRequest`: the `bidderRequest` argument passed to `toORTB`;
- `context`: a [context object](#context) that contains at least:
    - `bidRequests`: the `bidRequests` argument passed to `toORTB`.

#### Example: setting additional request properties

```javascript
const converter = ortbConverter({
   request(buildRequest, imps, bidderRequest, context) {
      const request = buildRequest(imps, bidderRequest, context);
      deepSetValue(request, 'ext.adapterVersion', '0.0.1'); 
      return request;
   }
})
```

### <a id="bidResponse" /> Customizing bid responses: `bidResponse(buildBidResponse, bid, context)`

Invoked once for each `seatbid[].bid[]` in the response; should return the corresponding Prebid.js bid response object.
The arguments are:
- `buildBidResponse`: a function taking `(bid, context)` and returning a Prebid.js bid response object;
- `bid`: an ORTB `seatbid[].bid[]` object;
- `context`: a [context object](#context) that contains at least:
    - `seatbid`: the ORTB `seatbid[]` object that encloses `bid`;
    - `imp`: the ORTB request's `imp` object that matches `bid.impid`;
    - `bidRequest`: the Prebid.js bid request object that was used to generate `context.imp`;
    - `ortbRequest`: the `request` argument passed to `fromORTB`;
    - `ortbResponse`: the `response` argument passed to `fromORTB`.

#### Example: setting a custom outstream renderer

```javascript
const converter = ortbConverter({
    bidResponse(buildBidResponse, bid, context) {
        const bidResponse = buildBidResponse(bid, context);
        const {bidRequest} = context;
        if (bidResponse.mediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') {
            bidResponse.renderer = Renderer.install({
                url: RENDERER_URL,
                id: bidRequest.bidId,  
                adUnitCode: bidRequest.adUnitCode
            });
        }
        return bidResponse;
    }
})
```

#### <a id="response-mediaTypes" /> Example: setting response mediaType

In ORTB 2.5, bid responses do not specify their mediatype, which is something Prebid.js requires. You can provide it as
`context.mediaType`:

```javascript
const converter = ortbConverter({
    bidResponse(buildBidResponse, bid, context) {
        context.mediaType =  deepAccess(bid, 'ext.mediaType'); 
        return buildBidResponse(bid, context)
    }
})
```

If you know that a particular ORTB request/response pair deals with exclusively one mediaType, you may also pass it directly in the [context parameter](#context). 
Note that - compared to the above - this has additional effects, because `context.mediaType` is also considered during `imp` generation - see [special context properties](#special-context).

```javascript
converter.toORTB({
    bidRequests: bidRequests.filter(isVideoBid),
    bidderRequest,
    context: {mediaType: 'video'} // make everything in this request/response deal with video only
})
```

Note that this will _not_ work as intended:

```javascript

const converter = ortbConverter({
    bidResponse(buildBidResponse, bid, context) {
        const bidResponse = buildBidResponse(bid, context); // this throws; buildBidResponse needs to know the 
                                                            // mediaType to properly populate bidResponse.ad, 
                                                            // bidResponse.native etc
        bidResponse.mediaType = deepAccess(bid, 'ext.mediaType'); // too late, use context.mediaType
        return bidResponse;
    }
});

```

### <a id="response" /> Customizing the response: `response(buildResponse, bidResponses, ortbResponse, context)`

Invoked once, after all `seatbid[].bid[]` objects have been converted to corresponding bid responses. The value returned 
by this function is also the value returned by `fromORTB`.
The arguments are:

- `buildResponse`: a function that takes `(bidResponses, ortbResponse, context)` and returns `{bids: bidResponses}`. In the future, this may contain additional response data not necessarily tied to any bid (for example fledge auction configuration).
- `bidResponses`: array of Prebid.js bid response objects
- `ortbResponse`: the `response` argument passed to `fromORTB`
- `context`: a [context object](#context) that contains at least:
    - `ortbRequest`: the `request` argument passed to `fromORTB`;
    - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`;
    - `bidRequests`: the `bidRequests` argument passed to `toORTB`.

#### Example: logging server-side errors

```javascript
const converter = ortbConverter({
    response(buildResponse, bidResponses, ortbResponse, context) {
        (deepAccess(ortbResponse, 'ext.errors') || []).forEach((e) => logWarn('Server error', e));
        return buildResponse(bidResponses, ortbResponse, context);
    }
})
```

### Even finer grained customization - processor overrides

Each of the four conversion steps described above - imp, request, bidResponse and response - is further broken down into
smaller units of work (called _processors_). For example, when the currency module is included, it adds a _request processor_ 
that sets `request.cur`; the priceFloors module adds an _imp processor_ that sets `imp.bidfloor` and `imp.bidfloorcur`, and so on.

Each processor can be overridden or disabled through the `overrides` argument:

#### Example: disabling currency
```javascript
const converter = ortbConverter({
    overrides: {
        request: {
            currency: false
        }
    }
})
```

The above is similar in effect to:

```javascript
const converter = ortbConverter({
    request(buildRequest, imps, bidderRequest, context) {
        const request = buildRequest(imps, bidderRequest, context);
        delete request.cur;
        return request;
    }
})
```

With the main difference being that setting `currency: false` will disable currency logic entirely, while the `request`
version will still set `request.cur`, then delete it. If the currency processor is ever updated to deal with more than just `request.cur`, the `request`
function will also need to be updated accordingly.

#### Example: taking video parameters from `bidRequest.params.video`

Processors can also be overridden:

```javascript
const converter = ortbConverter({
    overrides: {
        imp: {
            video(orig, imp, bidRequest, context) {
                // `orig` is the video imp processor, which looks at bidRequest.mediaTypes[VIDEO] 
                // to populate imp.video
                // alter its input `bidRequest` to also pick up parameters from `bidRequest.params`
                let videoParams = bidRequest.mediaTypes[VIDEO];
                if (videoParams) {
                    videoParams = Object.assign({}, videoParams, bidRequest.params.video);
                    bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}}
                }
                orig(imp, bidRequest, context);
            } 
        }
    }
});
```

#### Processor override functions

Processor overrides are similar to the override options described above, except that they take the object to process as argument:

- `imp` processor overrides take `(orig, imp, bidRequest, context)`, where:
   - `orig` is the processor function being overridden, which itself takes `(imp, bidRequest, context)`;
   - `imp` is the (partial) imp object to modify;
   - `bidRequest` and `context` are the same arguments passed to [imp](#imp).
- `request` processor overrides take `(orig, ortbRequest, bidderRequest, context)`, where:
   - `orig` is the processor function being overridden, and takes `(ortbRequest, bidderRequest, context)`;
   - `ortbRequest` is the partial request to modify;
   - `bidderRequest` and `context` are the same arguments passed to [request](#reuqest).
- `bidResponse` processor overrides take `(orig, bidResponse, bid, context)`, where: 
   - `orig` is the processor function being overridden, and takes `(bidResponse, bid, context)`;
   - `bidResponse` is the partial bid response to modify;
   - `bid` and `context` are the same arguments passed to [bidResponse](#bidResponse)
- `response` processor overrides take `(orig, response, ortbResponse, context)`, where:
   - `orig` is the processor function being overriden, and takes `(response, ortbResponse, context)`;
   - `response` is the partial response to modify;
   - `ortbRespones` and `context` are the same arguments passed to [response](#response).

### <a id="context" /> The `context` argument

All customization functions take a `context` argument. This is a plain JS object that is shared between `request` and its corresponding `response`; and between `imp` and its corresponding `bidResponse`:

```javascript
const converter = ortbConverter({
    imp(buildImp, bidRequest, context) {
        // `context` here will be later passed to `bidResponse` (if one matches the imp generated here)
        context.someData = somethingInterestingAbout(bidRequest);
        return buildImp(bidRequest, context);
    },
    bidResponse(buildBidResponse, bid, context) {
        const bidResponse = buildBidResponse(bid, context);
        doSomethingWith(context.someData);
        return bidResponse;
    }
})
```

`ortbConverter` automatically populates `context` with some values of interest, such as `bidRequest`, `bidderRequest`, etc - as detailed above. In addition, you may pass additional context properties through:

- the `context` argument of `ortbConverter`: e.g. `ortbConverter({context: {ttl: 30}})`. This will set `context.ttl = 30` globally for the converter.
- the `context` argument of `toORTB`: e.g. `converter.toORTB({bidRequests, bidderRequest, context: {ttl: 30}})`. This will set `context.ttl = 30` only for this request.

### <a id="special-context" /> Special `context` properties

For ease of use, the conversion logic gives special meaning to some context properties:

 - `currency`: a currency string (e.g. `'EUR'`). If specified, overrides the currency to use for computing price floors and `request.cur`. If omitted, both default to `getConfig('currency.adServerCurrency')`.
 - `mediaType`: a bid mediaType (`'banner'`, `'video'`, or `'native'`). If specified:
    - disables `imp` generation for other media types (i.e., if `context.mediaType === 'banner'`, only `imp.banner` will be populated; `imp.video` and `imp.native` will not, even if the bid request specifies them);
    - is passed as the `mediaType` option to `bidRequest.getFloor` when computing price floors;
    - sets `bidResponse.mediaType`.
 - `nativeRequest`: a plain object that serves as the base value for `imp.native.request` (and is relevant only for native bid requests).
      If not specified, the only property that is guaranteed to be populated is `assets`, since Prebid does not require anything else to define a native adUnit. You can use `context.nativeRequest` to provide other properties; for example, you may want to signal support for native impression trackers by setting it to `{eventtrackers: [{event: 1, methods: [1, 2]}]}` (see also the [ORTB Native spec](https://www.iab.com/wp-content/uploads/2018/03/OpenRTB-Native-Ads-Specification-Final-1.2.pdf)).
 - `netRevenue`: the value to set as `bidResponse.netRevenue`. This is a required property of bid responses that does not have a clear ORTB counterpart.
 - `ttl`: the default value to use for `bidResponse.ttl` (if the ORTB response does not provide one in `seatbid[].bid[].exp`).
   
## Prebid Server extensions

If your endpoint is a Prebid Server instance, you may take advantage of the `pbsExtension` companion library, which adds a number of processors that can populate and parse PBS-specific extensions (typically prefixed `ext.prebid`); these include bidder params (with `transformBidParams`), bidder aliases, targeting keys, and others. 

```javascript
import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'

const pbsConverter = ortbConverter({
    processors: pbsExtensions
})
```