-
Notifications
You must be signed in to change notification settings - Fork 221
New Adapter: Nativery #4223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
New Adapter: Nativery #4223
Conversation
| - video | ||
| - native | ||
| supported-vendors: [] | ||
| vendor-id: 0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1133
| - banner | ||
| - video | ||
| - native | ||
| supported-vendors: [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
leave it just as supported-vendors:
| BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties, | ||
| @NotBlank @Value("${external-url}") String externalUrl, | ||
| JacksonMapper mapper) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please format
| return mapper.mapper().convertValue(root, ExtRequest.class); | ||
| } | ||
|
|
||
| private boolean isAmpRequest(BidRequest request) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix the methods order
| for (Imp imp : request.getImp()) { | ||
| try { | ||
| final ExtImpNativery extImp = parseImpExt(imp); | ||
| if (widgetId == null && extImp != null && StringUtils.isNotBlank(extImp.getWidgetId())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extImp cannot be null at this stage
| final BidRequest singleImpRequest = request.toBuilder() | ||
| .imp(Collections.singletonList(imp)) | ||
| .ext(updatedExt) | ||
| .cur(Collections.singletonList(DEFAULT_CURRENCY)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is this change needed? I don't this in the Go version
| if (httpCall.getResponse() != null && httpCall.getResponse().getStatusCode() == 204) { | ||
| final MultiMap headers = httpCall.getResponse().getHeaders(); | ||
| final String nativeryErr = headers != null ? headers.get(NATIVERY_ERROR_HEADER) : null; | ||
| if (StringUtils.isNotBlank(nativeryErr)) { | ||
| return Result.withError(BidderError.badInput("Nativery Error: " + nativeryErr + ".")); | ||
| } | ||
| return Result.withError(BidderError.badServerResponse("No Content")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately this isn't going to work because the 204 status code is check before calling makeBids in the core, so probably we won't support this weird error-header approach
@CTMBNara what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Y, it won't work
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
|
|
||
| private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) { | ||
| try { | ||
| final ObjectNode nativeryExt = extractNativeryExt(bid.getExt()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's create a separate BidExtNativery object and deserialize with the object mapper, because that manual conversion feels bad
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^
| private ObjectNode extractNativeryExt(ObjectNode bidExt) { | ||
| if (bidExt == null) { | ||
| throw new PreBidException("missing bid.ext"); | ||
| } | ||
| final JsonNode node = bidExt.get("nativery"); | ||
| if (!(node instanceof ObjectNode nativeryNode)) { | ||
| throw new PreBidException("missing bid.ext.nativery"); | ||
| } | ||
| return nativeryNode; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with a simple Optional chain
| return Result.of(Collections.emptyList(), errors); | ||
| } | ||
|
|
||
| // 🟢 Zmienione — zamieniamy ExtRequest na ObjectNode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please remove the comments
| } | ||
|
|
||
| if (validImps.isEmpty()) { | ||
| return Result.of(Collections.emptyList(), errors); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Result.withErrors(errors)
| final ObjectNode originalExt = request.getExt() != null | ||
| ? mapper.mapper().convertValue(request.getExt(), ObjectNode.class) | ||
| : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not necessary to serialize ExtRequest object into the ObjectNode, because ExtRequest is a FlexibleExtension
| final ExtPrebid<?, ExtImpNativery> ext = | ||
| mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE); | ||
| return ext != null ? ext.getBidder() : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imp.ext can't be null at this stage, so it can't be deserialized into null
| private ExtRequest buildRequestExtWithNativery(ObjectNode originalExt, boolean isAmp, String widgetId) { | ||
| final ObjectNode root = originalExt != null | ||
| ? originalExt.deepCopy() | ||
| : mapper.mapper().createObjectNode(); | ||
|
|
||
| final ObjectNode nativeryNode = root.with("nativery"); | ||
|
|
||
| nativeryNode.put("isAmp", isAmp); | ||
| if (widgetId != null) { | ||
| nativeryNode.put("widgetId", widgetId); | ||
| } | ||
|
|
||
| return mapper.mapper().convertValue(root, ExtRequest.class); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use ExtRequest and FlexibleExtension capabilities
|
|
||
| private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) { | ||
| try { | ||
| final ObjectNode nativeryExt = extractNativeryExt(bid.getExt()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^^
| if (httpCall.getResponse() != null && httpCall.getResponse().getStatusCode() == 204) { | ||
| final MultiMap headers = httpCall.getResponse().getHeaders(); | ||
| final String nativeryErr = headers != null ? headers.get(NATIVERY_ERROR_HEADER) : null; | ||
| if (StringUtils.isNotBlank(nativeryErr)) { | ||
| return Result.withError(BidderError.badInput("Nativery Error: " + nativeryErr + ".")); | ||
| } | ||
| return Result.withError(BidderError.badServerResponse("No Content")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Y, it won't work
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
src/main/java/org/prebid/server/bidder/nativery/NativeryBidder.java
Outdated
Show resolved
Hide resolved
| final var response = httpCall.getResponse(); | ||
| if (response == null || StringUtils.isBlank(response.getBody())) { | ||
| return Result.withError(BidderError.badServerResponse("Empty response")); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for this check
| final List<String> advDomains = ListUtils.defaultIfNull( | ||
| nativeryExt.getBidAdvDomains(), Collections.emptyList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I made a typo and recommended the wrong method. Use ListUtils.emptyIfNull() directly.
| private static String mediaTypeString(BidType type) { | ||
| return switch (type) { | ||
| case banner -> type.getName(); | ||
| case video -> type.getName(); | ||
| case xNative -> type.getName(); | ||
| default -> throw new IllegalStateException("Unexpected value: " + type.getName()); | ||
| }; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this method and use type.getName instead
| final List<String> safeAdvDomains = Optional.ofNullable(advDomains) | ||
| .orElse(Collections.emptyList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
advDomains can't be null
| @JsonProperty("bid_ad_media_type") | ||
| String bidAdMediaType; | ||
|
|
||
| @JsonProperty("bid_adv_domains") | ||
| List<String> bidAdvDomains; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for @JsonProperty. Our objectMapper uses snake-case by default
| final ObjectNode extNode = mapper.createObjectNode(); | ||
| extNode.put("accountId", "acc-123"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ObjectNode -> ExtRequest. You can add custom properties to it since it is a FlexibleExtension
| final ObjectNode extNode = mapper.createObjectNode(); | ||
| final ObjectNode prebidNode = mapper.createObjectNode(); | ||
| final ObjectNode serverNode = mapper.createObjectNode(); | ||
| serverNode.put("endpoint", Endpoint.openrtb2_amp.value()); | ||
| prebidNode.set("server", serverNode); | ||
| extNode.set("prebid", prebidNode); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same
| final ExtRequest requestExt = bidRequest.getExt(); | ||
| final ExtRequestPrebid prebid = requestExt != null ? requestExt.getPrebid() : null; | ||
| final ExtRequestPrebidServer server = prebid != null ? prebid.getServer() : null; | ||
| return server != null ? server.getEndpoint() : null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please use Optional here
| public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) { | ||
| final List<BidderError> errors = new ArrayList<>(); | ||
|
|
||
| final var response = httpCall.getResponse(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please don't use var
| private BidExtNativery parseNativeryExt(ObjectNode bidExt) { | ||
| if (bidExt == null) { | ||
| throw new PreBidException("missing bid.ext"); | ||
| } | ||
| final JsonNode node = bidExt.get("nativery"); | ||
| if (!(node instanceof ObjectNode nativeryNode)) { | ||
| throw new PreBidException("missing bid.ext.nativery"); | ||
| } | ||
| try { | ||
| return mapper.mapper().convertValue(nativeryNode, BidExtNativery.class); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage()); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd simplify it to the optional chain like
return Optional.ofNullable(bidExt)
.map(ext -> ext.get("nativery"))
.filter(JsonNode::isObject)
.map(/*convertValue*/)
.orElseThrow(() -> new PreBidException("missing bid.ext.nativery"));| assertThat(result.getValue()) | ||
| .extracting(HttpRequest::getPayload) | ||
| .flatExtracting(BidRequest::getImp) | ||
| .extracting(Imp::getId) | ||
| .containsExactly("imp1"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the assert is missing, it should check the request.payload.impIds instead
actually you can merge this test with the makeHttpRequestsShouldMakeOneRequestPerImp
| public void makeHttpRequestsShouldPreserveOriginalExtFields() { | ||
| // given | ||
| final ExtRequest extRequest = ExtRequest.empty(); | ||
| extRequest.addProperty("accountId", mapper.convertValue("acc-123", JsonNode.class)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extRequest.addProperty("accountId", TextNode.valueOf("acc-123"));
| final ObjectNode serverNode = mapper.createObjectNode(); | ||
| serverNode.put("endpoint", Endpoint.openrtb2_amp.value()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there is a ExtRequestPrebidServer class that can be used
| final ObjectNode extNode = mapper.createObjectNode(); | ||
| extNode.set("prebid", prebidNode); | ||
|
|
||
| final ExtRequest extRequest = mapper.convertValue(extNode, ExtRequest.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just build the ExtRequest object - no need to convert
| assertThat(result.getValue()) | ||
| .extracting(BidderBid::getType) | ||
| .containsExactly(BidType.xNative); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally the whole BidderBid object should be checked
| .hasSize(1) | ||
| .allSatisfy(error -> { | ||
| assertThat(error.getMessage()) | ||
| .contains("unrecognized bid_ad_media_type in response from nativery: audio"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isEqualTo
| try { | ||
| return mapper.mapper().convertValue(node, BidExtNativery.class); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please extract into a separate method
| } | ||
|
|
||
| private BidExtNativery parseNativeryExt(ObjectNode bidExt) { | ||
| return Optional.of(bidExt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the last time there was a non-null check, why it's not here anymore?
| assertThat(result.getValue()).hasSize(2) | ||
| .extracting(HttpRequest::getPayload) | ||
| .extracting(BidRequest::getImp) | ||
| .extracting(List::size) | ||
| .containsOnly(1); | ||
| assertThat(result.getValue()) | ||
| .extracting(HttpRequest::getPayload) | ||
| .flatExtracting(BidRequest::getImp) | ||
| .extracting(Imp::getId) | ||
| .containsExactlyInAnyOrder("123", "321"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of check is missed
assertThat(results.getValue()).hasSize(2)
.extracting(HttpRequest::getImpIds)
.containsExactly(Set.of("123"), Set.of("321"));| assertThat(result.getValue()) | ||
| .extracting(BidderBid::getType) | ||
| .containsExactly(BidType.banner); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's better to assert the whole BidderBid object
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can’t write:
.containsExactly(BidderBid.of(Bid.builder().impid("123").build(), video, "EUR"));
because
the production code in NativeryBidder.makeBids always overrides bid.ext, setting it to a structure like:
{
"prebid": {
"meta": {
"mediaType": ...,
"advertiserDomains": [...]
}
}
}
The expected object has ext == null, so containsExactly(...) compares the full objects and detects a difference in the bid.ext field.
To make the assertion pass, you would need to construct the expected Bid with the same ext, or narrow the assertion to only check the type.
That’s why I used:
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.banner);
(Other adapters use this approach too, for example playdigo.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal of the test should be checking the bid is correct, not just a type of this bid. So narrowing the assertion might hide the potential issues. IMHO
| try { | ||
| return mapper.mapper().convertValue(node, BidExtNativery.class); | ||
| } catch (IllegalArgumentException e) { | ||
| throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please extract into a separate method
| } | ||
|
|
||
| private BidExtNativery parseNativeryExt(ObjectNode bidExt) { | ||
| return Optional.of(bidExt) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the last time there was a non-null check, why it's not here anymore?
| final Bid expectedBid = Bid.builder() | ||
| .impid("123") | ||
| .ext(mapper.valueToTree(ExtPrebid.of( | ||
| ExtBidPrebid.builder() | ||
| .meta(org.prebid.server.proto.openrtb.ext.response.ExtBidPrebidMeta.builder() | ||
| .mediaType("banner") | ||
| .advertiserDomains(List.of()) | ||
| .build()) | ||
| .build(), | ||
| null))) | ||
| .build(); | ||
|
|
||
| final BidderBid expected = BidderBid.of(expectedBid, banner, DEFAULT_CURRENCY); | ||
|
|
||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsExactly(expected); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please check the whole bids for other cases as well
🔧 Type of changes
✨ What's the context?
#4198