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

FPD Enrichment: support for Cookie Deprecation Label #10683

Merged
merged 12 commits into from
Nov 27, 2023

Conversation

jlquaccia
Copy link
Collaborator

Type of change

  • Feature

Description of change

  • Support for the Privacy Sandbox cookie deprecation label
  • Core will access the Privacy Sandbox JS API for testing mode and place the result in device.ext.dcep
  • A check for publisher purpose 1 consent before calling the Privacy Sandbox JS API will occur if the gdprEnforcement module is active

Other information

#10516

@jlquaccia
Copy link
Collaborator Author

@patmmccann @dgirardi, from #10516, it sounded like we wanted this change in enrichment.js. Let me know what you guys think.

@@ -78,6 +95,34 @@ function removeUndef(obj) {
return getDefinedParams(obj, Object.keys(obj))
}

export async function tryToGetCdepLabel(cb = getCookieDeprecationLabel) {
let cdep;
const consentData = gdprDataHandler.getConsentData();
Copy link
Collaborator

Choose a reason for hiding this comment

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

This should do a check on the accessDevice activity instead (somewhat like this).

Normally that would be enough - the TCF logic (which is just one of the ways storage is gated) is attached to accessDevice in its own module.

However for this case we decided that the TCF logic should be changed to check for publisher consent (publisher.consents[1] in the parsed TCF data), apparently that's what we should have been doing from the beginning, but we need to wait for 9 to avoid breaking people. Paging also @patmmccann since I can't find a summary of what was decided, I think we may also want to add transmitUfpd (purpose 4) checks.

For this PR that means:

  • the TCF logic linked above should be updated to look for publisher.*. I would put in a special case for a new component name, e.g. if in here you check isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')), then here you can have a special case for when params[ACTIVITY_PARAM_COMPONENT_NAME] === 'cdep'. That would then get conflated into the existing special case for MODULE_TYPE_PREBID in prebid 9.
  • If we need purpose 4 checks it should be enough to add device.ext.cdep to this list

Copy link
Collaborator

Choose a reason for hiding this comment

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

Update:

  • The new TCF logic should (in version 9) replace this so that if gvlid is VENDORLESS_GVLID, instead of looking at vendorData.purpose and switching off the check on vendorData.vendor, we check vendorData.publisher instead. So my suggestion is to introduce a second, temporary gvlid placeholder (maybe FIRST_PARTY_GVLID); use it to implement the new publisher logic, and update this to return it only for the cookie deprecation label for now - then in 9 we can get rid of the existing VENDORLESS_GVLID logic.
  • We do want transmitUfpd / purpose 4 checks, so device.ext.cdep should be added to the ORTB fields that are redacted out when that is denied

@jlquaccia
Copy link
Collaborator Author

jlquaccia commented Nov 2, 2023

Thanks @dgirardi, will look into the feedback items you mentioned above today 👍

@jlquaccia
Copy link
Collaborator Author

jlquaccia commented Nov 3, 2023

Hey @dgirardi, could you take another look? Left a comment below about one question I had about the last commit i made. Also, I can address tests once the general flow of logic looks good for the rest of this PR.

if (shouldEnforce(consentData, purposeNo, modName)) {
const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params));
let allow = !!checkConsent(consentData, modName, gvlid);
let allow =
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@dgirardi had one question about the logic here. know we want to go with publisher.consent in this scenario.. but was wondering if i put the FIRST_PARTY_GVLID ternary in the right place?

Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO it should be inside validateRules, so that you can keep it consistent with some of the other logic - for example if the publisher has disabled purpose enforcement it should allow regardless of consent data.

let allow = !!checkConsent(consentData, modName, gvlid);
let allow =
gvlid === FIRST_PARTY_GVLID
? !!deepAccess(consentData, `vendorData.publisher.consents.1`)
Copy link
Collaborator

Choose a reason for hiding this comment

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

vendorData.publisher.consents[${purposeNo}]; currently this path will always have purpose 1 but I don't want us to forget about it in the future

