Skip to content

Comments

Nativo: Add optional placementId parameter and SDK Renderer Version in response#4380

Open
rafataveira wants to merge 3 commits intoprebid:masterfrom
rafataveira:master
Open

Nativo: Add optional placementId parameter and SDK Renderer Version in response#4380
rafataveira wants to merge 3 commits intoprebid:masterfrom
rafataveira:master

Conversation

@rafataveira
Copy link
Contributor

@rafataveira rafataveira commented Feb 6, 2026

This pull request adds native support for the Nativo bidder to the codebase, moving it from a generic alias-based configuration to a dedicated, fully integrated bidder module. The changes include the implementation of the Nativo bidder logic, configuration, schema, and integration tests, as well as the removal of the old alias-based approach.

Nativo Bidder Integration:

  • Implemented the NativoBidder class, handling bid requests and responses, bid extension updates, and error handling. (NativoBidder.java)
  • Defined the Nativo-specific impression extension model ExtImpNativo. (ExtImpNativo.java)
  • Added a dedicated configuration class for Nativo, including dependency injection and usersyncer setup. (NativoConfiguration.java)
  • Created a Nativo-specific bidder configuration file. (nativo.yaml)
  • Added a JSON schema for validating Nativo bidder parameters. (nativo.json)

Testing and Documentation:

  • Added integration tests for the Nativo bidder, including request/response fixtures. (NativoTest.java, test-auction-nativo-request.json, test-auction-nativo-response.json, test-nativo-bid-request.json, test-nativo-bid-response.json) [1] [2] [3] [4] [5]

Configuration and Cleanup:

  • Removed the Nativo alias configuration from the generic bidder config and related test properties, fully switching to the dedicated Nativo adapter. (generic.yaml, test-application.properties, PbsConfig.groovy, BidderParamsSpec.groovy) [1] [2] [3] [4] [5]

These changes ensure that Nativo is now a first-class, configurable bidder in the system, with dedicated code, configuration, validation, and tests.

Schema validation:

  • Introduced a new JSON schema file nativo.json to define and validate the parameters accepted by the Nativo adapter.

🔧 Type of changes

  • new bid adapter
  • bid adapter update
  • new feature
  • new analytics adapter
  • new module
  • module update
  • bugfix
  • documentation
  • configuration
  • dependency update
  • tech debt (test coverage, refactorings, etc.)

✨ What's the context?

Adding optional property "placementId" to Nativo Adapter
Adding SDK Renderer Version in Response

🧠 Rationale behind the change

We have already been using this property but it wasn't correctly documented in the adapter.
Our SDK needs the SDK Renderer version to correctly work.

🔎 New Bid Adapter Checklist

  • verify email contact works
  • NO fully dynamic hostnames
  • geographic host parameters are NOT required
  • direct use of HTTP is prohibited - implement an existing Bidder interface that will do all the job
  • if the ORTB is just forwarded to the endpoint, use the generic adapter - define the new adapter as the alias of the generic adapter
  • cover an adapter configuration with an integration test

🧪 Test plan

How do you know the changes are safe to ship to production?
Added all the necessary tests.

🏎 Quality check

  • Are your changes following our code style guidelines?
  • Are there any breaking changes in your code?
  • Does your test coverage exceed 90%?
  • Are there any erroneous console logs, debuggers or leftover code in your changes?

@rafataveira rafataveira changed the title Nativo: Add optional placementId parameter Nativo: Add optional placementId parameter and SDK Renderer Version in response Feb 18, 2026
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see that in GO version this adapter supports Ortb2.6

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated

Comment on lines 6 to 8
/**
* Defines the contract for bidrequest.imp[i].ext.nativo
*/
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 comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 43 to 44
private static final TypeReference<ExtPrebid<ExtBidPrebid, ObjectNode>> EXT_PREBID_TYPE_REFERENCE =
new TypeReference<>() { };
Copy link
Collaborator

Choose a reason for hiding this comment

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

    private static final TypeReference<ExtPrebid<ExtBidPrebid, ?>> EXT_PREBID_TYPE_REFERENCE =
            new TypeReference<>() {
            };

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored

Comment on lines 77 to 155
private List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, List<BidderError> errors) {
final Map<String, Imp> impMap = bidRequest.getImp().stream()
.collect(Collectors.toMap(Imp::getId, Function.identity()));
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(seatBid -> seatBid.getBid().stream()
.filter(Objects::nonNull)
.map(bid -> updateBid(bid, bidRequest, errors))
.filter(Objects::nonNull)
.map(bid -> BidderBid.of(bid, BidderUtil.getBidType(bid, impMap), bidResponse.getCur()))
.toList())
.flatMap(Collection::stream)
.toList();
}

