Skip to content
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

Yieldlab: Add Digital Service Act (DSA) support #3011

Merged

Conversation

william-harris
Copy link
Contributor

@william-harris william-harris commented Feb 23, 2024

This ports this Go PR

A review and approval from a member of the Yieldlab team are required. (@brushmate, @rey1128, @nkloeber)

@william-harris william-harris force-pushed the Yieldlab-implementing-dsa-for-yieldlab branch from f381f45 to edf9d9f Compare February 26, 2024 15:54
Copy link

@rey1128 rey1128 left a comment

Choose a reason for hiding this comment

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

looks good to me

expectedDsa.put("adrender", 2);
expectedDsa.set("transparency", transparencies);

final var actualDsa = result.getValue().get(0).getBid().getExt().get("dsa");
Copy link
Collaborator

Choose a reason for hiding this comment

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

Code style of repository requires strict types instead of vars

);
}

@ParameterizedTest(name = "{index} - {0}")
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are not using @ParameterizedTest inside repository
Separate test cases == separate tests

import java.util.List;

@AllArgsConstructor(staticName = "of")
@Value
Copy link
Collaborator

Choose a reason for hiding this comment

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

@value(staticConstructor = "of")

Comment on lines 12 to 15
String behalf;
String paid;
Integer adrender;
List<Transparency> transparency;
Copy link
Collaborator

Choose a reason for hiding this comment

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

We prefer to have empty line after each field

if (dsa == null) {
return null;
}
final var ext = mapper.mapper().createObjectNode();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please replace each var with strict type

}

private static String encodeTransparency(ExtRegsDsaTransparency transparency) {
return "%s~%s".formatted(transparency.getDomain(), encodeTransparencyParams(transparency.getDsaParams()));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nitpick. %s~%s can be class level constant,
_ on line 296 also

}

private static boolean hasDsa(BidRequest request) {
return request.getRegs() != null
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here you can use Optional approach

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I understand what you mean by "Optional approach".

dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString());
}

if (dsa.getTransparency() != null && !dsa.getTransparency().isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

dsa.getTransparency() Can be extracted to separate variable

.map(Regs::getExt)
.map(ExtRegs::getDsa)
.map(YieldlabBidder::extractDsaRequestParamsFromDsaRegsExtension)
.orElse(Map.of());
Copy link
Contributor

Choose a reason for hiding this comment

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

Better use the lazy version: .orElseGet(Map::of)

.collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER));
}

private static Predicate<ExtRegsDsaTransparency> transparencyIsValid() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should rather be the predicate itself, e.g. accept the transparency object as a parameter and return a boolean. It should be referenced as YieldlabBidder::transparencyIsValid. I would also call the method isTransparencyValid.


private static Predicate<ExtRegsDsaTransparency> transparencyIsValid() {
return transparency ->
!Objects.isNull(transparency.getDomain())
Copy link
Contributor

Choose a reason for hiding this comment

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

transparency.getDomain() != null is more expressive IMHO.

dsaRequestParams.put("dsatransparency", encodeTransparenciesAsString(dsaTransparency));
}

return dsaRequestParams.entrySet().stream()
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this necessary? All values that might go into the map are checked for nullness already.

List<Transparency> transparency;

@AllArgsConstructor(staticName = "of")
@Value
Copy link
Contributor

Choose a reason for hiding this comment

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

Is staticConstructor missing here as well?