@@ -17,7 +17,8 @@ export const ORTB_UFPD_PATHS = [
'kwarray',
'id',
'buyeruid',
'customdata'
'customdata',
'device.ext.cdep'
Copy link
Collaborator

Choose a reason for hiding this comment

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

this becomes user.device.ext.cdep because of the .map below, which is not correct. this PR adds the first field that's not under user so it needs to be outside the map.

export async function tryToGetCdepLabel(cb = getCookieDeprecationLabel) {
let cdep;
if (isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep'))) {
return GreedyPromise.resolve(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would do return GreedyPromise.resolve(isActivityAllowed(...) && cb()). Your version wraps the promise two more times (with await and the .then() below), which seems unnecessary, plus if getCookieDeprecationLabel rejects this promise never resolves.

if (shouldEnforce(consentData, purposeNo, modName)) {
const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params));
let allow = !!checkConsent(consentData, modName, gvlid);
let allow =
Copy link
Collaborator

Choose a reason for hiding this comment

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

IMO it should be inside validateRules, so that you can keep it consistent with some of the other logic - for example if the publisher has disabled purpose enforcement it should allow regardless of consent data.

let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor);

if (gvlId === FIRST_PARTY_GVLID) {
validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`));
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

One question.. when FIRST_PARTY_GVLID is present I’m guessing we don’t need to worry about vendor for this validation? (looks to be the case with VENDORLESS_GVLID as well, so I was following down that road)

Copy link
Collaborator

Choose a reason for hiding this comment

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

yep, in this case purposeConsent is required, vendorConsent is not, and the flag to check is publisher.consents

// }

export async function tryToGetCdepLabel(cb = getCookieDeprecationLabel) {
return GreedyPromise.resolve(isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && await cb());
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think the getCookieDeprecationLabel func would require that we wait for it to resolve right? otherwise the label (if there is one) most likely wouldn’t get retrieved in time before the bid reqs go out.. (i'm not able to fetch it in time from my end at least). Was wondering what your thoughts on this use of await here are? Or if you have any other suggestions?

One thing I was thinking.. maybe right here a setTimeout would be good? One that would only wait x amount of time for navigator.cookieDeprecationLabel.getValue() to resolve and if it doesn’t we can resolve manually (and not block the auction)? I added in some arbitrary setTimeout code as an example, but of course that can be changed based on what you think.

Copy link
Collaborator

Choose a reason for hiding this comment

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

getCookieDeprecationLabel returns a promise; since this also returns a promise, await is not necessary. Unless I'm mistaken await P; [...] is just syntactic sugar for P.then(() => [...]) so in this case it does nothing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

You're correct! I think when I originally tried out your suggestion there was a bug on my end 😂 I've since tried it again and things are working as expected on my end 👍

@jlquaccia
Copy link
Collaborator Author

hey @dgirardi, could you please take another look?

@jlquaccia
Copy link
Collaborator Author

added a quick "on hold" label to this PR temporarily, just so pubmatic can test for #10695

let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor);

if (gvlId === FIRST_PARTY_GVLID) {
validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`));
Copy link
Collaborator

Choose a reason for hiding this comment

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

yep, in this case purposeConsent is required, vendorConsent is not, and the flag to check is publisher.consents

// }

export async function tryToGetCdepLabel(cb = getCookieDeprecationLabel) {
return GreedyPromise.resolve(isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && await cb());
Copy link
Collaborator

Choose a reason for hiding this comment

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

getCookieDeprecationLabel returns a promise; since this also returns a promise, await is not necessary. Unless I'm mistaken await P; [...] is just syntactic sugar for P.then(() => [...]) so in this case it does nothing.

});
setTimeout(() => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

we have exising examples of FPD that waits for promises without timeouts (like user agent hints). I think it's OK because none of them actually need to wait for anything - there's no network calls or user prompts as far as I can tell, they should always resolve (or reject) very quickly. If we do introduce a timeout it should be configurable, and this should reject - not resolve to false.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, thanks for the input! Sounds like we are ok without the setTimeout in this case at this time then. I removed it for now.

@jlquaccia
Copy link
Collaborator Author

@dgirardi let me know if you think anything else is needed on this PR? Only labeled this PR as "on hold" temporarily to give our team a small window to QA #10695. We are aiming to have QA done early next week.

@patmmccann
Copy link
Collaborator

#10695 is already merged and released

@patmmccann
Copy link
Collaborator

is the failed test bc of

expect(diagObj.url).to.equal('http://localhost:9876/context.html')

@jlquaccia jlquaccia removed the on hold label Nov 10, 2023
@jlquaccia
Copy link
Collaborator Author

@patmmccann removed the label I had added onto this ticket, good to go there!

Also, interesting with the failed test.. I couldn't reproduce it locally. I also remember seeing that failed test in the circleci build for this PR too the other day but now it looks as though everything is green and passing?

@jlquaccia
Copy link
Collaborator Author

@dgirardi, attached a screenshot below of the failed test error that came up the other day when the build failed. Seems like an intermittent thing.. All checks look to have passed now. Is this something we should look into more or are we ok?

Screenshot 2023-11-10 at 11 36 51 AM

@jlquaccia
Copy link
Collaborator Author

Looks as though the same error crept up here as well fyi: #10635

@@ -310,6 +312,55 @@ describe('FPD enrichment', () => {
});
});