private Bid updateBid(Bid bid, BidRequest bidRequest, List<BidderError> errors) {
final ObjectNode updateBidExt;
try {
updateBidExt = prepareBidExt(bid, bidRequest);
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return bid;
}

return bid.toBuilder()
.ext(updateBidExt)
.build();
}

private ObjectNode prepareBidExt(Bid bid, BidRequest bidRequest) {
final ObjectNode bidExt = bid.getExt();

final String nativoRendererVersion = getNativoRendererVersion(bidRequest);
if (nativoRendererVersion != null) {

final ExtPrebid<ExtBidPrebid, ObjectNode> extPrebid = getExtPrebid(bidExt, bid.getId());
final ExtBidPrebid extBidPrebid = extPrebid != null ? extPrebid.getPrebid() : null;

final ExtBidPrebidMeta meta = Optional.ofNullable(extBidPrebid)
.map(ExtBidPrebid::getMeta)
.orElse(null);

final ExtBidPrebidMeta updatedMeta = Optional.ofNullable(meta)
.map(ExtBidPrebidMeta::toBuilder)
.orElseGet(ExtBidPrebidMeta::builder)
.rendererVersion(nativoRendererVersion)
.build();

final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid != null
? extBidPrebid.toBuilder().meta(updatedMeta).build()
: ExtBidPrebid.builder().meta(updatedMeta).build();

final ObjectNode updatedBidExt = Optional.ofNullable(bidExt).orElseGet(mapper.mapper()::createObjectNode);
updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(modifiedExtBidPrebid));
return updatedBidExt;
}

return bidExt;
}

private String getNativoRendererVersion(BidRequest bidRequest) {
return Optional.ofNullable(bidRequest.getExt())
.map(ExtRequest::getPrebid)
.map(ExtRequestPrebid::getSdk)
.map(ExtRequestPrebidSdk::getRenderers)
.orElse(Collections.emptyList())
.stream()
.filter(renderer -> NATIVO_RENDERER_NAME.equals(renderer.getName()))
.map(ExtRequestPrebidSdkRenderer::getVersion)
.findFirst()
.orElse(null);
}

