Skip to content

Commit

Permalink
Merge branch 'main' into S2S-1925-symitri-rtd-module
Browse files Browse the repository at this point in the history
  • Loading branch information
paulmdorr committed Oct 21, 2024
2 parents 1bf2dff + a8fe0ae commit 718058c
Show file tree
Hide file tree
Showing 8 changed files with 252 additions and 86 deletions.
9 changes: 6 additions & 3 deletions modules.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"dfpAdServerVideo",
"enrichmentFpdModule",
"fabrickIdSystem",
"fledgeForGpt",
"gptPreAuction",
"gridBidAdapter",
"gumgumBidAdapter",
Expand All @@ -30,10 +29,10 @@
"ixBidAdapter",
"justpremiumBidAdapter",
"kargoBidAdapter",
"linkedInAdsIdSystem",
"liveIntentIdSystem",
"medianetBidAdapter",
"openxBidAdapter",
"paapi",
"pairIdSystem",
"prebidServerBidAdapter",
"priceFloors",
Expand All @@ -48,6 +47,7 @@
"sharethroughBidAdapter",
"sizeMappingV2",
"symitriDapRtdProvider",
"sonobiBidAdapter",
"teadsBidAdapter",
"topicsFpdModule",
"tripleliftBidAdapter",
Expand All @@ -57,7 +57,10 @@
"unifiedIdSystem",
"unrulyBidAdapter",
"userId",
"vidazooBidAdapter",
"videojsVideoProvider",
"yahoosspBidAdapter",
"yieldmoBidAdapter"
"yieldmoBidAdapter",
"pulsepointBidAdapter",
"pulsepointAnalyticsAdapter"
]
2 changes: 2 additions & 0 deletions modules/identityLinkIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ function getEnvelope(url, callback, configParams) {
utils.logInfo('identityLink: A 3P retrieval is attempted!');
setEnvelopeSource(false);
ajax(url, callbacks, undefined, { method: 'GET', withCredentials: true });
} else {
callback()
}
}

Expand Down
124 changes: 124 additions & 0 deletions modules/linkedInAdsIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* This module adds LinkedIn Ads ID to the User ID module
* The {@link module:modules/userId} module is required
* @module modules/linkedInAdsIdSystem
* @requires module:modules/userId
*/

import { logInfo, generateUUID } from '../src/utils.js';
import { submodule } from '../src/hook.js';
import { getStorageManager } from '../src/storageManager.js';
import { MODULE_TYPE_UID } from '../src/activities/modules.js';
import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js';

const MODULE_NAME = 'linkedInAdsId';
const STORAGE_KEY = 'li_adsId';
const LOG_PREFIX = 'LinkedIn Ads ID: ';
const LI_FAT_COOKIE = 'li_fat';
const LI_GIANT_COOKIE = 'li_giant';

export const BrowserStorage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME });

/** @type {Submodule} */
export const linkedInAdsIdSubmodule = {

/**
* Used to link submodule with config
* @type {string}
*/
name: MODULE_NAME,

/**
* @returns {Object} - Object with li_fat and li_giant as string properties
*/
getCookieIds() {
return {
li_fat: BrowserStorage.getCookie(LI_FAT_COOKIE),
li_giant: BrowserStorage.getCookie(LI_GIANT_COOKIE),
};
},

/**
* Decode stored value for bid requests
* @param {string} id
* @returns {Object}
*/
decode(id) {
const cookies = this.getCookieIds();
logInfo(`${LOG_PREFIX} found Legacy cookies: ${JSON.stringify(cookies)}`);

const linkedInAdsId = {
li_adsId: id,
ext: {
...cookies,
},
};

return { linkedInAdsId };
},

/**
* Performs actions to obtain `linkedInAdsId` from storage
* @returns { { id: string } | undefined }
*/
getId(config) {
const localKey = config?.storage?.name || STORAGE_KEY;
let id = BrowserStorage.localStorageIsEnabled() && BrowserStorage.getDataFromLocalStorage(localKey);

if (this.hasConsent() && !id) {
logInfo(`${LOG_PREFIX} existing id not found, generating a new li_adsId`);
id = generateUUID();
}

return { id };
},

/**
* Check all consent types, GDPR, CCPA, including processing TCF v2 consent string
* @returns {boolean}
*/
hasConsent: function() {
/**
* GDPR check
* Negative of (consentData.gdprApplies === true || consentData.gdprApplies === 1)
*/
const gdprConsentData = gdprDataHandler.getConsentData();
const hasGDPRConsent = !(gdprConsentData?.gdprApplies);

/**
* COPPA - Parental consent for the collection and use of personal data for users under the age of 13,
* as required by the COPPA regulation in the United States.
*/
const isCoppaApplicable = coppaDataHandler.getCoppa();

/**
* CCPA Consent String processing
* https://github.com/InteractiveAdvertisingBureau/USPrivacy/blob/master/CCPA/US%20Privacy%20String.md
*/
const usPrivacyString = uspDataHandler.getConsentData();
const hasCCPAConsent = !(
usPrivacyString.length === 4 &&
usPrivacyString[1] === 'Y' && // Publisher has provided notice
usPrivacyString[2] === 'Y' // user has made a choice to opt out of sale
);

return hasGDPRConsent && hasCCPAConsent && !isCoppaApplicable;
},

eids: {
'linkedInAdsId': {
source: 'linkedin.com',
getValue: function(data) {
return data.li_adsId;
},
getUidExt: function(data) {
if (data.ext) {
return data.ext;
}
},
atype: 1,
},
}
};

submodule('userId', linkedInAdsIdSubmodule);
14 changes: 11 additions & 3 deletions modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1041,11 +1041,19 @@ export function attachIdSystem(submodule) {
}
}

function normalizePromise(fn) {
function normalizePromise(fn, handleErrors = false, fnName) {
// for public methods that return promises, make sure we return a "normal" one - to avoid
// exposing confusing stack traces
return function() {
return Promise.resolve(fn.apply(this, arguments));
const promise = Promise.resolve(fn.apply(this, arguments));

if (handleErrors) {
return promise.catch(error => {
logWarn(`Error occurred in ${fnName}: ${error.message || error}`);
});
}

return promise;
}
}

Expand Down Expand Up @@ -1088,7 +1096,7 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) {
(getGlobal()).getUserIdsAsEids = getUserIdsAsEids;
(getGlobal()).getEncryptedEidsForSource = normalizePromise(getEncryptedEidsForSource);
(getGlobal()).registerSignalSources = registerSignalSources;
(getGlobal()).refreshUserIds = normalizePromise(refreshUserIds);
(getGlobal()).refreshUserIds = normalizePromise(refreshUserIds, true, 'refreshUserIds');
(getGlobal()).getUserIdsAsync = normalizePromise(getUserIdsAsync);
(getGlobal()).getUserIdsAsEidBySource = getUserIdsAsEidBySource;
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "prebid.js",
"version": "8.49.7-S2S-1925",
"version": "8.49.84",
"description": "Header Bidding Management Library",
"main": "src/prebid.js",
"scripts": {
Expand Down
77 changes: 0 additions & 77 deletions prebid-analytics-7.48.0.js

This file was deleted.

106 changes: 106 additions & 0 deletions test/spec/modules/linkedInAdsIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
linkedInAdsIdSubmodule,
BrowserStorage
} from 'modules/linkedInAdsIdSystem.js';
import * as Utils from '../../../src/utils.js';
import { uspDataHandler, coppaDataHandler, gdprDataHandler } from 'src/adapterManager.js';

describe('LinkedIn Ads ID module', () => {
const LI_FAT_COOKIE = 'li_fat';
const LI_GIANT_COOKIE = 'li_giant';
const dummyLiFat = '12345abc';
const dummyLiGiant = '67890xyz';

describe('getCookieIds', () => {
it('should return li_fat and li_giant cookies', () => {
const storageStub = sinon.stub(BrowserStorage, 'getCookie');

storageStub.withArgs(LI_FAT_COOKIE).returns(dummyLiFat);
storageStub.withArgs(LI_GIANT_COOKIE).returns(dummyLiGiant);

const cookies = linkedInAdsIdSubmodule.getCookieIds();

expect(cookies.li_fat).to.equal(dummyLiFat);
expect(cookies.li_giant).to.equal(dummyLiGiant);
storageStub.restore();
});
});

describe('decode', () => {
it('should return linkedInAdsId and legacy cookies if available', () => {
const storageStub = sinon.stub(BrowserStorage, 'getCookie');
storageStub.withArgs(LI_FAT_COOKIE).returns(dummyLiFat);
storageStub.withArgs(LI_GIANT_COOKIE).returns(dummyLiGiant);

const id = '12345abcde';
const decoded = linkedInAdsIdSubmodule.decode(id);

expect(decoded.linkedInAdsId).to.deep.equal({
li_adsId: id,
ext: {
li_fat: dummyLiFat,
li_giant: dummyLiGiant
}
});
storageStub.restore();
});
});

describe('getId', () => {
it('should generate and save a new ID', () => {
const genUuidStub = sinon.stub().returns('dummyId123');
Utils.generateUUID = genUuidStub;

const hasConsentStub = sinon.stub(linkedInAdsIdSubmodule, 'hasConsent');
hasConsentStub.returns(true);

const id = linkedInAdsIdSubmodule.getId();
expect(id).to.deep.equal({
id: 'dummyId123'
});

expect(genUuidStub.calledOnce).to.be.true;
hasConsentStub.restore();
});
});

describe('Consent checks `hasConsent`', () => {
let gdprStub, ccpaStub, coppaStub;

beforeEach(() => {
gdprStub = sinon.stub(gdprDataHandler, 'getConsentData');
ccpaStub = sinon.stub(uspDataHandler, 'getConsentData');
coppaStub = sinon.stub(coppaDataHandler, 'getCoppa');
});

afterEach(() => {
gdprStub.restore();
ccpaStub.restore();
coppaStub.restore();
});

it('should return false if GDPR consent missing', () => {
ccpaStub.returns('1YNN');
gdprStub.returns({gdprApplies: true});
expect(linkedInAdsIdSubmodule.hasConsent()).to.be.false;
});

it('should return false if CCPA opt-out', () => {
ccpaStub.returns('1YYN');
expect(linkedInAdsIdSubmodule.hasConsent()).to.be.false;
});

it('should return false if COPPA applicable', () => {
ccpaStub.returns('1YNN');
coppaStub.returns(true);
expect(linkedInAdsIdSubmodule.hasConsent()).to.be.false;
});

it('should return true if all consents present', () => {
gdprStub.returns({gdprApplies: false});
ccpaStub.returns('1YNN');
coppaStub.returns(false);
expect(linkedInAdsIdSubmodule.hasConsent()).to.be.true;
});
});
});

0 comments on commit 718058c

Please sign in to comment.