|
| 1 | +--- |
| 2 | +layout: page_v2 |
| 3 | +sidebarType: 5 |
| 4 | +title: Prebid Server | Developers | Adding a Go Module |
| 5 | + |
| 6 | +--- |
| 7 | + |
| 8 | +# Prebid Server - Adding a Go Module |
| 9 | +{: .no_toc} |
| 10 | + |
| 11 | +* TOC |
| 12 | +{:toc } |
| 13 | + |
| 14 | +## Overview |
| 15 | + |
| 16 | +This document details how to make a module for PBS-Go. |
| 17 | + |
| 18 | +You will want to be familiar with the following background information: |
| 19 | + |
| 20 | +- the [module overview](/prebid-server/developers/add-a-module.html) |
| 21 | +- the [PBS-Go Modularity Tech Spec](https://docs.google.com/document/d/1CmamniQpwcI3p0_rHe2F17zV4sEhzpOdrqU7zuZVZ_I/edit?usp=sharing) |
| 22 | + |
| 23 | +### Contributing |
| 24 | + |
| 25 | +Check out the [PBS-Go contribution guide](https://github.com/prebid/prebid-server/blob/master/docs/developers/contributing.md) before introducing any code changes. |
| 26 | + |
| 27 | +## Module Directory Layout |
| 28 | + |
| 29 | +The Prebid Server repository contains a package `modules` located in the root project directory. It includes all available PBS modules. So, in order to add a new module, fork the repository and create a folder with the desired name inside the `modules` folder with the following structure: |
| 30 | + |
| 31 | +``` |
| 32 | ++- prebid-server/ |
| 33 | + +- modules/ <- package with modules that implement various hooks |
| 34 | + +- builder.go <- contains a list of all available modules |
| 35 | + +- {YOUR_VENDOR_NAME}/ <- top-level package used to group modules from the same vendor |
| 36 | + +- {YOUR_MODULE_NAME}/ <- package with source code of your module |
| 37 | + +- module.go <- file with module initialization function |
| 38 | +``` |
| 39 | +Module directory names (`{YOUR_VENDOR_NAME}/YOUR_MODULE_NAME/}`) must consist of valid identifiers. |
| 40 | +A valid identifier is defined as a sequence of one or more letters, including an underscore character (`_`), and digits. |
| 41 | +All other symbols such as `-`, `.`, etc. are not permitted. |
| 42 | + |
| 43 | +### Your module's build file |
| 44 | + |
| 45 | +Here's a partial example of your module-specific `module.go` file: |
| 46 | + |
| 47 | +``` |
| 48 | +package your_module_name |
| 49 | +
|
| 50 | +import ( |
| 51 | + "context" |
| 52 | + "encoding/json" |
| 53 | +
|
| 54 | + "github.com/prebid/prebid-server/hooks/hookstage" |
| 55 | + "github.com/prebid/prebid-server/modules/moduledeps" |
| 56 | +) |
| 57 | +
|
| 58 | +func Builder(config json.RawMessage, deps moduledeps.ModuleDeps) (interface{}, error) { |
| 59 | + return Module{}, nil |
| 60 | +} |
| 61 | +
|
| 62 | +// Module must implement at least 1 hook interface. |
| 63 | +type Module struct{} |
| 64 | +
|
| 65 | +func (m Module) HandleBidderRequestHook( |
| 66 | + ctx context.Context, |
| 67 | + invocationCtx hookstage.ModuleInvocationContext, |
| 68 | + payload hookstage.BidderRequestPayload, |
| 69 | +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { |
| 70 | + result := hookstage.HookResult[hookstage.BidderRequestPayload]{} |
| 71 | +
|
| 72 | + // hook handling logic |
| 73 | + |
| 74 | + return result, nil |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +In the example above, our module only implements the `bidder-request` hook interface. |
| 79 | + |
| 80 | +The module's `Builder` function receives 2 arguments: |
| 81 | +1. `config json.RawMessage` - represents a global config of your module, see [Configuration](#configuration). |
| 82 | +2. `deps moduledeps.ModuleDeps` - contains dependencies that your module might require. |
| 83 | + |
| 84 | +and returns 2 values: |
| 85 | +1. `interface{}` - must implement at least 1 hook interface, see [hooks](#hook-interfaces). PBS uses type assertion to find out which hook interfaces implemented by module. |
| 86 | +2. `error` - any error occurred during module initialization. |
| 87 | + |
| 88 | +### Expose your module to PBS |
| 89 | + |
| 90 | +All available modules are exposed through the `modules/builder.go` file. This file is auto-generated, so you shouldn’t edit it manually. |
| 91 | + |
| 92 | +To register a new module, you just need to run one of the following commands from the PBS root directory: |
| 93 | + - `make build-modules` |
| 94 | + - or `go generate modules/modules.go` |
| 95 | + |
| 96 | +This command scans the `modules/` directory for files matching the pattern `modules/*/*/module.go` and adds all matching packages to the `modules/builder.go` file. |
| 97 | + |
| 98 | +## Module Code |
| 99 | + |
| 100 | +The quick start is to take a look in two places: |
| 101 | +- the [prebid ortb2blocking module](https://github.com/prebid/prebid-server/tree/master/modules/prebid/ortb2blocking) |
| 102 | +- the [hook source code and tests](https://github.com/prebid/prebid-server/tree/master/hooks) |
| 103 | + |
| 104 | +### Adding module documentation |
| 105 | +It is required to add a "README.md" file to the root of your module folder. It's recommended to specify the description of what the implemented module does, links to external documentation and include maintainer contact info (email, slack, etc). |
| 106 | + |
| 107 | +The documentation must also live on the docs.prebid.org site. Please add a markdown file to https://github.com/prebid/prebid.github.io/tree/master/prebid-server/pbs-modules |
| 108 | + |
| 109 | +### Hook Interfaces |
| 110 | + |
| 111 | +The Prebid server processing workflow is divided into several 'stages' where module authors can inject a specific function signature called a 'hook'. |
| 112 | + |
| 113 | +The Prebid Server host company will define which modules to run in which order by setting up a configuration defining which hooks run serially and which can run in parallel. |
| 114 | + |
| 115 | +The supported stages are described in the [general module overview](/prebid-server/developers/add-a-module.html#2-understand-the-endpoints-and-stages) and in PBS-Core source code at the "github.com/prebid/prebid-server/hooks" package. |
| 116 | + |
| 117 | +These are the available hooks that can be implemented in a module: |
| 118 | + |
| 119 | +- github.com/prebid/prebid-server/hooks/hookstage.Entrypoint |
| 120 | +- github.com/prebid/prebid-server/hooks/hookstage.RawAuctionRequest |
| 121 | +- github.com/prebid/prebid-server/hooks/hookstage.ProcessedAuctionRequest |
| 122 | +- github.com/prebid/prebid-server/hooks/hookstage.BidderRequest |
| 123 | +- github.com/prebid/prebid-server/hooks/hookstage.RawBidderResponse |
| 124 | +- github.com/prebid/prebid-server/hooks/hookstage.AllProcessedBidResponses |
| 125 | +- github.com/prebid/prebid-server/hooks/hookstage.AuctionResponse |
| 126 | + |
| 127 | +In a module it is not necessary to implement all mentioned interfaces but at least one is required by your functionality. |
| 128 | + |
| 129 | +### Examples |
| 130 | + |
| 131 | +1) To **update** the request in the `BidderRequest`, your implementation would return a hook result with a change set: |
| 132 | +``` |
| 133 | +import ( |
| 134 | + "context" |
| 135 | +
|
| 136 | + "github.com/prebid/prebid-server/hooks/hookstage" |
| 137 | +) |
| 138 | +
|
| 139 | +type Module struct{} |
| 140 | +
|
| 141 | +func (m Module) HandleBidderRequestHook( |
| 142 | + ctx context.Context, |
| 143 | + invocationCtx hookstage.ModuleInvocationContext, |
| 144 | + payload hookstage.BidderRequestPayload, |
| 145 | +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { |
| 146 | + changeSet := hookstage.ChangeSet[hookstage.BidderRequestPayload]{} |
| 147 | + changeSet.BidderRequest().BAdv().Update([]string{"a.com"}) |
| 148 | + |
| 149 | + return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: changeSet}, nil |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +Please note, the `hookstage.ChangeSet` has a restricted set of methods, but methods can be easily extended when more use cases come up. |
| 154 | + |
| 155 | +For more complex payload updates, you can choose another method: |
| 156 | +``` |
| 157 | +func (m Module) HandleBidderRequestHook( |
| 158 | + ctx context.Context, |
| 159 | + invocationCtx hookstage.ModuleInvocationContext, |
| 160 | + payload hookstage.BidderRequestPayload, |
| 161 | +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { |
| 162 | + battrByImp := map[string][]adcom1.CreativeAttribute{"imp_ID1": []adcom1.CreativeAttribute{adcom1.AttrAudioAuto}} |
| 163 | + changeSet := hookstage.ChangeSet[hookstage.BidderRequestPayload]{} |
| 164 | + changeSet.AddMutation(func(payload hookstage.BidderRequestPayload) (hookstage.BidderRequestPayload, error) { |
| 165 | + for i, imp := range payload.BidRequest.Imp { |
| 166 | + if battr, ok := battrByImp[imp.ID]; ok { |
| 167 | + imp.Banner.BAttr = battr |
| 168 | + payload.BidRequest.Imp[i] = imp |
| 169 | + } |
| 170 | + } |
| 171 | + return payload, nil |
| 172 | + }, hookstage.MutationUpdate, "bidrequest", "imp", "banner", "battr") |
| 173 | + |
| 174 | + return hookstage.HookResult[hookstage.BidderRequestPayload]{ChangeSet: changeSet}, nil |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +2) To **reject** the bidder in the `BidderRequest`, your hook implementation would return a hook result with a reject flag and an NBR code: |
| 179 | +``` |
| 180 | +func (m Module) HandleBidderRequestHook( |
| 181 | + ctx context.Context, |
| 182 | + invocationCtx hookstage.ModuleInvocationContext, |
| 183 | + payload hookstage.BidderRequestPayload, |
| 184 | +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { |
| 185 | + return hookstage.HookResult[hookstage.BidderRequestPayload]{Reject: true, NbrCode: 7}, nil |
| 186 | +} |
| 187 | +``` |
| 188 | + |
| 189 | +Refer [here](https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/OpenRTB%20v3.0%20FINAL.md#list--no-bid-reason-codes-) for a list of available No Bid Response Codes. |
| 190 | + |
| 191 | +3) To supply [analytics tags](/prebid-server/developers/module-atags.html) in the `BidderRequest`, your hook implementation would return a hook result with analytics tags: |
| 192 | +``` |
| 193 | +import ( |
| 194 | + "context" |
| 195 | +
|
| 196 | + "github.com/prebid/prebid-server/hooks/hookstage" |
| 197 | + "github.com/prebid/prebid-server/hooks/hookanalytics" |
| 198 | +) |
| 199 | +
|
| 200 | +func (m Module) HandleBidderRequestHook( |
| 201 | + ctx context.Context, |
| 202 | + invocationCtx hookstage.ModuleInvocationContext, |
| 203 | + payload hookstage.BidderRequestPayload, |
| 204 | +) (hookstage.HookResult[hookstage.BidderRequestPayload], error) { |
| 205 | + return hookstage.HookResult[hookstage.BidderRequestPayload]{ |
| 206 | + AnalyticsTags: hookanalytics.Analytics{ |
| 207 | + Activities: []hookanalytics.Activity{ |
| 208 | + { |
| 209 | + Name: "enforce_blocking", |
| 210 | + Status: hookanalytics.ActivityStatusSuccess, |
| 211 | + Results: []hookanalytics.Result{ |
| 212 | + { |
| 213 | + Status: hookanalytics.ResultStatusBlock, |
| 214 | + Values: map[string]interface{}{ |
| 215 | + "attributes": []string{"bcat"}, |
| 216 | + "bcat": []string{"IAB-1"}, |
| 217 | + }, |
| 218 | + AppliedTo: hookanalytics.AppliedTo{Bidder: "appnexus", ImpIds: []string{"imp_ID1"}}, |
| 219 | + }, |
| 220 | + { |
| 221 | + Status: hookanalytics.ResultStatusAllow, |
| 222 | + AppliedTo: hookanalytics.AppliedTo{Bidder: "appnexus", ImpIds: []string{"imp_ID2"}}, |
| 223 | + }, |
| 224 | + }, |
| 225 | + }, |
| 226 | + }, |
| 227 | + }, |
| 228 | + }, nil |
| 229 | +} |
| 230 | +``` |
| 231 | + |
| 232 | +More test implementations for each hook can be found in unit-tests at [https://github.com/prebid/prebid-server/tree/master/modules/prebid/ortb2blocking](https://github.com/prebid/prebid-server/tree/master/modules/prebid/ortb2blocking) folder. |
| 233 | + |
| 234 | +### Configuration |
| 235 | + |
| 236 | +It's possible to define default module configuration which can be read by the module at PBS startup. Please see the [Configuration](https://docs.google.com/document/d/1CmamniQpwcI3p0_rHe2F17zV4sEhzpOdrqU7zuZVZ_I/edit#heading=h.mh3urph3k1mk) section of the technical specification. |
| 237 | + |
| 238 | +An example configuration for hooks might look like this: |
| 239 | +```json |
| 240 | +{ |
| 241 | + "hooks": { |
| 242 | + "enabled": true, |
| 243 | + "modules": { |
| 244 | + "vendor1": { |
| 245 | + "module1": { |
| 246 | + "enabled": true |
| 247 | + } |
| 248 | + } |
| 249 | + }, |
| 250 | + "host_execution_plan": { |
| 251 | + "endpoints": { |
| 252 | + "/openrtb2/auction": { |
| 253 | + "stages": { |
| 254 | + "bidder_request": { |
| 255 | + "groups": [ |
| 256 | + { |
| 257 | + "timeout": 10, |
| 258 | + "hook_sequence": [ |
| 259 | + { |
| 260 | + "module_code": "vendor1.module1", |
| 261 | + "hook_impl_code": "code123" |
| 262 | + } |
| 263 | + ] |
| 264 | + } |
| 265 | + ] |
| 266 | + } |
| 267 | + } |
| 268 | + } |
| 269 | + } |
| 270 | + } |
| 271 | + } |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +### Testing |
| 276 | + |
| 277 | +Unit tests are required. Each implemented hook must be at least 90% covered by unit tests. |
| 278 | + |
| 279 | +### How to build and install a module |
| 280 | + |
| 281 | +Read about the module building in the [building section](https://docs.google.com/document/d/1CmamniQpwcI3p0_rHe2F17zV4sEhzpOdrqU7zuZVZ_I/edit#heading=h.o8dv0neoq4xm) of the technical specification. |
| 282 | + |
| 283 | +## Analytics Adapters and Modules |
| 284 | + |
| 285 | +Each module can inject analytics tags into the request as described in the analytics tags section. |
| 286 | + |
| 287 | +Analytics adapters receive these tags through the Auction/AMP analytic object. |
| 288 | + |
| 289 | +To get analytics tags you need to go into: |
| 290 | + |
| 291 | +``` |
| 292 | +AuctionObject/AmpObject |
| 293 | + -> HookExecutionOutcome (iterate through stages) |
| 294 | + -> Groups (iterate through groups) |
| 295 | + -> InvocationResults (go through hooks invocation results and find interested one) |
| 296 | + -> AnalyticsTags |
| 297 | +``` |
| 298 | + |
| 299 | +The `AnalyticsTags` object has activities with collection of `github.com/prebid/prebid-server/hooks/hookanalytics.Result` objects inside. Each `Result` has the `Values` field which holds arbitrary values set by a module. |
| 300 | + |
| 301 | +It depends on the particular module implementation how to parse their analytics tags, since the internal structure is custom and depends on the module. Therefore, analytics modules that want to report on specific behavior need to be coded to know about that module. See the prebid ortb2blocking module for an example of what analytics tags may be available. |
| 302 | + |
| 303 | +## Further Reading |
| 304 | + |
| 305 | +- [PBS Module Overview](/prebid-server/developers/add-a-module.html) |
| 306 | +- [PBS Module Analytics Tags Conventions](/prebid-server/developers/module-atags.html) |
0 commit comments