Skip to content

New adapter: HypeLab#4488

Open
Minebomber wants to merge 1 commit into
prebid:masterfrom
gohypelab:feat/hypelab-adapter
Open

New adapter: HypeLab#4488
Minebomber wants to merge 1 commit into
prebid:masterfrom
gohypelab:feat/hypelab-adapter

Conversation

@Minebomber
Copy link
Copy Markdown

@Minebomber Minebomber commented May 6, 2026

🔧 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?

This PR adds the HypeLab bid adapter to Prebid Server Java.

The adapter supports site banner, native, and video inventory. It accepts property_slug and placement_slug bidder params, forwards eligible OpenRTB 2.6 requests to HypeLab, adds HypeLab display manager metadata, and maps HypeLab bid responses back into Prebid bid types.

🧠 Rationale behind the change

HypeLab needs a dedicated adapter because the request is not just a raw OpenRTB pass-through. The adapter validates required bidder params, sets tagid from placement_slug, preserves the HypeLab bidder ext, adds provider metadata, and resolves response bid type from mtype, ext.hypelab.creative_type, VAST markup, or the original impression media type.

🔎 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 unit coverage for request creation, required param validation, mixed valid/invalid impressions, response parsing failures, empty responses, and bid type resolution.
  • Added integration coverage for the /openrtb2/auction path and HypeLab adapter configuration.
  • Ran ./mvnw -Dtest=HypeLabBidderTest test successfully.
  • Generated a local JaCoCo report for HypeLabBidder: 96% instruction coverage and 97% line coverage.

🏎 Quality check

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

}

if (validImps.isEmpty()) {
return Result.of(Collections.emptyList(), errors);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Result.withErrors

Comment on lines +76 to +85
return Result.of(Collections.singletonList(
HttpRequest.<BidRequest>builder()
.method(HttpMethod.POST)
.uri(endpointUrl)
.headers(headers())
.impIds(BidderUtil.impIds(outgoingRequest))
.body(mapper.encodeToBytes(outgoingRequest))
.payload(outgoingRequest)
.build()),
errors);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use BidderUtil.defaultRequest

}

private Imp makeOutgoingImp(Imp imp) {
final ExtImpHypeLab extImp = parseImpExt(imp);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Move parsing out of this method

Comment on lines +101 to +108
if (imp.getExt() == null) {
throw new PreBidException("imp %s: unable to unmarshal ext".formatted(imp.getId()));
}

final JsonNode bidderNode = imp.getExt().get("bidder");
if (bidderNode == null || bidderNode.isNull()) {
throw new PreBidException("imp %s: unable to unmarshal ext.bidder".formatted(imp.getId()));
}
Copy link
Copy Markdown
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 checks


final ExtImpHypeLab extImp;
try {
extImp = mapper.mapper().convertValue(bidderNode, ExtImpHypeLab.class);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Use TypeReference<ExtPrebid<?, ExtImpHypeLab>> instead

Comment on lines +154 to +183
private static List<BidderBid> extractBids(BidRequest request, BidResponse response, List<BidderError> errors) {
if (response == null || CollectionUtils.isEmpty(response.getSeatbid())) {
return Collections.emptyList();
}

final Map<String, Imp> impIdToImp = request.getImp().stream()
.collect(Collectors.toMap(Imp::getId, imp -> imp));

return response.getSeatbid().stream()
.filter(Objects::nonNull)
.map(seatBid -> bidsFromSeatBid(seatBid, impIdToImp, response.getCur(), errors))
.flatMap(Collection::stream)
.toList();
}

private static List<BidderBid> bidsFromSeatBid(SeatBid seatBid, Map<String, Imp> impIdToImp, String currency,
List<BidderError> errors) {

final List<Bid> bids = seatBid.getBid();
if (CollectionUtils.isEmpty(bids)) {
return Collections.emptyList();
}

return bids.stream()
.filter(Objects::nonNull)
.map(bid -> makeBidderBid(bid, seatBid.getSeat(), impIdToImp, currency, errors))
.filter(Objects::nonNull)
.toList();
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

        final Map<String, Imp> impIdToImp = request.getImp().stream()
                .collect(Collectors.toMap(Imp::getId, imp -> imp));

        return response.getSeatbid().stream()
                .filter(Objects::nonNull)
                .flatMap(seatBid -> CollectionUtils.emptyIfNull(seatBid.getBid()).stream()
                        .filter(Objects::nonNull)
                        .map(bid -> makeBidderBid(bid, seatBid.getSeat(), impIdToImp, response.getCur(), errors))
                        .filter(Objects::nonNull))
                .toList();
    }

    private static BidderBid makeBidderBid(Bid bid,
                                           String seat,
                                           Map<String, Imp> impIdToImp, String currency,
                                           List<BidderError> errors) {

        try {
            return BidderBid.of(bid, resolveBidType(bid, impIdToImp), seat, currency);
        } catch (PreBidException e) {
            errors.add(BidderError.badServerResponse(e.getMessage()));
            return null;
        }
    }

    private static BidType resolveBidType(Bid bid, Map<String, Imp> impIdToImp) {
        return bidTypeFromMtype(bid.getMtype())
                .or(() -> bidTypeFromExt(bid))
                .or(() -> bidTypeFromAdm(bid.getAdm()))
                .or(() -> bidTypeFromImp(bid, impIdToImp))
                .orElseThrow(() -> new PreBidException("unable to determine media type for bid %s on imp %s"
                        .formatted(bid.getId(), bid.getImpid())));
    }

    private static Optional<BidType> bidTypeFromMtype(Integer mtype) {
        return Optional.ofNullable(switch (mtype) {
            case 1 -> BidType.banner;
            case 2 -> BidType.video;
            case 4 -> BidType.xNative;
            case null, default -> null;
        });
    }

    private static Optional<BidType> bidTypeFromExt(Bid bid) {
        return Optional.ofNullable(bid.getExt())
                .map(ext -> ext.get("hypelab"))
                .map(hypelab -> hypelab.get("creative_type"))
                .filter(JsonNode::isTextual)
                .map(JsonNode::asText)
                .map(creativeType -> switch (creativeType) {
                    case "display" -> BidType.banner;
                    case "video" -> BidType.video;
                    default -> null;
                });
    }

    private static Optional<BidType> bidTypeFromAdm(String adm) {
        return StringUtils.startsWith(StringUtils.trimToEmpty(adm), "<VAST")
                ? Optional.of(BidType.video)
                : Optional.empty();
    }

    private static Optional<BidType> bidTypeFromImp(Bid bid, Map<String, Imp> impIdToImp) {
        return impIdToImp.containsKey(bid.getImpid())
                ? Optional.of(BidderUtil.getBidType(bid, impIdToImp))
                : Optional.empty();
    }

}

outgoingExt.addProperty("source", mapper.mapper().valueToTree(SOURCE));
outgoingExt.addProperty("provider_version", mapper.mapper().valueToTree(pbsVersion()));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see the difference with GO version. You didn't append prebid-server@ prefix

return switch (StringUtils.defaultString(creativeType)) {
case "display" -> BidType.banner;
case "video" -> BidType.video;
default -> null;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

GO version have native case:

	case "native":
		return openrtb_ext.BidTypeNative, true, nil

Comment on lines +210 to +212
if (impIdToImp.containsKey(bid.getImpid())) {
return BidderUtil.getBidType(bid, impIdToImp);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

GO doesn't support multi-media imps: mediaTypeCount == 1

hype: ~
meta-info:
maintainer-email: sdk@hypelab.com
app-media-types: []
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No need to declare empty app-media-types

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.

2 participants