describe('privacy sandbox cookieDeprecationLabel', () => {
it('attempts to set ext.cdep on device obj when the gdprEnforcement module is not active', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The test does not match its description - and also mixes the behavior of enrichment and gdprEnforcement. IMO a better set of tests is:

  • if isActivityAllowed is mocked to true, and the navigator API returns a promise to some label X, the enrichment puts X in device.ext.cdep (your tests only check that the navigator is invoked)
  • if isActivityAllowed is mocked to false, the navigator API is not called and no enrichment happens
  • if the navigator API returns a promise that rejects, the enrichment does not halt forever (I expect your current version to fail this one)
  • if validateRules is passed FIRST_PARTY_GVLID, it will use publisher.consents - this should be part of the gdprEnforcement suite

return GreedyPromise.resolve(isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && cb());
}

function getCookieDeprecationLabel() {
Copy link
Collaborator

@dgirardi dgirardi Nov 13, 2023

Choose a reason for hiding this comment

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

I'll be more specific - this function, as far as I can tell, behaves the same as directly calling navigator.cookieDeprecationLabel.getValue() except that you lose rejections / errors, the promise you return never completes in that case.

@jlquaccia
Copy link
Collaborator Author

@dgirardi thanks! Last few comments you left make sense, addressed them and pushed another commit. Could you review again when you have time? (also, can see your changes from #10723 reflecting now too).

if (navigator.cookieDeprecationLabel) delete navigator.cookieDeprecationLabel;
});

it('enrichment sets device.ext.cdep when navigator.getCookieDeprecationLabel exists and isActivityAllowed is mocked to true', () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

these are not actually mocking isActivityAllowed, they are still relying on gdprEnforcement to actually set up the activity rule. If you try to run only this test suite (gulp serve-and-test --file test/spec/fpd/enrichment_spec.js) it fails, since the gdpr module is not loaded.


it('if the navigator API returns a promise that rejects, the enrichment does not halt forever', () => {
navigator.cookieDeprecationLabel = {};
sandbox.stub(dep, 'getCookieDeprecationLabel').returns(Promise.reject(new Error('oops, something went wrong')));
Copy link
Collaborator

Choose a reason for hiding this comment

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

More mismatch between intent and code - because you are not mocking the navigator API but instead the broken piece of logic, this test is not picking up the problem. It would pass no matter what you put in getCookieDeprecationLabel.

Copy link
Collaborator

@dgirardi dgirardi left a comment

Choose a reason for hiding this comment

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

I pushed 83e3d3b to show what I meant with my last comments - consider it a suggestion, it it looks good to you then of course it also LGTM

With the caveat that my browser does not have 'navigator.cookieDeprecationLabel', so I did not test the actual integration - is there a flag to turn that on, or do we need to wait until it's too late?

@dgirardi
Copy link
Collaborator

Update: chrome 120 does have the API, but only on (I think) https sites. For me it returns an empty string which with this results in device.ext.cdep not being set. I think that makes sense.

@pm-nitin-nimbalkar
Copy link
Contributor

pm-nitin-nimbalkar commented Nov 16, 2023

Update: chrome 120 does have the API, but only on (I think) https sites. For me it returns an empty string which with this results in device.ext.cdep not being set. I think that makes sense.

@dgirardi Try using chrome flags chrome://flags/#tpc-phase-out-facilitated-testing. This might help.

@jlquaccia
Copy link
Collaborator Author

@dgirardi saw your last commit, see what you mean about isActivityAllowed. also the other changes made sense and lgtm too!

@jlquaccia
Copy link
Collaborator Author

@patmmccann @dgirardi are we ok to merge this one?

@patmmccann
Copy link
Collaborator

LGTM

@patmmccann patmmccann merged commit ecf4272 into prebid:master Nov 27, 2023
4 checks passed
@jlquaccia
Copy link
Collaborator Author

Awesome, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants