New Adapter: Nativery#4223
Conversation
| - video | ||
| - native | ||
| supported-vendors: [] | ||
| vendor-id: 0 |
| - banner | ||
| - video | ||
| - native | ||
| supported-vendors: [] |
There was a problem hiding this comment.
leave it just as supported-vendors:
| BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties, | ||
| @NotBlank @Value("${external-url}") String externalUrl, | ||
| JacksonMapper mapper) { |
| return mapper.mapper().convertValue(root, ExtRequest.class); | ||
| } | ||
|
|
||
| private boolean isAmpRequest(BidRequest request) { |
There was a problem hiding this comment.
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.
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.
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.
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?
|
|
||
| private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) { | ||
| try { | ||
| final ObjectNode nativeryExt = extractNativeryExt(bid.getExt()); |
There was a problem hiding this comment.
let's create a separate BidExtNativery object and deserialize with the object mapper, because that manual conversion feels bad
| 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.
Replace with a simple Optional chain
| return Result.of(Collections.emptyList(), errors); | ||
| } | ||
|
|
||
| // 🟢 Zmienione — zamieniamy ExtRequest na ObjectNode |
There was a problem hiding this comment.
please remove the comments
| } | ||
|
|
||
| if (validImps.isEmpty()) { | ||
| return Result.of(Collections.emptyList(), errors); |
There was a problem hiding this comment.
Result.withErrors(errors)
| final ObjectNode originalExt = request.getExt() != null | ||
| ? mapper.mapper().convertValue(request.getExt(), ObjectNode.class) | ||
| : null; |
There was a problem hiding this comment.
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.
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.
please use ExtRequest and FlexibleExtension capabilities
|
|
||
| private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) { | ||
| try { | ||
| final ObjectNode nativeryExt = extractNativeryExt(bid.getExt()); |
| 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")); | ||
| } |
| final var response = httpCall.getResponse(); | ||
| if (response == null || StringUtils.isBlank(response.getBody())) { | ||
| return Result.withError(BidderError.badServerResponse("Empty response")); | ||
| } |
| final List<String> advDomains = ListUtils.defaultIfNull( | ||
| nativeryExt.getBidAdvDomains(), Collections.emptyList()); |
There was a problem hiding this comment.
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.
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.
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.
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.
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); |
| 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.
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.
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.
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.
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.
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.
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.
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.
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"); |
| 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.
please extract into a separate method
| } | ||
|
|
||
| private BidExtNativery parseNativeryExt(ObjectNode bidExt) { | ||
| return Optional.of(bidExt) |
There was a problem hiding this comment.
the last time there was a non-null check, why it's not here anymore?
| assertThat(result.getValue()) | ||
| .extracting(BidderBid::getType) | ||
| .containsExactly(BidType.banner); |
There was a problem hiding this comment.
it's better to assert the whole BidderBid object
There was a problem hiding this comment.
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.
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.
please extract into a separate method
| } | ||
|
|
||
| private BidExtNativery parseNativeryExt(ObjectNode bidExt) { | ||
| return Optional.of(bidExt) |
There was a problem hiding this comment.
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.
please check the whole bids for other cases as well
There was a problem hiding this comment.
the requested changes were about asserting the bidder bid object in other tests where it's applicable in the same way you did it in the makeBidsShouldReturnBannerBidForRichMediaType
There was a problem hiding this comment.
Yes, you're right. I just wanted to shorten the tests because they have 500 lines of code and check only a few cases instead of all of them.
🔧 Type of changes
✨ What's the context?
#4198