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 2 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
240 changes: 240 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,240 @@
package org.prebid.server.bidder.sparteo;

import com.fasterxml.jackson.core.JsonProcessingException;
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.json.DecodeException;
import org.prebid.server.json.JacksonMapper;
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.List;
import java.util.Objects;

public class SparteoBidder implements Bidder<BidRequest> {

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 JsonNode bidderNode = imp.getExt().get("bidder");
final ExtImpSparteo bidderParams = mapper.mapper().treeToValue(bidderNode, ExtImpSparteo.class);

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

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

modifiedImps.add(imp.toBuilder().ext(modifiedExt).build());
} catch (NullPointerException | JsonProcessingException e) {
errors.add(BidderError.badInput(
String.format("ignoring imp id=%s, error processing ext: %s",
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);
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
final List<BidderError> errors = new ArrayList<>();
final int statusCode = httpCall.getResponse().getStatusCode();

if (statusCode == 204) {
return Result.of(Collections.emptyList(), errors);
}

if (statusCode != 200) {
return Result.withError(BidderError.badServerResponse(
String.format("HTTP status %d returned from Sparteo", statusCode)));
}

try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
return Result.of(extractBids(bidResponse, errors), errors);
} catch (DecodeException e) {
return Result.withError(BidderError.badServerResponse(
String.format("Failed to decode Sparteo response: %s", e.getMessage())));
}
}

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

final ObjectNode extMap = mapper.mapper().convertValue(imp.getExt(), ObjectNode.class);

extMap.remove("bidder");

final JsonNode sparteoNode = extMap.get("sparteo");
final ObjectNode outgoingParamsNode;

if (sparteoNode != null && sparteoNode.isObject() && sparteoNode.has("params")
&& sparteoNode.get("params").isObject()) {
outgoingParamsNode = (ObjectNode) sparteoNode.get("params");
} else {
outgoingParamsNode = extMap.putObject("sparteo").putObject("params");
}

final ObjectNode bidderParamsAsNode = mapper.mapper().convertValue(bidderParams, ObjectNode.class);
outgoingParamsNode.setAll(bidderParamsAsNode);

final JsonNode prebidNode = extMap.get("prebid");
if (prebidNode != null && prebidNode.has("adunitcode")) {
outgoingParamsNode.set("adUnitCode", prebidNode.get("adunitcode"));
}

return extMap;
}

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;

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();
}

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)
.map(bid -> {
if (bid == null) {
errors.add(BidderError.badServerResponse("Received null bid object within a seatbid."));
return null;
}
return toBidderBid(bid, bidResponse.getCur(), errors);
})
.filter(Objects::nonNull)
.toList();
}

private BidderBid toBidderBid(Bid bid, String currency, List<BidderError> errors) {
try {
final BidType bidType = getBidTypeFromBidExtension(bid);
return BidderBid.of(bid, bidType, currency);
} catch (Exception e) {
errors.add(BidderError.badServerResponse(e.getMessage()));
return null;
}
}

private BidType getBidTypeFromBidExtension(Bid bid) throws Exception {
final ObjectNode bidExtNode = bid.getExt();

if (bidExtNode == null || !bidExtNode.hasNonNull("prebid")) {
throw new Exception(
String.format("Bid extension or bid.ext.prebid missing for impression id: %s",
bid.getImpid())
);
}

final JsonNode prebidNode = bidExtNode.get("prebid");
final ExtBidPrebid extBidPrebid;

try {
extBidPrebid = mapper.mapper().treeToValue(prebidNode, ExtBidPrebid.class);
} catch (JsonProcessingException e) {
throw new Exception(
String.format("Failed to parse bid.ext.prebid for impression id: %s, error: %s",
bid.getImpid(),
e.getMessage()
),
e);
}

if (extBidPrebid == null || extBidPrebid.getType() == null) {
throw new Exception(
String.format("Missing type in bid.ext.prebid for impression id: %s",
bid.getImpid()
));
}

final BidType bidType = extBidPrebid.getType();
if (bidType == BidType.audio) {
throw new Exception(
String.format("Audio bid type not supported by this adapter for impression id: %s",
bid.getImpid())
);
}

return bidType;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.prebid.server.proto.openrtb.ext.request.sparteo;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Value;

import java.util.HashMap;
import java.util.Map;

@Value(staticConstructor = "of")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExtImpSparteo {

@JsonProperty("networkId")
String networkId;

@JsonProperty("custom1")
String custom1;

@JsonProperty("custom2")
String custom2;

@JsonProperty("custom3")
String custom3;

@JsonProperty("custom4")
String custom4;

@JsonProperty("custom5")
String custom5;

Map<String, JsonNode> additionalProperties = new HashMap<>();

@JsonAnySetter
public void addAdditionalProperty(String key, JsonNode value) {
additionalProperties.put(key, value);
}

@JsonAnyGetter
public Map<String, JsonNode> getAdditionalProperties() {
return additionalProperties;
}
}
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