Skip to content

Commit

Permalink
Passthrough gpp alongside gpp_sid when available
Browse files Browse the repository at this point in the history
  • Loading branch information
zapo committed Dec 18, 2024
1 parent 056e7a2 commit 5732ec7
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 78 deletions.
4 changes: 4 additions & 0 deletions lib/core/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ function buildRequest(path: string, config: ResolvedConfig, init?: RequestInit):
url.searchParams.set("gpp", config.consent.gpp);
}

if (config.consent.gpp_sid) {
url.searchParams.set("gpp_sid", config.consent.gpp_sid.join(","));
}

if (config.consent.tcf) {
url.searchParams.set("tcf", config.consent.tcf);
}
Expand Down
152 changes: 90 additions & 62 deletions lib/core/regs/consent.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { getConsent } from "./consent";
import { SectionID as TCFEuV2SectionID, APIPrefix as TCFEuV2APIPrefix } from "./gpp/tcfeuv2";
import { SectionID as TCFCaV1SectionID, APIPrefix as TCFCaV1APIPrefix } from "./gpp/tcfcav1";
import * as gpp from "./gpp";

describe("getConsent", () => {
let windowSpy;
Expand Down Expand Up @@ -61,22 +60,7 @@ describe("getConsent", () => {
});

it("updates consent based on tcf signals for gdpr", () => {
let listener;
const tcf = jest.fn((_command, _version, cb) => {
listener = cb;
});
windowSpy.mockImplementation(() => ({ __tcfapi: tcf }));

const signal = (overrides) => {
listener(
{
gdprApplies: true,
eventStatus: "tcloaded",
...overrides,
},
true
);
};
const signal = mockTCFSignal(windowSpy);

const consent = getConsent("gdpr");
// By default device access is denied
Expand Down Expand Up @@ -114,129 +98,173 @@ describe("getConsent", () => {
});

it("updates consent based on gpp signals for gdpr", () => {
let listener;
const gpp = jest.fn((_command, cb) => {
listener = cb;
});
windowSpy.mockImplementation(() => ({ __gpp: gpp }));
const signal = mockGPPSignal(windowSpy);

const consent = getConsent("gdpr");
// By default device access is denied
expect(consent.deviceAccess).toBe(false);
expect(consent.reg).toBe("gdpr");

const signal = (pingData) => {
listener(
{
data: "ready",
eventName: "signalStatus",
pingData,
},
true
);
};

// Simulate gpp ready event indicating no applicable sections
signal({ applicableSections: [-1], gppString: "ignored" });
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBeUndefined();

// Section tcfeuv2 applies but nothing in parsed sections
signal({ parsedSections: {}, applicableSections: [TCFEuV2SectionID], gppString: "noparsedsections" });
signal({ parsedSections: {}, applicableSections: [gpp.tcfeuv2.SectionID], gppString: "noparsedsections" });
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("noparsedsections");
expect(consent.gpp_sid).toEqual([gpp.tcfeuv2.SectionID]);

// Section tcfeuv2 applies but no publisher segment
signal({
parsedSections: { [TCFEuV2APIPrefix]: [] },
applicableSections: [TCFEuV2SectionID],
parsedSections: { [gpp.tcfeuv2.APIPrefix]: [] },
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "nopublishersegment",
});
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("nopublishersegment");
expect(consent.gpp_sid).toEqual([gpp.tcfeuv2.SectionID]);

// Section tcfeuv2 applies but no purpose 1 consent in publisher segment
signal({
parsedSections: {
[TCFEuV2APIPrefix]: [
[gpp.tcfeuv2.APIPrefix]: [
{
SegmentType: 3,
PubPurposesConsent: [],
},
],
},
applicableSections: [TCFEuV2SectionID],
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "nopurpose1consent",
});
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("nopurpose1consent");
expect(consent.gpp_sid).toEqual([gpp.tcfeuv2.SectionID]);

// Section tcfeuv2 applies and purpose 1 granted to publisher
signal({
parsedSections: {
[TCFEuV2APIPrefix]: [
[gpp.tcfeuv2.APIPrefix]: [
{
SegmentType: 3,
PubPurposesConsent: [1],
},
],
},
applicableSections: [TCFEuV2SectionID],
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "purpose1",
});
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBe("purpose1");
expect(consent.gpp_sid).toEqual([gpp.tcfeuv2.SectionID]);

// Section tcfeuv2 applies and purpose 1 revoked
// to publisher
signal({
parsedSections: {
[TCFEuV2APIPrefix]: [
[gpp.tcfeuv2.APIPrefix]: [
{
SegmentType: 3,
PubPurposesConsent: [],
},
],
},
applicableSections: [TCFEuV2SectionID],
applicableSections: [gpp.tcfeuv2.SectionID],
gppString: "revoked",
});
expect(consent.deviceAccess).toBe(false);
expect(consent.gpp).toBe("revoked");
expect(consent.gpp_sid).toEqual([gpp.tcfeuv2.SectionID]);
});

it("updates consent based on gpp signals for can", () => {
let listener;
const gpp = jest.fn((_command, cb) => {
listener = cb;
});
windowSpy.mockImplementation(() => ({ __gpp: gpp }));
const signal = mockGPPSignal(windowSpy);

const consent = getConsent("can");
// Device access is always granted
expect(consent.deviceAccess).toBe(true);
expect(consent.reg).toBe("can");

const signal = (pingData) => {
listener(
{
data: "ready",
eventName: "signalStatus",
pingData,
},
true
);
};
// Simulate gpp ready event indicating no applicable sections
signal({ applicableSections: [-1], gppString: "ignored" });
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBeUndefined();
expect(consent.gpp_sid).toBeUndefined();

// It listens to can section changes and propagates gpp and gpp sid
signal({ parsedSections: {}, applicableSections: [gpp.tcfcav1.SectionID], gppString: "can" });
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBe("can");
expect(consent.gpp_sid).toEqual([gpp.tcfcav1.SectionID]);
});

it("updates consent based on gpp signals for us", () => {
const signal = mockGPPSignal(windowSpy);

const consent = getConsent("us");
// Device access is always granted
expect(consent.deviceAccess).toBe(true);
expect(consent.reg).toBe("us");

// Simulate gpp ready event indicating no applicable sections
signal({ applicableSections: [-1], gppString: "ignored" });
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBeUndefined();
expect(consent.gpp_sid).toBeUndefined();

// Section tcfcav1 applies but nothing in parsed sections doesn't impact
// device access
signal({ parsedSections: {}, applicableSections: [TCFCaV1SectionID], gppString: "noparsedsections" });
// It listens to us sections changes and propagates gpp and gpp sid
signal({ parsedSections: {}, applicableSections: [gpp.usnat.SectionID], gppString: "usnat" });
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBe("noparsedsections");
expect(consent.gpp).toBe("usnat");
expect(consent.gpp_sid).toEqual([gpp.usnat.SectionID]);

signal({
parsedSections: {},
applicableSections: [gpp.usnat.SectionID, gpp.usca.SectionID],
gppString: "usnat_and_usca",
});
expect(consent.deviceAccess).toBe(true);
expect(consent.gpp).toBe("usnat_and_usca");
expect(consent.gpp_sid).toEqual([gpp.usnat.SectionID, gpp.usca.SectionID]);
});
});

function mockGPPSignal(windowSpy) {
let listener;
const gpp = jest.fn((_command, cb) => {
listener = cb;
});
windowSpy.mockImplementation(() => ({ __gpp: gpp }));

return (pingData) => {
listener(
{
data: "ready",
eventName: "signalStatus",
pingData,
},
true
);
};
}

function mockTCFSignal(windowSpy) {
let listener;
const tcf = jest.fn((_command, _version, cb) => {
listener = cb;
});
windowSpy.mockImplementation(() => ({ __tcfapi: tcf }));

return (overrides) => {
listener(
{
gdprApplies: true,
eventStatus: "tcloaded",
...overrides,
},
true
);
};
}
66 changes: 50 additions & 16 deletions lib/core/regs/consent.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,74 @@
import { inferRegulation, Regulation } from "./regulations";
import { PingReturn as GPPConsentData } from "./gpp/cmpapi";
import { TCData as TCFConsentData } from "./tcf/cmpapi";
import { SectionID as TCFEuV2SectionID, APIPrefix as TCFEuV2APIPrefix } from "./gpp/tcfeuv2";
import { SectionID as TCFCaV1SectionID } from "./gpp/tcfcav1";
import * as gpp from "./gpp";
import * as tcf from "./tcf";

type Consent = {
deviceAccess: boolean;
reg: Regulation | null;
tcf?: string;
gpp?: string;
gpp_sid?: number[];
};

const gdprSectionIDs = [gpp.tcfeuv2.SectionID];

const canSectionIDs = [gpp.tcfcav1.SectionID];

const usSectionIDs = [
gpp.usnat.SectionID,
gpp.usca.SectionID,
gpp.usco.SectionID,
gpp.usct.SectionID,
gpp.usde.SectionID,
gpp.usfl.SectionID,
gpp.usia.SectionID,
gpp.usmt.SectionID,
gpp.usne.SectionID,
gpp.usnh.SectionID,
gpp.usnj.SectionID,
gpp.usor.SectionID,
gpp.ustn.SectionID,
gpp.ustx.SectionID,
gpp.usut.SectionID,
gpp.usva.SectionID,
];

function gdprConsent(): Consent {
const consent: Consent = { deviceAccess: false, reg: "gdpr" };

// For use TCF if available, otherwise use GPP,
// Use TCF if available, otherwise use GPP,
// if none available assume device access is not allowed
if (hasTCF()) {
onTCFChange((data) => {
consent.deviceAccess = tcfDeviceAccess(data);
consent.tcf = data.tcString;
});
} else if (hasGPP()) {
onGPPSectionChange(TCFEuV2SectionID, (data) => {
onGPPSectionChange(gdprSectionIDs, (data) => {
consent.deviceAccess = gppEUDeviceAccess(data);
consent.gpp = data.gppString;
consent.gpp_sid = data.applicableSections;
});
}

return consent;
}

function usConsent(): Consent {
return { deviceAccess: true, reg: "us" };
const consent: Consent = { deviceAccess: true, reg: "us" };
onGPPSectionChange(usSectionIDs, (data) => {
consent.gpp = data.gppString;
consent.gpp_sid = data.applicableSections;
});

return consent;
}

function canConsent(): Consent {
const consent: Consent = { deviceAccess: true, reg: "can" };
onGPPSectionChange(TCFCaV1SectionID, (data) => {
// Device access is always granted
onGPPSectionChange(canSectionIDs, (data) => {
consent.gpp = data.gppString;
consent.gpp_sid = data.applicableSections;
});

return consent;
Expand All @@ -58,12 +87,12 @@ function getConsent(reg: Regulation | null): Consent {
}
}

function gppEUDeviceAccess(data: GPPConsentData): boolean {
if (!(TCFEuV2APIPrefix in data.parsedSections)) {
function gppEUDeviceAccess(data: gpp.cmpapi.PingReturn): boolean {
if (!(gpp.tcfeuv2.APIPrefix in data.parsedSections)) {
return false;
}

const section = data.parsedSections[TCFEuV2APIPrefix] || [];
const section = data.parsedSections[gpp.tcfeuv2.APIPrefix] || [];
const publisherSubsection = section.find((s) => {
return "SegmentType" in s && s.SegmentType === 3;
});
Expand All @@ -75,14 +104,14 @@ function gppEUDeviceAccess(data: GPPConsentData): boolean {
return publisherSubsection.PubPurposesConsent.includes(1);
}

function tcfDeviceAccess(data: TCFConsentData): boolean {
function tcfDeviceAccess(data: tcf.cmpapi.TCData): boolean {
if (!data.gdprApplies) {
return true;
}
return !!data.publisher.consents["1"];
}

function onGPPSectionChange(sectionID: number, cb: (_: GPPConsentData) => void): void {
function onGPPSectionChange(sectionIDs: number[], cb: (_: gpp.cmpapi.PingReturn) => void): void {
if (!hasGPP()) {
return;
}
Expand All @@ -96,14 +125,19 @@ function onGPPSectionChange(sectionID: number, cb: (_: GPPConsentData) => void):
if (!ready) {
return;
}
if (!data.pingData.applicableSections.includes(sectionID)) {

const anyChanged = sectionIDs.some((sectionID) => {
return data.pingData.applicableSections.includes(sectionID);
});

if (!anyChanged) {
return;
}
cb(data.pingData);
});
}

function onTCFChange(cb: (_: TCFConsentData) => void): void {
function onTCFChange(cb: (_: tcf.cmpapi.TCData) => void): void {
if (!hasTCF()) {
return;
}
Expand Down

0 comments on commit 5732ec7

Please sign in to comment.