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
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 226 additions & 0 deletions src/main/java/org/prebid/server/bidder/sparteo/SparteoBidder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
package org.prebid.server.bidder.sparteo;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.request.Publisher;
import com.iab.openrtb.request.Site;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.apache.commons.collections4.CollectionUtils;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderCall;
import org.prebid.server.bidder.model.BidderError;
import org.prebid.server.bidder.model.HttpRequest;
import org.prebid.server.bidder.model.Result;
import org.prebid.server.exception.PreBidException;
import org.prebid.server.json.DecodeException;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
import org.prebid.server.proto.openrtb.ext.request.ExtPublisher;
import org.prebid.server.proto.openrtb.ext.request.sparteo.ExtImpSparteo;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

public class SparteoBidder implements Bidder<BidRequest> {

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 bidderParams = parseExtImp(imp);

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

final ObjectNode modifiedExt = modifyImpExt(imp);

modifiedImps.add(imp.toBuilder().ext(modifiedExt).build());
} catch (PreBidException 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 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()) {
final Map.Entry<String, JsonNode> field = fields.next();
paramsNode.set(field.getKey(), field.getValue());
}
}
return modifiedImpExt;
}

private Site modifySite(Site site, String siteNetworkId, JacksonMapper mapper) {
if (site == null || site.getPublisher() == null || siteNetworkId == null) {
return site;
}

final Publisher publisher = site.getPublisher();

final ExtPublisher extPublisher = publisher.getExt() != null
? publisher.getExt()
: ExtPublisher.empty();

final JsonNode paramsProperty = extPublisher.getProperty("params");
final ObjectNode paramsNode;

if (paramsProperty != null && paramsProperty.isObject()) {
paramsNode = (ObjectNode) paramsProperty;
} else {
paramsNode = mapper.mapper().createObjectNode();
extPublisher.addProperty("params", paramsNode);
}

paramsNode.put("networkId", siteNetworkId);

final Publisher modifiedPublisher = publisher.toBuilder()
.ext(extPublisher)
.build();

return site.toBuilder()
.publisher(modifiedPublisher)
.build();
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderError> errors = new ArrayList<>();
return Result.of(extractBids(bidResponse, errors), errors);
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
return Collections.emptyList();
}

return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> toBidderBid(bid, bidResponse.getCur(), errors))
.filter(Objects::nonNull)
.toList();
}

private BidType getBidType(Bid bid) throws PreBidException {
final BidType bidType = Optional.ofNullable(bid.getExt())
.map(ext -> ext.get("prebid"))
.filter(JsonNode::isObject)
.map(this::parseExtBidPrebid)
.map(ExtBidPrebid::getType)
.orElseThrow(() -> new PreBidException(
"Failed to parse bid mediatype for impression \"%s\"".formatted(bid.getImpid())));

if (bidType == BidType.audio) {
throw new PreBidException(
"Audio bid type not supported by this adapter for impression id: %s".formatted(bid.getImpid()));
}

return bidType;
}

private ExtBidPrebid parseExtBidPrebid(JsonNode prebidNode) {
try {
return mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class);
} catch (JsonProcessingException e) {
return null;
}
}

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 ?

try {
final BidType bidType = getBidType(bid);

final Integer mtype = switch (bidType) {
case banner -> 1;
case video -> 2;
case xNative -> 4;
default -> null;
};

final Bid bidWithMtype = mtype != null ? bid.toBuilder().mtype(mtype).build() : bid;

return BidderBid.of(bidWithMtype, bidType, currency);
} catch (PreBidException e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.prebid.server.proto.openrtb.ext.request.sparteo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

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

@JsonProperty("networkId")
String networkId;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.prebid.server.spring.config.bidder;

import org.prebid.server.bidder.BidderDeps;
import org.prebid.server.bidder.sparteo.SparteoBidder;
import org.prebid.server.json.JacksonMapper;
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
import org.prebid.server.spring.env.YamlPropertySourceFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import jakarta.validation.constraints.NotBlank;

@Configuration
@PropertySource(value = "classpath:/bidder-config/sparteo.yaml",
factory = YamlPropertySourceFactory.class)
public class SparteoConfiguration {

private static final String BIDDER_NAME = "sparteo";

@Bean("sparteoConfigurationProperties")
@ConfigurationProperties("adapters.sparteo")
public BidderConfigurationProperties configurationProperties() {
return new BidderConfigurationProperties();
}

@Bean
public BidderDeps sparteoBidderDeps(BidderConfigurationProperties sparteoConfigurationProperties,
@NotBlank @Value("${external-url}") String externalUrl,
JacksonMapper mapper) {

return BidderDepsAssembler.forBidder(BIDDER_NAME)
.withConfig(sparteoConfigurationProperties)
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
.bidderCreator(config -> new SparteoBidder(config.getEndpoint(), mapper))
.assemble();
}
}
20 changes: 20 additions & 0 deletions src/main/resources/bidder-config/sparteo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
adapters:
sparteo:
endpoint: https://bid.sparteo.com/s2s-auction
meta-info:
maintainer-email: [email protected]
app-media-types:
- banner
- video
- native
site-media-types:
- banner
- video
- native
supported-vendors:
vendor-id: 1028
usersync:
cookie-family-name: sparteo
iframe:
url: "https://sync.sparteo.com/s2s_sync?gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&redirect_url={{redirect_url}}"
support-cors: true
34 changes: 34 additions & 0 deletions src/main/resources/static/bidder-params/sparteo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Sparteo Params",
"type": "object",
"properties": {
"networkId": {
"type": "string",
"description": "Sparteo network ID. This information will be given to you by the Sparteo team."
},
"custom1": {
"type": "string",
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
},
"custom2": {
"type": "string",
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
},
"custom3": {
"type": "string",
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
},
"custom4": {
"type": "string",
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
},
"custom5": {
"type": "string",
"description": "To be used in reporting. Alphanumeric strings ; case sensitive ; max 40 characters ; only allowed symbols are hyphen and underscore."
}
},
"required": [
"networkId"
]
}
Loading