@william-harris william-harris force-pushed the Yieldlab-implementing-dsa-for-yieldlab branch from 07acc54 to 35f54c5 Compare March 5, 2024 08:36
try {
dsaNode = mapper.mapper().convertValue(dsa, JsonNode.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("Failed to serialize DSA object", e);
Copy link
Collaborator

@AntoxaAntoxic AntoxaAntoxic Mar 6, 2024

Choose a reason for hiding this comment

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

looks like the PBS Go version has a more detailed error message fmt.Errorf("failed to make JSON for seatbid.bid.ext for adslotID %v. This is most likely a programming issue", bid.ID)

Perhaps it's worth keeping it here as well

Also, according to the PBS Go version the error shouldn't disrupt other bids processing, so the error has to be reported and the ext has to be null in this case. Please double-check


private static boolean isTransparencyValid(ExtRegsDsaTransparency transparency) {
return transparency.getDomain() != null
&& StringUtils.isNotBlank(transparency.getDomain())
Copy link
Collaborator

Choose a reason for hiding this comment

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

you can use just isNotBlank, it covers not null case as well

@@ -486,7 +492,8 @@ private ObjectNode resolveExtParameter(YieldlabDigitalServicesActResponse dsa) {
try {
dsaNode = mapper.mapper().convertValue(dsa, JsonNode.class);
} catch (IllegalArgumentException e) {
throw new PreBidException("Failed to serialize DSA object", e);
logger.error("Failed to serialize DSA object for adslot {}", yieldlabResponse.getId(), e);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I believe this error should be returned in the response, not just logged

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would require a big refactoring of the existing implementation as there is no support for returning valid bids along with failed bids. Yieldlab did not write the existing implementation, and we feel it is beyond the scope of this ticket to be refactoring the existing implementation. Our goal is simply to add DSA support since this is a legal requirement. We are fine with not reporting failures.

Copy link
Collaborator

@CTMBNara CTMBNara left a comment

Choose a reason for hiding this comment

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

Some of the comments not related to the scope, so feel free to ignore them, but please at least provide answers on comments related to difference in logic between PBS-Java and PBS-Go.

.map(Regs::getExt)
.map(ExtRegs::getDsa)
.map(YieldlabBidder::extractDsaRequestParamsFromDsaRegsExtension)
.orElseGet(Map::of);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are preferring to use Collections.empty* instead of *.of().
Also, as Collections.emptyMap() (and Map.of()) returns already created object, it's better to use .orElse instead of orElseGet.

So, please, change it with:
.orElse(Collections.emptyMap());

Copy link
Collaborator

Choose a reason for hiding this comment

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

Comparing the logic with the GO version, I don't see any sorting operations for adSlots in GO. Does PBS-Java need this?
adSlotIds.stream().sorted().collect(...)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It isn't needed, no.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also, i don't see anything similar in GO for ts parameter:
isDebugEnabled(request) ? "200000" : ...

Does PBS-Java need to pass this debug value? Just out of interest I removed the debug part and all tests passed:
final String timestamp = String.valueOf(clock.instant().getEpochSecond());

Same question about
final int weekNumber = isDebugEnabled(bidRequest) ? 35 : ...

and

        final String timestamp = isDebugEnabled(bidRequest)
                ? "200000"
                : ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As far as we are concerned, it is not needed no.

Copy link
Collaborator

Choose a reason for hiding this comment

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

String.join("ylid:", user.getBuyeruid()) doesn't corresponds to GO logic. It just doesn't do anything.
It's need to be "ylid" + user.getBuyeruid().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a courtesy I can fix this, but this wasn't written by us and is also out of scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implementing this breaks the tests constructExtImpShouldWorkWithDuplicateKeysTargeting and makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders, so I will have to decline.

Copy link
Collaborator

Choose a reason for hiding this comment

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

                uriBuilder.addParameter("lat", resolveNumberParameter(geo.getLat()));
                uriBuilder.addParameter("lon", resolveNumberParameter(geo.getLon()));

This code doesn't handle nulls as GO does.
Java: null -> null
GO: nil -> "0"

Copy link
Contributor Author

@william-harris william-harris Mar 25, 2024

Choose a reason for hiding this comment

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

This is out of scope, and I don't intend to change it.

}

final List<ExtRegsDsaTransparency> dsaTransparency = dsa.getTransparency();
if (dsaTransparency != null && !dsaTransparency.isEmpty()) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Predicate can be simplified: CollectionUtils.isNotEmpty(dsaTransparency)


final List<ExtRegsDsaTransparency> dsaTransparency = dsa.getTransparency();
if (dsaTransparency != null && !dsaTransparency.isEmpty()) {
dsaRequestParams.put("dsatransparency", encodeTransparenciesAsString(dsaTransparency));
Copy link
Collaborator

Choose a reason for hiding this comment

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

encodeTransparencyAsString can return an empty string if all entries are invalid. PBS-GO doesn't add the dsatransparency parameter in this case.

Comment on lines +282 to +303
private static String encodeTransparenciesAsString(List<ExtRegsDsaTransparency> transparencies) {
return transparencies.stream()
.filter(YieldlabBidder::isTransparencyValid)
.map(YieldlabBidder::encodeTransparency)
.collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER));
}

private static boolean isTransparencyValid(ExtRegsDsaTransparency transparency) {
return StringUtils.isNotBlank(transparency.getDomain())
&& transparency.getDsaParams() != null
&& CollectionUtils.isNotEmpty(transparency.getDsaParams());
}

private static String encodeTransparency(ExtRegsDsaTransparency transparency) {
return TRANSPARENCY_TEMPLATE.formatted(transparency.getDomain(),
encodeTransparencyParams(transparency.getDsaParams()));
}

private static String encodeTransparencyParams(List<Integer> dsaParams) {
return dsaParams.stream().map(Objects::toString).collect(Collectors.joining(
TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER));
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

There are some differences with GO logic:

  • transparency0 - invalid, transparency1 - valid
    Java: "transparency1"
    GO: "~~transparency1"
  • transparency0 - valid, transparency1 - invalid, transparency2 - valid
    Java: "transparency0~~transparency2"
    GO: "transparency0~~~~transparency2"

Similar problems with ~ separator:

  • If transparency1.dsaParams is empty
    Java: "transparency0~~transparency2"
    GO: "transparency0~~domain1~~~transparency2"
  • ...

And so on :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To me the Java implementation looks correct, and the GO version incorrect.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Out of scope, but....

Can i ask you to extract collectImpExt(imps) from ExtImpYieldlab getMatchedExtImp(...) method, so it will be out of for loop on bids? Just to make it from O(nk) to O(n).... 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm sorry, but I must decline. Like you said it is out of scope.

final ObjectNode ext = mapper.mapper().createObjectNode();
final JsonNode dsaNode;
try {
dsaNode = mapper.mapper().convertValue(dsa, JsonNode.class);
Copy link
Collaborator

Choose a reason for hiding this comment

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

mapper.mapper().valueToTree(dsa)

@SerhiiNahornyi SerhiiNahornyi merged commit a475313 into prebid:master Mar 26, 2024
5 checks passed
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.

7 participants