Skip to content

New Adapter: Sparteo #3985

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open

Conversation

t-sormonte
Copy link

@t-sormonte t-sormonte commented Jun 4, 2025

🔧 Type of changes

  • [ X] new bid adapter

✨ What's the context?

We would like to allow our partners to be able to include us in s2s bidding.
The Go PR: prebid/prebid-server#4275

🧠 Rationale behind the change

The adapter logic is the same as the one we have implemented for the GO version

🔎 New Bid Adapter Checklist

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

🧪 Test plan

How do you know the changes are safe to ship to production?
We have implemented the unit and integration test.
We have run several manual "real" tests.

🏎 Quality check

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

Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic left a comment

Choose a reason for hiding this comment

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

please make sure the Go and Java versions have feature parity before merging

@t-sormonte
Copy link
Author

t-sormonte commented Jun 25, 2025

Hi @AntoxaAntoxic ,
thank you for your time on this review, i have implemented your suggestions, please let me know if i have miss implemented some of your comments.

))
.willReturn(aResponse().withBody(
jsonFrom("openrtb2/sparteo/test-sparteo-bid-response.json")
)));
Copy link
Collaborator

Choose a reason for hiding this comment

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

closing parenthesis shouldn't be on the new line, please check the code style guide and fix everywhere

Copy link
Author

Choose a reason for hiding this comment

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

Formatted

Copy link
Collaborator

Choose a reason for hiding this comment

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

not fixed

Copy link
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 52 to 134
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final List<BidderError> errors = new ArrayList<>();
final List<Imp> modifiedImps = new ArrayList<>();
String siteNetworkId = null;

for (Imp imp : request.getImp()) {
try {
final ObjectNode impExt = mapper.mapper().convertValue(imp.getExt(), ObjectNode.class);

final JsonNode bidderNode = impExt.get("bidder");
final ExtImpSparteo bidderParams = mapper.mapper().treeToValue(bidderNode, ExtImpSparteo.class);

if (siteNetworkId == null && bidderParams.getNetworkId() != null) {
siteNetworkId = bidderParams.getNetworkId();
}

final ObjectNode modifiedExt = buildImpExt(impExt, bidderParams, mapper);

modifiedImps.add(imp.toBuilder().ext(modifiedExt).build());
} catch (JsonProcessingException e) {
errors.add(BidderError.badInput(
"ignoring imp id=%s, error processing ext: %s".formatted(
imp.getId(), e.getMessage())));
}
}

if (modifiedImps.isEmpty()) {
return Result.withErrors(errors);
}

final BidRequest outgoingRequest = request.toBuilder()
.imp(modifiedImps)
.site(modifySite(request.getSite(), siteNetworkId, mapper))
.build();

final HttpRequest<BidRequest> call = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper);

return Result.of(Collections.singletonList(call), errors);
}

private ObjectNode buildImpExt(ObjectNode impExt, ExtImpSparteo bidderParams, JacksonMapper mapper)
throws JsonProcessingException {

final String adUnitCode = Optional.ofNullable(impExt.get("prebid"))
.map(prebidNode -> prebidNode.get("adunitcode"))
.filter(JsonNode::isTextual)
.map(JsonNode::asText)
.orElse(null);

final Map<String, JsonNode> sparteoProperties = Optional.ofNullable(impExt.get("sparteo"))
.map(sparteoNode -> sparteoNode.get("params"))
.filter(JsonNode::isObject)
.map(paramsNode -> mapper.mapper().convertValue(paramsNode,
new TypeReference<Map<String, JsonNode>>() { }))
.orElse(Collections.emptyMap());

final Map<String, JsonNode> additionnalProperties = new HashMap<>(sparteoProperties);
additionnalProperties.putAll(bidderParams.getAdditionalProperties());

final ExtImpParamsSparteo sparteoParams = ExtImpParamsSparteo.of(
bidderParams.getNetworkId(),
bidderParams.getCustom1(),
bidderParams.getCustom2(),
bidderParams.getCustom3(),
bidderParams.getCustom4(),
bidderParams.getCustom5(),
adUnitCode,
additionnalProperties);

final ObjectNode ext = mapper.mapper().createObjectNode();

impExt.fields().forEachRemaining(entry -> {
final String key = entry.getKey();
if (!key.equals("bidder") && !key.equals("sparteo")) {
ext.set(key, entry.getValue());
}
});

final ObjectNode sparteoParamsAsNode = mapper.mapper().convertValue(sparteoParams, ObjectNode.class);
ext.putObject("sparteo").set("params", sparteoParamsAsNode);

return ext;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

okay, spent some time on refactoring, here is what I came up with

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

    private final String endpointUrl;
    private final JacksonMapper mapper;

    public SparteoBidder(String endpointUrl, JacksonMapper mapper) {
        this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
        this.mapper = Objects.requireNonNull(mapper);
    }

    @Override
    public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
        final List<BidderError> errors = new ArrayList<>();
        final List<Imp> modifiedImps = new ArrayList<>();
        String siteNetworkId = null;

        for (Imp imp : request.getImp()) {
            try {
                final ExtImpSparteo extImp = parseExtImp(imp);
                if (siteNetworkId == null && extImp.getNetworkId() != null) {
                    siteNetworkId = extImp.getNetworkId();
                }
            } catch (PreBidException e) {
                errors.add(BidderError.badInput("ignoring imp id=%s, error processing ext: %s".formatted(imp.getId(), e.getMessage())));
            }

            modifiedImps.add(imp.toBuilder().ext(modifyImpExt(imp)).build());
        }

        if (modifiedImps.isEmpty()) {
            return Result.withErrors(errors);
        }

        final BidRequest outgoingRequest = request.toBuilder()
                .imp(modifiedImps)
                .site(modifySite(request.getSite(), siteNetworkId, mapper))
                .build();

        final HttpRequest<BidRequest> call = BidderUtil.defaultRequest(outgoingRequest, endpointUrl, mapper);

        return Result.of(Collections.singletonList(call), errors);
    }

    private ExtImpSparteo parseExtImp(Imp imp) {
        try {
            return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
        } catch (IllegalArgumentException e) {
            throw new PreBidException("invalid imp.ext");
        }
    }

    private static ObjectNode modifyImpExt(Imp imp) {
        final ObjectNode modifiedImpExt = imp.getExt().deepCopy();
        final JsonNode sparteoJsonNode = modifiedImpExt.get("sparteo");
        final ObjectNode sparteoNode = sparteoJsonNode == null || !sparteoJsonNode.isObject()
                ? modifiedImpExt.putObject("sparteo")
                : (ObjectNode) sparteoJsonNode;

        final JsonNode paramsJsonNode = sparteoNode.get("params");
        final ObjectNode paramsNode = paramsJsonNode == null || !paramsJsonNode.isObject()
                ? sparteoNode.putObject("params")
                : (ObjectNode) paramsJsonNode;

        final JsonNode bidderJsonNode = modifiedImpExt.remove("bidder");
        if (bidderJsonNode != null && bidderJsonNode.isObject()) {
            final Iterator<Map.Entry<String, JsonNode>> fields = bidderJsonNode.fields();
            while (fields.hasNext()) {
                Map.Entry<String, JsonNode> field = fields.next();
                paramsNode.set(field.getKey(), field.getValue());
            }
        }
        return modifiedImpExt;
    }

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, now we don't need ExtImpParamsSparteo

having separate DTOs was bad idea from my side according to the Go version because there is no strict list of fields we can potentially receive within the imp.ext

also ExtImpSparteo can be shrink to the following

@Value(staticConstructor = "of")
public class ExtImpSparteo {

    @JsonProperty("networkId")
    String networkId;
}

all the custom params will be copied to the outgoing DTO anyway

Copy link
Collaborator

Choose a reason for hiding this comment

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

please double-check in case I have missed something and sorry for confusion

Copy link
Author

@t-sormonte t-sormonte Jun 27, 2025

Choose a reason for hiding this comment

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

Hey @AntoxaAntoxic ,
No problem, i admit that having no strict list of fields could misled.
Thank you to have spent time to refactor it. I have implemented the changes i just have changed the loop logic to include the
modifiedImps.add(imp.toBuilder().ext(modifyImpExt(imp)).build());
into the try block.

Please let me know if you see anything else.

Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic left a comment

Choose a reason for hiding this comment

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

the bidder itself looks good, some comments on the test class

}
}

private BidderBid toBidderBid(Bid bid, String currency, List<BidderError> errors) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

the method should be higher

Copy link
Author

Choose a reason for hiding this comment

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

What is the rule behind ? I did an alphabetic order for private methods. Where should i put this method ?

Comment on lines 164 to 195
final HttpRequest<BidRequest> httpRequest = result.getValue().get(0);
assertThat(httpRequest.getMethod())
.isEqualTo(HttpMethod.POST);
assertThat(httpRequest.getUri())
.isEqualTo(ENDPOINT_URL);

final BidRequest outgoingRequest = httpRequest.getPayload();
assertThat(outgoingRequest.getImp())
.hasSize(1);
final ObjectNode modifiedImpExt = outgoingRequest.getImp().get(0).getExt();
assertThat(modifiedImpExt
.get("sparteo")
.get("params")
.get("networkId")
.asText())
.isEqualTo("testNetworkId");
assertThat(modifiedImpExt
.get("sparteo")
.get("params")
.get("customParam")
.asText())
.isEqualTo("customValue");
assertThat(modifiedImpExt.has("bidder"))
.isFalse();

final ExtPublisher publisherExt = (ExtPublisher) outgoingRequest.getSite().getPublisher().getExt();
assertThat(publisherExt
.getProperties()
.get("params")
.get("networkId")
.asText())
.isEqualTo("testNetworkId");
Copy link
Collaborator

Choose a reason for hiding this comment

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

please utilize assertj extracting methods whenever it's possible

Copy link
Author

Choose a reason for hiding this comment

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

updated but i'm not 100% i did it the right way, do not hesitate to tell me if it is not what you expected

Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic Jul 1, 2025

Choose a reason for hiding this comment

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

it's especially handy while working with lists of objects instead of comparing one by one with the duplicated code

@AntoxaAntoxic AntoxaAntoxic linked an issue Jun 30, 2025 that may be closed by this pull request
Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic left a comment

Choose a reason for hiding this comment

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

Overall, looks good. Only minor issues are left. Thank you!

@t-sormonte t-sormonte force-pushed the New-Adapter--Sparteo branch from 53c2d04 to 5f5206f Compare July 1, 2025 14:31
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: Sparteo
2 participants