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

Add GPP/TCF cmpapi integration to respect device access in EU/CA/US #152

Merged
merged 11 commits into from
Jan 7, 2025
4 changes: 2 additions & 2 deletions demos/react/src/identify.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React, { useContext, createContext, useState } from "react";
import ReactDOM from "react-dom";
import OptableSDK, { OptableConfig } from "@optable/web-sdk";
import OptableSDK, { InitConfig } from "@optable/web-sdk";

const OptableContext = createContext<OptableSDK | null>(null);

// Sandbox configuration injected by webpack based on build environment (see demos/react/webpack.config.js)
declare global {
const DCN_CONFIG: OptableConfig;
const DCN_CONFIG: InitConfig;
}

// Provide a global SDK instance across the application
Expand Down
41 changes: 41 additions & 0 deletions lib/config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getConfig } from "./config";
import globalConsent from "./core/regs/consent";

describe("getConfig", () => {
it("returns the default config when no overrides are provided", () => {
expect(getConfig({ host: "host", site: "site" })).toEqual({
host: "host",
site: "site",
cookies: true,
initPassport: true,
consent: { deviceAccess: true, reg: null },
});
});

it("allows overriding all properties", () => {
expect(
getConfig({
host: "host",
site: "site",
cookies: false,
initPassport: false,
consent: { deviceAccess: true, reg: "us" },
})
).toEqual({
host: "host",
site: "site",
cookies: false,
initPassport: false,
consent: { deviceAccess: true, reg: "us" },
});
});

it("resolves to globalConsent when using auto", () => {
const config = getConfig({
host: "host",
site: "site",
consent: "auto",
});
expect(config.consent).toEqual(globalConsent);
});
});
31 changes: 26 additions & 5 deletions lib/config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,39 @@
type OptableConfig = {
import globalConsent, { Consent } from "./core/regs/consent";

type InitConfig = {
host: string;
site: string;
cookies?: boolean;
initPassport?: boolean;
consent?: Consent | "auto";
};

type ResolvedConfig = Required<InitConfig> & {
consent: Consent;
};

const DCN_DEFAULTS = {
cookies: true,
initPassport: true,
// Backwards compatibility, default to device access allowed
// Once we have measured the impact of automatic CMP integration in the wild
// we may move to "auto" default.
consent: { deviceAccess: true, reg: null } as Consent,
};

function getConfig(config: OptableConfig): Required<OptableConfig> {
return { ...DCN_DEFAULTS, ...config };
function getConfig(init: InitConfig): ResolvedConfig {
const config = {
host: init.host,
site: init.site,
cookies: init.cookies ?? DCN_DEFAULTS.cookies,
initPassport: init.initPassport ?? DCN_DEFAULTS.initPassport,
consent: DCN_DEFAULTS.consent,
};

if (init.consent) {
config.consent = init.consent === "auto" ? globalConsent : init.consent;
}
return config;
}

export { OptableConfig, getConfig };
export default OptableConfig;
export { InitConfig, ResolvedConfig, getConfig };
12 changes: 10 additions & 2 deletions lib/core/network.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@ import { default as buildInfo } from "../build.json";

describe("buildRequest", () => {
test("preserves path query string", () => {
const dcn = { cookies: true, host: "host", site: "site" };
const dcn = {
cookies: true,
host: "host",
site: "site",
consent: { reg: "can", gpp: "gpp" },
};

const req = { method: "GET" };
const request = buildRequest("/path?query=string", dcn, req);

expect(request.url).toBe(`https://host/site/path?query=string&osdk=web-${buildInfo.version}&cookies=yes`);
expect(request.url).toBe(
`https://host/site/path?query=string&osdk=web-${buildInfo.version}&reg=can&gpp=gpp&cookies=yes`
);
});
});
18 changes: 15 additions & 3 deletions lib/core/network.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
import type { OptableConfig } from "../config";
import type { ResolvedConfig } from "../config";
import { default as buildInfo } from "../build.json";
import { LocalStorage } from "./storage";

function buildRequest(path: string, config: Required<OptableConfig>, init?: RequestInit): Request {
function buildRequest(path: string, config: ResolvedConfig, init?: RequestInit): Request {
const { site, host, cookies } = config;

const url = new URL(`${site}${path}`, `https://${host}`);
url.searchParams.set("osdk", `web-${buildInfo.version}`);

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

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

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

if (cookies) {
url.searchParams.set("cookies", "yes");
} else {
Expand All @@ -25,7 +37,7 @@ function buildRequest(path: string, config: Required<OptableConfig>, init?: Requ
return request;
}

async function fetch<T>(path: string, config: Required<OptableConfig>, init?: RequestInit): Promise<T> {
async function fetch<T>(path: string, config: ResolvedConfig, init?: RequestInit): Promise<T> {
const response = await globalThis.fetch(buildRequest(path, config, init));

const contentType = response.headers.get("Content-Type");
Expand Down
Loading