Skip to content

Conversation

@przemkaczmarek
Copy link
Collaborator

🔧 Type of changes

  • new bid adapter

✨ What's the context?

#4198

@przemkaczmarek przemkaczmarek self-assigned this Oct 2, 2025
@osulzhenko osulzhenko linked an issue Oct 3, 2025 that may be closed by this pull request
- video
- native
supported-vendors: []
vendor-id: 0
Copy link
Collaborator

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: []
Copy link
Collaborator

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:

Comment on lines 31 to 33
BidderDeps nativeryBidderDeps(BidderConfigurationProperties nativeryConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {
Copy link
Collaborator

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) {
Copy link
Collaborator

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())) {
Copy link
Collaborator

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))
Copy link
Collaborator

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

Comment on lines 141 to 148
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"));
}
Copy link
Collaborator

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?

Copy link
Collaborator

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


private BidderBid resolveBidderBid(Bid bid, String currency, List<BidderError> errors) {
try {
final ObjectNode nativeryExt = extractNativeryExt(bid.getExt());
Copy link
Collaborator

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

Copy link
Collaborator

Choose a reason for hiding this comment

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

^^

Comment on lines 193 to 202
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;
}
Copy link
Collaborator

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
Copy link
Collaborator

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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Result.withErrors(errors)

Comment on lines 86 to 88
final ObjectNode originalExt = request.getExt() != null
? mapper.mapper().convertValue(request.getExt(), ObjectNode.class)
: null;
Copy link
Collaborator

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

Comment on lines 117 to 119
final ExtPrebid<?, ExtImpNativery> ext =
mapper.mapper().convertValue(imp.getExt(), NATIVERY_EXT_TYPE_REFERENCE);
return ext != null ? ext.getBidder() : null;
Copy link
Collaborator

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

Comment on lines 125 to 138
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);
}
Copy link
Collaborator

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());
Copy link
Collaborator

Choose a reason for hiding this comment

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

^^

Comment on lines 141 to 148
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"));
}
Copy link
Collaborator

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

Comment on lines 141 to 144
final var response = httpCall.getResponse();
if (response == null || StringUtils.isBlank(response.getBody())) {
return Result.withError(BidderError.badServerResponse("Empty response"));
}
Copy link
Collaborator

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

Comment on lines 178 to 179
final List<String> advDomains = ListUtils.defaultIfNull(
nativeryExt.getBidAdvDomains(), Collections.emptyList());
Copy link
Collaborator

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.

Comment on lines 215 to 222
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());
};
}
Copy link
Collaborator

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

Comment on lines 227 to 228
final List<String> safeAdvDomains = Optional.ofNullable(advDomains)
.orElse(Collections.emptyList());
Copy link
Collaborator

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

Comment on lines 11 to 15
@JsonProperty("bid_ad_media_type")
String bidAdMediaType;

@JsonProperty("bid_adv_domains")
List<String> bidAdvDomains;
Copy link
Collaborator

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

Comment on lines 133 to 134
final ObjectNode extNode = mapper.createObjectNode();
extNode.put("accountId", "acc-123");
Copy link
Collaborator

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

Comment on lines 164 to 169
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);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same

@And1sS And1sS requested a review from AntoxaAntoxic November 5, 2025 11:56
Comment on lines 105 to 108
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;
Copy link
Collaborator

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();
Copy link
Collaborator

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

Comment on lines 185 to 198
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());
}
}
Copy link
Collaborator

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"));

Comment on lines 89 to 93
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.flatExtracting(BidRequest::getImp)
.extracting(Imp::getId)
.containsExactly("imp1");
Copy link
Collaborator

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));
Copy link
Collaborator

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"));

Comment on lines 154 to 155
final ObjectNode serverNode = mapper.createObjectNode();
serverNode.put("endpoint", Endpoint.openrtb2_amp.value());
Copy link
Collaborator

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);
Copy link
Collaborator

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

Comment on lines 312 to 314
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.xNative);
Copy link
Collaborator

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");
Copy link
Collaborator

Choose a reason for hiding this comment

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

isEqualTo

Comment on lines 193 to 197
try {
return mapper.mapper().convertValue(node, BidExtNativery.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
}
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Comment on lines +69 to +78
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");
Copy link
Collaborator

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"));

Comment on lines 292 to 294
assertThat(result.getValue())
.extracting(BidderBid::getType)
.containsExactly(BidType.banner);
Copy link
Collaborator

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

Copy link
Collaborator Author

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.)

Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic Nov 14, 2025

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

Comment on lines 193 to 197
try {
return mapper.mapper().convertValue(node, BidExtNativery.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("invalid bid.ext.nativery: " + e.getMessage());
}
Copy link
Collaborator

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)
Copy link
Collaborator

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?

Comment on lines 296 to 311
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);
Copy link
Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Port PR from PBS-Go: New Adapter: Nativery

4 participants