@c15t/scripts 3.0.0-alpha.0 → 3.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/README.md +1 -0
- package/dist/engine/runtime.js +14 -3
- package/dist/registry.js +10 -0
- package/dist/vendors/analytics/microsoft-clarity.js +4 -1
- package/dist/vendors/tag-managers/cloudflare-zaraz.js +98 -0
- package/dist-types/__tests__/helpers.d.ts +2 -2
- package/dist-types/engine/compile.d.ts +1 -1
- package/dist-types/engine/runtime.d.ts +1 -1
- package/dist-types/registry.d.ts +9 -0
- package/dist-types/resolve.d.ts +1 -1
- package/dist-types/vendors/_shared/install-builders.d.ts +1 -1
- package/dist-types/vendors/analytics/adobe-analytics.d.ts +1 -1
- package/dist-types/vendors/analytics/matomo-analytics.d.ts +1 -1
- package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +39 -0
- package/docs/README.md +3 -0
- package/docs/frameworks/javascript/script-loader.md +47 -0
- package/docs/frameworks/next/script-loader.md +42 -0
- package/docs/frameworks/react/script-loader.md +8 -0
- package/docs/guides/deployment-modes.md +12 -0
- package/docs/integrations/building-integrations.md +5 -0
- package/docs/integrations/clear-on-revocation.md +167 -0
- package/docs/integrations/cloudflare-zaraz.md +399 -0
- package/docs/integrations/google-maps.md +20 -20
- package/docs/integrations/granular-consent.md +208 -0
- package/docs/integrations/overview.md +5 -4
- package/docs/integrations/youtube.md +26 -21
- package/docs/upgrade-v3.md +47 -0
- package/package.json +7 -2
- package/readme.json +0 -19
package/AGENTS.md
CHANGED
|
@@ -47,14 +47,17 @@ These docs describe v3. Start with Inth hosted setup, identify the framework, ro
|
|
|
47
47
|
- [Ahrefs Analytics](./docs/integrations/ahrefs-analytics.md): Configure Ahrefs Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
48
48
|
- [Amplitude](./docs/integrations/amplitude.md): Configure Amplitude with c15t v3, understand measurement permission and verify loading and revocation.
|
|
49
49
|
- [Custom integrations](./docs/integrations/building-integrations.md): Define loading, initialization and consent-change behavior for a vendor without a helper.
|
|
50
|
+
- [Clear on revocation](./docs/integrations/clear-on-revocation.md): Remove configured first-party cookies and Web Storage keys when their consent category is denied.
|
|
50
51
|
- [Clearbit](./docs/integrations/clearbit.md): Configure Clearbit with c15t v3, understand marketing permission and verify loading and revocation.
|
|
51
52
|
- [Cloudflare Web Analytics](./docs/integrations/cloudflare-web-analytics.md): Configure Cloudflare Web Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
53
|
+
- [Cloudflare Zaraz](./docs/integrations/cloudflare-zaraz.md): Synchronize c15t permissions with Zaraz purposes while Cloudflare manages your tools.
|
|
52
54
|
- [Crisp](./docs/integrations/crisp.md): Configure Crisp with c15t v3, understand functionality permission and verify loading and revocation.
|
|
53
55
|
- [Databuddy](./docs/integrations/databuddy.md): Configure Databuddy's initial and updated consent state with c15t v3.
|
|
54
56
|
- [Fathom Analytics](./docs/integrations/fathom-analytics.md): Configure Fathom Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
55
57
|
- [Google Maps](./docs/integrations/google-maps.md): Prevent a map iframe from mounting before the required permission.
|
|
56
58
|
- [Google Tag](./docs/integrations/google-tag.md): Configure gtag with c15t Consent Mode signals and understand its loading behavior.
|
|
57
59
|
- [Google Tag Manager](./docs/integrations/google-tag-manager.md): Load GTM with c15t consent signals and verify the tags inside your container.
|
|
60
|
+
- [Granular consent](./docs/integrations/granular-consent.md): Let visitors grant a category and still turn one vendor off, without adopting IAB TCF.
|
|
58
61
|
- [Heap](./docs/integrations/heap.md): Configure Heap with c15t v3, understand measurement permission and verify loading and revocation.
|
|
59
62
|
- [Hightouch](./docs/integrations/hightouch.md): Configure Hightouch with c15t v3, understand measurement permission and verify loading and revocation.
|
|
60
63
|
- [Hotjar](./docs/integrations/hotjar.md): Configure Hotjar with c15t v3, understand measurement permission and verify loading and revocation.
|
package/README.md
CHANGED
|
@@ -37,6 +37,7 @@ For further information, guides, and examples visit the [reference documentation
|
|
|
37
37
|
## Integrations
|
|
38
38
|
|
|
39
39
|
- **Google Tag Manager**: Loads with Google Consent Mode v2 defaults set to denied; GTM-managed tags fire only once matching consent is granted ([guide](https://c15t.com/docs/integrations/google-tag-manager))
|
|
40
|
+
- **Cloudflare Zaraz**: Maps c15t effective permissions to Zaraz purpose IDs for tools managed in Cloudflare ([guide](https://c15t.com/docs/integrations/cloudflare-zaraz))
|
|
40
41
|
- **Google Analytics 4 + Google Ads (gtag.js)**: Consent Mode v2 defaults and consent updates when users make a choice ([guide](https://c15t.com/docs/integrations/google-tag))
|
|
41
42
|
- **Conversion pixels**: Meta Pixel, OpenAI Pixel, TikTok Pixel, LinkedIn Insights, Microsoft UET (Microsoft Ads), X Pixel, Reddit Pixel, Snapchat Pixel
|
|
42
43
|
- **Analytics**: PostHog, Amplitude, Heap, Segment, RudderStack, Hightouch, Mixpanel, Microsoft Clarity, Hotjar, Plausible, Fathom, Matomo, Umami, Vercel Analytics
|
package/dist/engine/runtime.js
CHANGED
|
@@ -292,6 +292,16 @@ const runtime_partitionConsentIds = function(mapping, consents) {
|
|
|
292
292
|
deniedConsentIds
|
|
293
293
|
};
|
|
294
294
|
};
|
|
295
|
+
const ALL_DENIED = {
|
|
296
|
+
experience: false,
|
|
297
|
+
functionality: false,
|
|
298
|
+
marketing: false,
|
|
299
|
+
measurement: false,
|
|
300
|
+
necessary: true
|
|
301
|
+
};
|
|
302
|
+
const runtime_signalConsents = function(info) {
|
|
303
|
+
return info.vendor?.granted === false ? ALL_DENIED : info.consents;
|
|
304
|
+
};
|
|
295
305
|
const runtime_getConsentSignalSteps = function(resolvedManifest, mode, consents) {
|
|
296
306
|
if (!resolvedManifest.consentMapping || !resolvedManifest.consentSignal) return [];
|
|
297
307
|
switch(resolvedManifest.consentSignal){
|
|
@@ -346,7 +356,8 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
|
|
|
346
356
|
defer: resolvedManifest.loadScript?.defer,
|
|
347
357
|
id: resolvedManifest.vendor,
|
|
348
358
|
persistAfterConsentRevoked: resolvedManifest.persistAfterConsentRevoked,
|
|
349
|
-
src: resolvedManifest.loadScript?.src
|
|
359
|
+
src: resolvedManifest.loadScript?.src,
|
|
360
|
+
vendor: resolvedManifest.vendor
|
|
350
361
|
};
|
|
351
362
|
if (resolvedManifest.bootstrapSteps.length > 0 || resolvedManifest.setupSteps.length > 0 || resolvedManifest.onBeforeLoadGrantedSteps.length > 0 || resolvedManifest.onBeforeLoadDeniedSteps.length > 0 || hasConsentMapping) script.onBeforeLoad = (info)=>{
|
|
352
363
|
const baseContext = {
|
|
@@ -359,7 +370,7 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
|
|
|
359
370
|
...baseContext,
|
|
360
371
|
phase: 'bootstrap'
|
|
361
372
|
});
|
|
362
|
-
runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'default', info
|
|
373
|
+
runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'default', runtime_signalConsents(info)), {
|
|
363
374
|
...baseContext,
|
|
364
375
|
phase: 'consent-default'
|
|
365
376
|
});
|
|
@@ -415,7 +426,7 @@ const runtime_resolvedManifestToScript = function(resolvedManifest) {
|
|
|
415
426
|
hasConsent: info.hasConsent,
|
|
416
427
|
scriptId: resolvedManifest.vendor
|
|
417
428
|
};
|
|
418
|
-
runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'update', info
|
|
429
|
+
runtime_executePhaseSteps(runtime_getConsentSignalSteps(resolvedManifest, 'update', runtime_signalConsents(info)), {
|
|
419
430
|
...baseContext,
|
|
420
431
|
phase: 'consent-update'
|
|
421
432
|
});
|
package/dist/registry.js
CHANGED
|
@@ -17,6 +17,16 @@ const BUILT_IN_INTEGRATION_CATEGORIES = [
|
|
|
17
17
|
}
|
|
18
18
|
];
|
|
19
19
|
const builtInScriptIntegrations = [
|
|
20
|
+
{
|
|
21
|
+
consentCategory: 'necessary',
|
|
22
|
+
docsSlug: 'cloudflare-zaraz',
|
|
23
|
+
hint: 'Consent bridge for tools managed by Zaraz',
|
|
24
|
+
integrationCategory: 'tag-manager',
|
|
25
|
+
key: 'cloudflareZaraz',
|
|
26
|
+
label: 'Cloudflare Zaraz',
|
|
27
|
+
packageSubpath: 'cloudflare-zaraz',
|
|
28
|
+
vendor: 'cloudflare-zaraz'
|
|
29
|
+
},
|
|
20
30
|
{
|
|
21
31
|
consentCategory: 'necessary',
|
|
22
32
|
docsSlug: 'google-tag-manager',
|
|
@@ -48,7 +48,10 @@ const microsoft_clarity_getClarityConsentPayload = function(consents, defaultCon
|
|
|
48
48
|
return fallback;
|
|
49
49
|
};
|
|
50
50
|
const microsoft_clarity_syncClarityConsent = function(info, defaultConsent) {
|
|
51
|
-
window.clarity?.('consentv2',
|
|
51
|
+
window.clarity?.('consentv2', info.vendor?.granted === false ? {
|
|
52
|
+
ad_Storage: 'denied',
|
|
53
|
+
analytics_Storage: 'denied'
|
|
54
|
+
} : microsoft_clarity_getClarityConsentPayload(info.consents, defaultConsent));
|
|
52
55
|
};
|
|
53
56
|
const clarityManifest = {
|
|
54
57
|
...vendorManifestContract,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const consentCategories = [
|
|
2
|
+
'necessary',
|
|
3
|
+
'functionality',
|
|
4
|
+
'experience',
|
|
5
|
+
'measurement',
|
|
6
|
+
'marketing'
|
|
7
|
+
];
|
|
8
|
+
const readyEvent = 'zarazConsentAPIReady';
|
|
9
|
+
const isConsentApi = (api)=>'object' == typeof api && null !== api && 'APIReady' in api && true === api.APIReady && 'getAll' in api && 'function' == typeof api.getAll && 'set' in api && 'function' == typeof api.set && 'sendQueuedEvents' in api && 'function' == typeof api.sendQueuedEvents;
|
|
10
|
+
const getConsentApi = ()=>{
|
|
11
|
+
if ("u" < typeof window || !('zaraz' in window)) return;
|
|
12
|
+
const { zaraz } = window;
|
|
13
|
+
if ('object' != typeof zaraz || null === zaraz || !('consent' in zaraz)) return;
|
|
14
|
+
return isConsentApi(zaraz.consent) ? zaraz.consent : void 0;
|
|
15
|
+
};
|
|
16
|
+
const cloudflareZaraz = (options)=>{
|
|
17
|
+
const mappings = new Map();
|
|
18
|
+
for (const category of consentCategories)for (const purpose of options.purposes[category] ?? []){
|
|
19
|
+
if (!purpose.trim() || purpose !== purpose.trim()) throw new Error('Zaraz purpose IDs must be non-empty and have no surrounding whitespace.');
|
|
20
|
+
if (mappings.has(purpose)) throw new Error(`Zaraz purpose '${purpose}' is mapped more than once.`);
|
|
21
|
+
mappings.set(purpose, category);
|
|
22
|
+
}
|
|
23
|
+
if (0 === mappings.size) throw new Error('Map at least one Zaraz purpose to a c15t category.');
|
|
24
|
+
let latest;
|
|
25
|
+
let listeningDocument;
|
|
26
|
+
let initialized = false;
|
|
27
|
+
const pendingReplay = new Set();
|
|
28
|
+
const synchronize = ()=>{
|
|
29
|
+
const api = getConsentApi();
|
|
30
|
+
if (!latest || !api) return false;
|
|
31
|
+
const previous = api.getAll();
|
|
32
|
+
const permissions = {};
|
|
33
|
+
let changed = false;
|
|
34
|
+
for (const purpose of Object.keys(previous)){
|
|
35
|
+
const category = mappings.get(purpose);
|
|
36
|
+
const allowed = void 0 !== category && true === latest[category];
|
|
37
|
+
Object.defineProperty(permissions, purpose, {
|
|
38
|
+
enumerable: true,
|
|
39
|
+
value: allowed
|
|
40
|
+
});
|
|
41
|
+
changed ||= previous[purpose] !== allowed;
|
|
42
|
+
if (allowed && true !== previous[purpose]) pendingReplay.add(purpose);
|
|
43
|
+
}
|
|
44
|
+
for (const purpose of pendingReplay)if (true !== permissions[purpose]) pendingReplay.delete(purpose);
|
|
45
|
+
if (false !== options.hideBuiltInModal && true === api.modal) api.modal = false;
|
|
46
|
+
if (changed) api.set(permissions);
|
|
47
|
+
if (pendingReplay.size > 0 && false !== options.sendQueuedEvents) api.sendQueuedEvents();
|
|
48
|
+
pendingReplay.clear();
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
const apply = ()=>{
|
|
52
|
+
try {
|
|
53
|
+
if (!synchronize()) return false;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (!options.onError) throw error;
|
|
56
|
+
options.onError(error);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
listeningDocument?.removeEventListener(readyEvent, apply);
|
|
60
|
+
listeningDocument = void 0;
|
|
61
|
+
if (!initialized) {
|
|
62
|
+
initialized = true;
|
|
63
|
+
options.onReady?.();
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
};
|
|
67
|
+
const update = ({ consents })=>{
|
|
68
|
+
latest = consents;
|
|
69
|
+
if ("u" < typeof document) return;
|
|
70
|
+
let applied = false;
|
|
71
|
+
try {
|
|
72
|
+
applied = apply();
|
|
73
|
+
} finally{
|
|
74
|
+
if (!applied && !listeningDocument) {
|
|
75
|
+
listeningDocument = document;
|
|
76
|
+
document.addEventListener(readyEvent, apply);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
return {
|
|
81
|
+
alwaysLoad: true,
|
|
82
|
+
callbackOnly: true,
|
|
83
|
+
category: 'necessary',
|
|
84
|
+
id: 'cloudflare-zaraz',
|
|
85
|
+
onConsentChange: (info)=>{
|
|
86
|
+
if (info.hasConsent) update(info);
|
|
87
|
+
},
|
|
88
|
+
onDispose: ()=>{
|
|
89
|
+
listeningDocument?.removeEventListener(readyEvent, apply);
|
|
90
|
+
listeningDocument = void 0;
|
|
91
|
+
latest = void 0;
|
|
92
|
+
initialized = false;
|
|
93
|
+
pendingReplay.clear();
|
|
94
|
+
},
|
|
95
|
+
onLoad: update
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
export { cloudflareZaraz };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConsentState, Script, ScriptCallbackInfo } from '@c15t/core';
|
|
2
|
-
import type { BuiltInScriptIntegrationKey } from '../registry';
|
|
2
|
+
import type { BuiltInScriptIntegrationKey } from '../registry.js';
|
|
3
3
|
/**
|
|
4
4
|
* Typed view of `globalThis` used by script helper tests.
|
|
5
5
|
*
|
|
@@ -31,7 +31,7 @@ export interface ExpectedScriptSnapshot {
|
|
|
31
31
|
/** Expected consent-revocation persistence flag from the generated script. */
|
|
32
32
|
persistAfterConsentRevoked: boolean | undefined;
|
|
33
33
|
/** Expected remote script URL from the generated helper script. */
|
|
34
|
-
src: string;
|
|
34
|
+
src: string | undefined;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Consent state with only required storage enabled.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { ResolvedManifest, VendorManifest } from '../types';
|
|
1
|
+
import type { ResolvedManifest, VendorManifest } from '../types.js';
|
|
2
2
|
export declare const interpolateValue: (value: unknown, config: Record<string, unknown>) => unknown;
|
|
3
3
|
export declare const compileManifest: (manifest: VendorManifest, config?: Record<string, unknown>) => ResolvedManifest;
|
package/dist-types/registry.d.ts
CHANGED
|
@@ -77,6 +77,15 @@ export declare const BUILT_IN_INTEGRATION_CATEGORIES: readonly [{
|
|
|
77
77
|
* the CLI can discover it from the same source.
|
|
78
78
|
*/
|
|
79
79
|
export declare const builtInScriptIntegrations: readonly [{
|
|
80
|
+
readonly consentCategory: 'necessary';
|
|
81
|
+
readonly docsSlug: 'cloudflare-zaraz';
|
|
82
|
+
readonly hint: 'Consent bridge for tools managed by Zaraz';
|
|
83
|
+
readonly integrationCategory: 'tag-manager';
|
|
84
|
+
readonly key: 'cloudflareZaraz';
|
|
85
|
+
readonly label: 'Cloudflare Zaraz';
|
|
86
|
+
readonly packageSubpath: 'cloudflare-zaraz';
|
|
87
|
+
readonly vendor: 'cloudflare-zaraz';
|
|
88
|
+
}, {
|
|
80
89
|
readonly consentCategory: 'necessary';
|
|
81
90
|
readonly docsSlug: 'google-tag-manager';
|
|
82
91
|
readonly hint: 'GTM container script';
|
package/dist-types/resolve.d.ts
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { AllConsentNames, Script } from '@c15t/core';
|
|
2
|
+
/** The Zaraz Consent API methods used by the bridge. */
|
|
3
|
+
export interface ZarazConsentApi {
|
|
4
|
+
APIReady: boolean;
|
|
5
|
+
modal?: unknown;
|
|
6
|
+
getAll: () => Record<string, boolean>;
|
|
7
|
+
set: (permissions: Record<string, boolean>) => void;
|
|
8
|
+
sendQueuedEvents: () => void;
|
|
9
|
+
}
|
|
10
|
+
export interface CloudflareZarazOptions {
|
|
11
|
+
/** Map c15t categories to purpose IDs from the Zaraz dashboard. */
|
|
12
|
+
purposes: Partial<Record<AllConsentNames, readonly string[]>>;
|
|
13
|
+
/** Hide the currently visible Zaraz modal. Disable auto-display in Zaraz too. @default true */
|
|
14
|
+
hideBuiltInModal?: boolean;
|
|
15
|
+
/** Replay Zaraz's queued pageview events after a denied purpose becomes allowed. @default true */
|
|
16
|
+
sendQueuedEvents?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Called once after the first successful synchronization. With automatic
|
|
19
|
+
* pageviews disabled in Zaraz, send the initial Pageview from this callback.
|
|
20
|
+
*/
|
|
21
|
+
onReady?: () => void;
|
|
22
|
+
/** Called when synchronization fails. Retried on the next consent update or readiness event. */
|
|
23
|
+
onError?: (error: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Synchronize c15t effective permissions with externally loaded Zaraz tools.
|
|
27
|
+
* Does not load Zaraz or configure tools. Unmapped Zaraz purposes are denied.
|
|
28
|
+
* Disable automatic pageviews and send the first Pageview from onReady to avoid
|
|
29
|
+
* running tools with a stale Zaraz consent cookie before synchronization.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Purpose mapping and synchronization callbacks.
|
|
32
|
+
* @returns A callback-only script for the existing c15t script loader.
|
|
33
|
+
* @throws {Error} If the mapping is empty, contains blank IDs, or maps an ID twice.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* cloudflareZaraz({ purposes: { measurement: ['analytics-purpose'] } });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare const cloudflareZaraz: (options: CloudflareZarazOptions) => Script;
|
package/docs/README.md
CHANGED
|
@@ -47,14 +47,17 @@ These docs describe v3. Start with Inth hosted setup, identify the framework, ro
|
|
|
47
47
|
- [Ahrefs Analytics](./integrations/ahrefs-analytics.md): Configure Ahrefs Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
48
48
|
- [Amplitude](./integrations/amplitude.md): Configure Amplitude with c15t v3, understand measurement permission and verify loading and revocation.
|
|
49
49
|
- [Custom integrations](./integrations/building-integrations.md): Define loading, initialization and consent-change behavior for a vendor without a helper.
|
|
50
|
+
- [Clear on revocation](./integrations/clear-on-revocation.md): Remove configured first-party cookies and Web Storage keys when their consent category is denied.
|
|
50
51
|
- [Clearbit](./integrations/clearbit.md): Configure Clearbit with c15t v3, understand marketing permission and verify loading and revocation.
|
|
51
52
|
- [Cloudflare Web Analytics](./integrations/cloudflare-web-analytics.md): Configure Cloudflare Web Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
53
|
+
- [Cloudflare Zaraz](./integrations/cloudflare-zaraz.md): Synchronize c15t permissions with Zaraz purposes while Cloudflare manages your tools.
|
|
52
54
|
- [Crisp](./integrations/crisp.md): Configure Crisp with c15t v3, understand functionality permission and verify loading and revocation.
|
|
53
55
|
- [Databuddy](./integrations/databuddy.md): Configure Databuddy's initial and updated consent state with c15t v3.
|
|
54
56
|
- [Fathom Analytics](./integrations/fathom-analytics.md): Configure Fathom Analytics with c15t v3, understand measurement permission and verify loading and revocation.
|
|
55
57
|
- [Google Maps](./integrations/google-maps.md): Prevent a map iframe from mounting before the required permission.
|
|
56
58
|
- [Google Tag](./integrations/google-tag.md): Configure gtag with c15t Consent Mode signals and understand its loading behavior.
|
|
57
59
|
- [Google Tag Manager](./integrations/google-tag-manager.md): Load GTM with c15t consent signals and verify the tags inside your container.
|
|
60
|
+
- [Granular consent](./integrations/granular-consent.md): Let visitors grant a category and still turn one vendor off, without adopting IAB TCF.
|
|
58
61
|
- [Heap](./integrations/heap.md): Configure Heap with c15t v3, understand measurement permission and verify loading and revocation.
|
|
59
62
|
- [Hightouch](./integrations/hightouch.md): Configure Hightouch with c15t v3, understand measurement permission and verify loading and revocation.
|
|
60
63
|
- [Hotjar](./integrations/hotjar.md): Configure Hotjar with c15t v3, understand measurement permission and verify loading and revocation.
|
|
@@ -45,3 +45,50 @@ assuming all helpers have the same network behavior.
|
|
|
45
45
|
The loader exposes `updateScripts`, `getLoadedScriptIds` and `dispose`.
|
|
46
46
|
Unloading an element cannot reverse requests or code that already ran. Test
|
|
47
47
|
revocation and vendor cleanup with [verification](../../guides/verify-consent.md).
|
|
48
|
+
|
|
49
|
+
## Dispose integration resources
|
|
50
|
+
|
|
51
|
+
Ordinary scripts keep their mounted resource when `updateScripts` receives a
|
|
52
|
+
fresh object with the same ID and unchanged element configuration. New callback
|
|
53
|
+
functions alone do not reload the vendor or send a temporary denial. Changing
|
|
54
|
+
the source, inline code or element attributes starts a new loading lifecycle.
|
|
55
|
+
Consent conditions are reevaluated on every configuration update. Pending
|
|
56
|
+
load and error events use the latest registered callbacks and consent state.
|
|
57
|
+
Setting `persistAfterConsentRevoked` to `false` removes an owned retained element
|
|
58
|
+
when the script no longer has consent.
|
|
59
|
+
|
|
60
|
+
Custom script configurations can use `onDispose(info)` to release event
|
|
61
|
+
listeners or other resources. Adding this hook opts the configuration into an
|
|
62
|
+
object-owned lifecycle: replacing the object disposes its resources and starts
|
|
63
|
+
again, even with the same ID. Keep these objects stable across framework
|
|
64
|
+
rerenders. The loader also calls the hook when the configuration is removed or
|
|
65
|
+
the loader is disposed, including configurations that never loaded.
|
|
66
|
+
`info.element` contains the last loaded or retained element when available,
|
|
67
|
+
even when configuration removal has already detached it.
|
|
68
|
+
|
|
69
|
+
Duplicate references receive one cleanup per registration. Re-registering a
|
|
70
|
+
removed object starts a new lifecycle. Updates requested from lifecycle
|
|
71
|
+
callbacks run after the current pass; if several are requested, the latest
|
|
72
|
+
configuration wins. Disposal stops further reconciliation. A callback feedback
|
|
73
|
+
loop exceeding 100 consecutive passes disposes the loader and reports an
|
|
74
|
+
`error` debug event. Avoid callbacks that keep changing consent or replacing
|
|
75
|
+
their own configuration.
|
|
76
|
+
|
|
77
|
+
`onBeforeLoad` prepares a loading attempt. If a callback changes consent or
|
|
78
|
+
replaces configurations, the loader cancels that attempt before loading and
|
|
79
|
+
reevaluates the latest state. A still-eligible script can retry preparation
|
|
80
|
+
with a new element and updated consent. Make `onBeforeLoad` safe to repeat;
|
|
81
|
+
use `onLoad` for initialization that requires a completed load. This also
|
|
82
|
+
applies to callback-only scripts, whose `onLoad` is skipped when preparation
|
|
83
|
+
invalidates the current pass.
|
|
84
|
+
|
|
85
|
+
Consent revocation alone does not call `onDispose`. Use `onConsentChange` for
|
|
86
|
+
vendor opt-out commands. Cleanup errors are reported through the loader's debug
|
|
87
|
+
events and do not prevent other configurations from being cleaned up.
|
|
88
|
+
|
|
89
|
+
## Clear stored tracking data
|
|
90
|
+
|
|
91
|
+
Script gating does not remove cookies or Web Storage entries that a script
|
|
92
|
+
already wrote. Configure [clear on revocation](../../integrations/clear-on-revocation.md)
|
|
93
|
+
on your runtime, or attach its module to your existing kernel, to remove
|
|
94
|
+
declared data when its category is denied.
|
|
@@ -166,3 +166,45 @@ with DevTools, or follow [verification](../../guides/verify-consent.md) in your
|
|
|
166
166
|
Use [custom integrations](../../integrations/building-integrations.md) for an
|
|
167
167
|
unlisted vendor. Google helpers have a separate
|
|
168
168
|
[Consent Mode contract](../../integrations/google-tag-manager.md).
|
|
169
|
+
|
|
170
|
+
## Granular consent
|
|
171
|
+
|
|
172
|
+
A visitor can grant marketing and still turn one vendor off. Declare the
|
|
173
|
+
vendors next to the scripts in `lib/scripts.ts`, with the `vendor` slug each
|
|
174
|
+
script already carries, and pass both to `ConsentRoot`:
|
|
175
|
+
|
|
176
|
+
```ts title="lib/scripts.ts"
|
|
177
|
+
import type { Vendor } from 'c15t';
|
|
178
|
+
|
|
179
|
+
export const vendors: Vendor[] = [
|
|
180
|
+
{
|
|
181
|
+
id: 'x-pixel',
|
|
182
|
+
name: 'X Pixel',
|
|
183
|
+
category: 'marketing',
|
|
184
|
+
privacyPolicyUrl: 'https://x.com/privacy',
|
|
185
|
+
},
|
|
186
|
+
];
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```tsx title="components/consent.tsx"
|
|
190
|
+
import { scripts, vendors } from '../lib/scripts';
|
|
191
|
+
|
|
192
|
+
<ConsentRoot state={state} config={consentConfig} scripts={scripts} vendors={vendors}>
|
|
193
|
+
{children}
|
|
194
|
+
</ConsentRoot>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The preference center lists each vendor under its category with a switch.
|
|
198
|
+
Integrations from `@c15t/scripts` set `vendor` to their manifest slug, so
|
|
199
|
+
`xPixel()` needs no extra wiring; give a hand-written script the same slug
|
|
200
|
+
in its `vendor` field. A backend manifest can declare vendors too. See
|
|
201
|
+
[granular consent](../../integrations/granular-consent.md) for storage,
|
|
202
|
+
bulk actions and the hooks a custom control uses.
|
|
203
|
+
|
|
204
|
+
## Clear stored tracking data
|
|
205
|
+
|
|
206
|
+
Script gating does not remove cookies or Web Storage entries that a script
|
|
207
|
+
already wrote. Add `clearOnRevocation` to your `ConsentRoot` or provider
|
|
208
|
+
options to remove declared data when its category is denied. See
|
|
209
|
+
[clear on revocation](../../integrations/clear-on-revocation.md) for configuration
|
|
210
|
+
and browser limits.
|
|
@@ -53,3 +53,11 @@ Use [custom integrations](../../integrations/building-integrations.md) for an
|
|
|
53
53
|
unlisted vendor and [verification](../../guides/verify-consent.md) for the network
|
|
54
54
|
checks. Google helpers have a separate
|
|
55
55
|
[Consent Mode contract](../../integrations/google-tag-manager.md).
|
|
56
|
+
|
|
57
|
+
## Clear stored tracking data
|
|
58
|
+
|
|
59
|
+
Script gating does not remove cookies or Web Storage entries that a script
|
|
60
|
+
already wrote. Add `clearOnRevocation` to `ConsentProvider.options` to remove
|
|
61
|
+
declared data when its category is denied. See
|
|
62
|
+
[clear on revocation](../../integrations/clear-on-revocation.md) for configuration
|
|
63
|
+
and browser limits.
|
|
@@ -30,6 +30,18 @@ in your application while consent writes still go to Inth. Regular backend
|
|
|
30
30
|
browser setup. See [data fetching and transports](./data-fetching.md) for
|
|
31
31
|
the comparison, including custom transports and offline mode.
|
|
32
32
|
|
|
33
|
+
Manifest resolution removes the per-visitor `/init` request, and with it the
|
|
34
|
+
backend's only count of visitors. The server adapters replace it with a session
|
|
35
|
+
report: after each resolution, on a server-rendered page or the same-origin
|
|
36
|
+
init route, the host posts a small report to the backend's `POST /sessions`,
|
|
37
|
+
server-to-server and detached from the response. The browser makes no request
|
|
38
|
+
and the report stores no identity; the visitor's IP and user agent travel as
|
|
39
|
+
forwarded headers under the backend's usual IP handling. Each report is one
|
|
40
|
+
resolution; a page view can produce a `render` and a `route` report, and the
|
|
41
|
+
consuming side groups them into sessions by address and user agent within a
|
|
42
|
+
window. Static output resolves in the browser and sends none. Set `reportSessions: false` on an adapter to turn
|
|
43
|
+
it off.
|
|
44
|
+
|
|
33
45
|
## Match initialization to your application output
|
|
34
46
|
|
|
35
47
|
| Application output | Initial state | Required setup |
|
|
@@ -31,6 +31,11 @@ Replace the example URL and implement the vendor's initialization. This is a
|
|
|
31
31
|
loader template, not a functioning analytics SDK. The script stays blocked
|
|
32
32
|
while measurement permission is denied.
|
|
33
33
|
|
|
34
|
+
Add `vendor: 'example-analytics'` and declare the vendor in the runtime's
|
|
35
|
+
`vendors` option or in the backend manifest when visitors should be able to
|
|
36
|
+
turn this vendor off inside a granted category. See
|
|
37
|
+
[granular consent](./granular-consent.md).
|
|
38
|
+
|
|
34
39
|
## Define revocation deliberately
|
|
35
40
|
|
|
36
41
|
`onConsentChange` receives current permission information. Use it to update the
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Clear on revocation
|
|
3
|
+
description: Remove configured first-party cookies and Web Storage keys when
|
|
4
|
+
their consent category is denied.
|
|
5
|
+
group: integrations
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Configure cleanup
|
|
9
|
+
|
|
10
|
+
Add `clearOnRevocation` to your provider or runtime options. Declare only the
|
|
11
|
+
data owned by each optional category:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { hosted, type ClearOnRevocationConfig } from 'c15t';
|
|
15
|
+
import { createConsentRuntime } from 'c15t/runtime';
|
|
16
|
+
|
|
17
|
+
const clearOnRevocation = {
|
|
18
|
+
measurement: {
|
|
19
|
+
cookies: ['_ga', '_ga_*'],
|
|
20
|
+
localStorage: ['analytics:*'],
|
|
21
|
+
},
|
|
22
|
+
marketing: {
|
|
23
|
+
cookies: ['_fbp'],
|
|
24
|
+
sessionStorage: ['campaign-id'],
|
|
25
|
+
},
|
|
26
|
+
} satisfies ClearOnRevocationConfig;
|
|
27
|
+
|
|
28
|
+
const runtime = createConsentRuntime({
|
|
29
|
+
mode: hosted({ url: '/api/c15t' }),
|
|
30
|
+
clearOnRevocation,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Start in the browser after mount. The endpoint must serve your c15t backend.
|
|
34
|
+
runtime.start();
|
|
35
|
+
|
|
36
|
+
// Call runtime.dispose() when the app no longer needs consent management.
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
For React, pass the same configuration through `ConsentProvider.options`:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import type { ReactNode } from 'react';
|
|
43
|
+
import { ConsentProvider, hosted } from 'c15t/react';
|
|
44
|
+
|
|
45
|
+
const mode = hosted({ url: '/api/c15t' });
|
|
46
|
+
|
|
47
|
+
export function Consent({ children }: { children: ReactNode }) {
|
|
48
|
+
return (
|
|
49
|
+
<ConsentProvider
|
|
50
|
+
options={{
|
|
51
|
+
mode,
|
|
52
|
+
clearOnRevocation: {
|
|
53
|
+
measurement: { cookies: ['_ga', '_ga_*'] },
|
|
54
|
+
},
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</ConsentProvider>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The same option is available in Next.js and TanStack Start `ConsentRoot` props, Vue and
|
|
64
|
+
Nuxt configuration, Svelte providers, and Astro integration options. Solid and
|
|
65
|
+
other headless integrations can use `createConsentRuntime` as shown above.
|
|
66
|
+
Every adapter uses the same cleanup module.
|
|
67
|
+
|
|
68
|
+
Omitting `clearOnRevocation` leaves cleanup disabled. The provider option is
|
|
69
|
+
initial-only. Remount the provider to replace its cleanup configuration. For
|
|
70
|
+
a shared runtime, configure the runtime owner rather than a borrowing provider.
|
|
71
|
+
|
|
72
|
+
## Matching names and cookie scopes
|
|
73
|
+
|
|
74
|
+
Use an exact string or a nonempty prefix followed by `*`. `_ga_*` matches
|
|
75
|
+
`_ga_ABC123`; it does not match `_ga`. Regular expressions, wildcards in other
|
|
76
|
+
positions, and a bare `*` are unsupported. Web Storage keys may contain spaces,
|
|
77
|
+
Unicode, and punctuation. Cookie names use their raw spelling, without URL
|
|
78
|
+
decoding.
|
|
79
|
+
|
|
80
|
+
Cookies can share a name while having different domains or paths. Cleanup
|
|
81
|
+
tries the current host and its parent domains, and the current path and its
|
|
82
|
+
ancestors. To target a specific scope, use an object:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
const clearOnRevocation = {
|
|
86
|
+
measurement: {
|
|
87
|
+
cookies: [
|
|
88
|
+
{ name: 'analytics-id', domain: 'example.com', path: '/' },
|
|
89
|
+
{ name: 'checkout-metrics', domain: '', path: '/checkout' },
|
|
90
|
+
{ name: 'partitioned-metrics', partitioned: true },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
An empty `domain` means host-only. Explicit domains and paths replace the
|
|
97
|
+
automatic attempts for that field. Exact names can be deleted at a configured
|
|
98
|
+
path even when the current page cannot read that cookie. Prefix matching can
|
|
99
|
+
only discover cookie names visible to the current page, so use an exact name
|
|
100
|
+
for a cookie on another path.
|
|
101
|
+
|
|
102
|
+
Partitioned cookies require `partitioned: true`; ordinary targets remove
|
|
103
|
+
unpartitioned cookies. Cookie deletion preserves the browser's `__Secure-`
|
|
104
|
+
and `__Host-` prefix requirements. c15t protects its own consent, notice,
|
|
105
|
+
privacy, pending-save, and IAB consent records in cookies and localStorage,
|
|
106
|
+
including configured custom storage keys, even if your patterns match them.
|
|
107
|
+
c15t does not store consent in sessionStorage, so targeted entries there are
|
|
108
|
+
removed even when their names match consent storage keys.
|
|
109
|
+
|
|
110
|
+
## When cleanup runs
|
|
111
|
+
|
|
112
|
+
The runtime attaches cleanup after persistence and the script loader. Cleanup
|
|
113
|
+
waits while the policy is pending. On the first settled snapshot, it removes
|
|
114
|
+
configured data for every denied category. This includes a new opt-in visitor
|
|
115
|
+
who has not made a choice and a returning visitor whose permission expired.
|
|
116
|
+
|
|
117
|
+
After that first pass, cleanup runs when a category changes from allowed to
|
|
118
|
+
denied. Saving a refusal, expiry, a policy change, Global Privacy Control,
|
|
119
|
+
or synchronized records can cause that transition. Under an opt-out policy,
|
|
120
|
+
an expired grant that remains effectively allowed does not trigger deletion.
|
|
121
|
+
The `necessary` category cannot be configured for cleanup.
|
|
122
|
+
|
|
123
|
+
Cleanup keeps waiting if a failed initial request leaves the policy pending.
|
|
124
|
+
If a previously settled policy falls back to denial after an initialization
|
|
125
|
+
failure, cleanup removes its configured data. A later successful retry cannot
|
|
126
|
+
restore deleted data.
|
|
127
|
+
|
|
128
|
+
Runtime construction, server rendering, draft checkbox edits, opening the
|
|
129
|
+
dialog, and disposal do not clear data. Cleanup does not poll storage or repeat
|
|
130
|
+
on unrelated UI updates.
|
|
131
|
+
|
|
132
|
+
## Use an existing kernel
|
|
133
|
+
|
|
134
|
+
For a manually assembled integration, attach the module in the browser after
|
|
135
|
+
persistence hydration and script-loader setup:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createClearOnRevocation } from 'c15t/modules/clear-on-revocation';
|
|
139
|
+
|
|
140
|
+
const cleanup = createClearOnRevocation({
|
|
141
|
+
kernel,
|
|
142
|
+
config: { measurement: { cookies: ['_ga', '_ga_*'] } },
|
|
143
|
+
storageConfig,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Stop observing consent when this integration is torn down.
|
|
147
|
+
cleanup.dispose();
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Here `kernel` is your existing consent kernel. Pass the same `storageConfig`
|
|
151
|
+
used by persistence so cleanup protects custom record keys. Attaching the
|
|
152
|
+
module can immediately clear denied categories if the policy is already
|
|
153
|
+
settled. Do not also attach it when your provider or runtime owns cleanup.
|
|
154
|
+
|
|
155
|
+
## Browser limits
|
|
156
|
+
|
|
157
|
+
Cleanup can remove JavaScript-accessible first-party cookies and keys in the
|
|
158
|
+
current origin's `localStorage` and `sessionStorage`. It cannot remove
|
|
159
|
+
`HttpOnly` cookies or another origin's data. Keep `HttpOnly` protections and
|
|
160
|
+
use your server to expire cookies that require server access. Cookie deletion
|
|
161
|
+
must match the cookie's scope. See the
|
|
162
|
+
[browser cookie documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies).
|
|
163
|
+
|
|
164
|
+
Browser restrictions can prevent reads or deletions. Cleanup failures do not
|
|
165
|
+
block consent updates. A running SDK may write data again after a sweep, so
|
|
166
|
+
keep script gating and the integration's consent-change or teardown behavior
|
|
167
|
+
configured. Deleting a script element cannot undo JavaScript it already ran.
|