private ExtPrebid<ExtBidPrebid, ObjectNode> getExtPrebid(ObjectNode bidExt, String bidId) {
try {
return bidExt != null ? mapper.mapper().convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE) : null;
} catch (IllegalArgumentException e) {
throw new PreBidException("Invalid ext passed in bid with id: " + bidId);
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

    private List<BidderBid> bidsFromResponse(BidRequest bidRequest, BidResponse bidResponse, List<BidderError> errors) {
        final Map<String, Imp> impMap = bidRequest.getImp().stream()
                .collect(Collectors.toMap(Imp::getId, Function.identity()));
        final String rendererVersion = getNativoRendererVersion(bidRequest);

        return bidResponse.getSeatbid().stream()
                .filter(Objects::nonNull)
                .map(SeatBid::getBid)
                .filter(Objects::nonNull)
                .flatMap(Collection::stream)
                .filter(Objects::nonNull)
                .map(bid -> updateBid(bid, rendererVersion, errors))
                .map(bid -> BidderBid.of(bid, BidderUtil.getBidType(bid, impMap), bidResponse.getCur()))
                .toList();
    }

    private static String getNativoRendererVersion(BidRequest bidRequest) {
        return Optional.ofNullable(bidRequest.getExt())
                .map(ExtRequest::getPrebid)
                .map(ExtRequestPrebid::getSdk)
                .map(ExtRequestPrebidSdk::getRenderers)
                .orElse(Collections.emptyList())
                .stream()
                .filter(renderer -> NATIVO_RENDERER_NAME.equals(renderer.getName()))
                .map(ExtRequestPrebidSdkRenderer::getVersion)
                .findFirst()
                .orElse(null);
    }

    private Bid updateBid(Bid bid, String rendererVersion, List<BidderError> errors) {
        if (rendererVersion == null) {
            return bid;
        }

        final ObjectNode updateBidExt;
        try {
            updateBidExt = updateBidExt(bid, rendererVersion);
        } catch (PreBidException e) {
            errors.add(BidderError.badServerResponse(e.getMessage()));
            return bid;
        }

        return bid.toBuilder().ext(updateBidExt).build();
    }

    private ObjectNode updateBidExt(Bid bid, String rendererVersion) {
        final ObjectNode bidExt = bid.getExt();
        final Optional<ExtBidPrebid> extBidPrebid = Optional.ofNullable(bidExt)
                .map(ext -> parseExtBidPrebid(bidExt, bid.getId()));

        final ExtBidPrebidMeta updatedMeta = extBidPrebid
                .map(ExtBidPrebid::getMeta)
                .map(ExtBidPrebidMeta::toBuilder)
                .orElseGet(ExtBidPrebidMeta::builder)
                .rendererVersion(rendererVersion)
                .build();

        final ExtBidPrebid modifiedExtBidPrebid = extBidPrebid
                .map(ExtBidPrebid::toBuilder)
                .orElseGet(ExtBidPrebid::builder)
                .meta(updatedMeta)
                .build();

        final ObjectNode updatedBidExt = bidExt != null ? bidExt : mapper.mapper().createObjectNode();
        updatedBidExt.set(PREBID_EXT, mapper.mapper().valueToTree(modifiedExtBidPrebid));
        return updatedBidExt;
    }

    private ExtBidPrebid parseExtBidPrebid(ObjectNode bidExt, String bidId) {
        try {
            return mapper.mapper().convertValue(bidExt, EXT_PREBID_TYPE_REFERENCE).getPrebid();
        } catch (IllegalArgumentException e) {
            throw new PreBidException("Invalid ext passed in bid with id: " + bidId);
        }
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored

Comment on lines 268 to 285
@Test
public void makeHttpRequestsShouldPassThroughRequestWithIntegerPlacementId() {
// given
final ObjectNode impExt = mapper.createObjectNode();
impExt.putObject("bidder").put("placementId", 12345678);
final BidRequest bidRequest = BidRequest.builder()
.imp(singletonList(Imp.builder().id("imp1").banner(Banner.builder().build()).ext(impExt).build()))
.build();

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1);
assertThat(result.getValue().get(0).getPayload().getImp().get(0).getExt()
.path("bidder").path("placementId").intValue()).isEqualTo(12345678);
}
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 test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 287 to 304
@Test
public void makeHttpRequestsShouldPassThroughRequestWithStringPlacementId() {
// given
final ObjectNode impExt = mapper.createObjectNode();
impExt.putObject("bidder").put("placementId", "string-placement-id");
final BidRequest bidRequest = BidRequest.builder()
.imp(singletonList(Imp.builder().id("imp1").banner(Banner.builder().build()).ext(impExt).build()))
.build();

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1);
assertThat(result.getValue().get(0).getPayload().getImp().get(0).getExt()
.path("bidder").path("placementId").textValue()).isEqualTo("string-placement-id");
}
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 test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 306 to 323
@Test
public void makeHttpRequestsShouldPassThroughRequestWithAbsentPlacementId() {
// given
final ObjectNode impExt = mapper.createObjectNode();
impExt.putObject("bidder");
final BidRequest bidRequest = BidRequest.builder()
.imp(singletonList(Imp.builder().id("imp1").banner(Banner.builder().build()).ext(impExt).build()))
.build();

// when
final Result<List<HttpRequest<BidRequest>>> result = target.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1);
assertThat(result.getValue().get(0).getPayload().getImp().get(0).getExt()
.path("bidder").has("placementId")).isFalse();
}
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 test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

Comment on lines 325 to 359
@Test
public void extImpNativoShouldDeserializeIntegerPlacementId() throws JsonProcessingException {
// given
final String json = "{\"placementId\": 12345678}";

// when
final ExtImpNativo extImpNativo = mapper.readValue(json, ExtImpNativo.class);

// then
assertThat(extImpNativo.getPlacementId()).isEqualTo(12345678);
}

@Test
public void extImpNativoShouldDeserializeStringPlacementId() throws JsonProcessingException {
// given
final String json = "{\"placementId\": \"string-placement-id\"}";

// when
final ExtImpNativo extImpNativo = mapper.readValue(json, ExtImpNativo.class);

// then
assertThat(extImpNativo.getPlacementId()).isEqualTo("string-placement-id");
}

@Test
public void extImpNativoShouldDeserializeAbsentPlacementIdAsNull() throws JsonProcessingException {
// given
final String json = "{}";

// when
final ExtImpNativo extImpNativo = mapper.readValue(json, ExtImpNativo.class);

// then
assertThat(extImpNativo.getPlacementId()).isNull();
}
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 tests

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants