@c15t/scripts 3.0.0-alpha.2 → 3.0.0-alpha.3

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.
Files changed (300) hide show
  1. package/AGENTS.md +129 -63
  2. package/README.md +8 -29
  3. package/SKILL.md +33 -0
  4. package/dist/adobe-analytics.js +2 -0
  5. package/dist/ahrefs-analytics.js +2 -0
  6. package/dist/amplitude.js +2 -0
  7. package/dist/clearbit.js +2 -0
  8. package/dist/cloudflare-web-analytics.js +2 -0
  9. package/dist/cloudflare-zaraz.js +2 -0
  10. package/dist/crisp.js +2 -0
  11. package/dist/databuddy.js +2 -0
  12. package/dist/e2e-test-utils.js +2 -139
  13. package/dist/engine/compile.js +2 -89
  14. package/dist/engine/runtime.js +2 -448
  15. package/dist/events.js +2 -218
  16. package/dist/fathom-analytics.js +2 -0
  17. package/dist/front-chat.js +2 -0
  18. package/dist/google-tag-manager.js +2 -0
  19. package/dist/google-tag.js +2 -0
  20. package/dist/heap.js +2 -0
  21. package/dist/hightouch.js +2 -0
  22. package/dist/hotjar.js +2 -0
  23. package/dist/intercom.js +2 -0
  24. package/dist/klaviyo.js +2 -0
  25. package/dist/linkedin-insights.js +2 -0
  26. package/dist/logrocket.js +2 -0
  27. package/dist/matomo-analytics.js +2 -0
  28. package/dist/meta-pixel.js +2 -0
  29. package/dist/microsoft-clarity.js +2 -0
  30. package/dist/microsoft-uet.js +2 -0
  31. package/dist/mixpanel-analytics.js +2 -0
  32. package/dist/one-dollar-stats.js +2 -0
  33. package/dist/openai-pixel.js +2 -0
  34. package/dist/pinterest-tag.js +2 -0
  35. package/dist/pirsch.js +2 -0
  36. package/dist/plausible-analytics.js +2 -0
  37. package/dist/posthog.js +2 -0
  38. package/dist/promptwatch.js +2 -0
  39. package/dist/reddit-pixel.js +2 -0
  40. package/dist/registry.js +2 -422
  41. package/dist/resolve.js +2 -33
  42. package/dist/rudderstack.js +2 -0
  43. package/dist/rybbit-analytics.js +2 -0
  44. package/dist/segment.js +2 -0
  45. package/dist/snapchat-pixel.js +2 -0
  46. package/dist/tiktok-pixel.js +2 -0
  47. package/dist/types.js +2 -16
  48. package/dist/umami-analytics.js +2 -0
  49. package/dist/vendors/_shared/attributes.js +2 -14
  50. package/dist/vendors/_shared/google-consent.js +2 -27
  51. package/dist/vendors/_shared/install-builders.js +2 -21
  52. package/dist/vendors/_shared/required-id.js +2 -0
  53. package/dist/vendors/_shared/script-url.js +2 -28
  54. package/dist/vendors/ads-and-pixels/linkedin-insights.js +2 -48
  55. package/dist/vendors/ads-and-pixels/meta-pixel.js +2 -153
  56. package/dist/vendors/ads-and-pixels/microsoft-uet.js +2 -110
  57. package/dist/vendors/ads-and-pixels/openai-pixel.js +2 -88
  58. package/dist/vendors/ads-and-pixels/pinterest-tag.js +2 -123
  59. package/dist/vendors/ads-and-pixels/reddit-pixel.js +2 -107
  60. package/dist/vendors/ads-and-pixels/snapchat-pixel.js +2 -87
  61. package/dist/vendors/ads-and-pixels/tiktok-pixel.js +2 -89
  62. package/dist/vendors/ads-and-pixels/x-pixel.js +2 -48
  63. package/dist/vendors/analytics/adobe-analytics.js +2 -49
  64. package/dist/vendors/analytics/ahrefs-analytics.js +2 -27
  65. package/dist/vendors/analytics/amplitude.js +2 -134
  66. package/dist/vendors/analytics/clearbit.js +2 -28
  67. package/dist/vendors/analytics/cloudflare-web-analytics.js +2 -32
  68. package/dist/vendors/analytics/databuddy.js +2 -103
  69. package/dist/vendors/analytics/fathom-analytics.js +2 -35
  70. package/dist/vendors/analytics/google-tag.js +2 -78
  71. package/dist/vendors/analytics/heap.js +2 -134
  72. package/dist/vendors/analytics/hightouch.js +2 -109
  73. package/dist/vendors/analytics/hotjar.js +2 -44
  74. package/dist/vendors/analytics/logrocket.js +2 -58
  75. package/dist/vendors/analytics/matomo-analytics.js +2 -191
  76. package/dist/vendors/analytics/microsoft-clarity.js +2 -100
  77. package/dist/vendors/analytics/mixpanel-analytics.js +2 -93
  78. package/dist/vendors/analytics/one-dollar-stats.js +2 -30
  79. package/dist/vendors/analytics/pirsch.js +2 -67
  80. package/dist/vendors/analytics/plausible-analytics.js +2 -81
  81. package/dist/vendors/analytics/posthog.js +2 -200
  82. package/dist/vendors/analytics/promptwatch.js +2 -29
  83. package/dist/vendors/analytics/rudderstack.js +2 -183
  84. package/dist/vendors/analytics/rybbit-analytics.js +2 -63
  85. package/dist/vendors/analytics/segment.js +2 -65
  86. package/dist/vendors/analytics/umami-analytics.js +2 -39
  87. package/dist/vendors/analytics/vercel-analytics.js +2 -53
  88. package/dist/vendors/email-and-sms/klaviyo.js +2 -0
  89. package/dist/vendors/functional/crisp.js +2 -100
  90. package/dist/vendors/functional/front-chat.js +2 -64
  91. package/dist/vendors/functional/intercom.js +2 -45
  92. package/dist/vendors/tag-managers/cloudflare-zaraz.js +2 -98
  93. package/dist/vendors/tag-managers/google-tag-manager.js +2 -73
  94. package/dist/vercel-analytics.js +2 -0
  95. package/dist/x-pixel.js +2 -0
  96. package/dist-types/adobe-analytics.d.ts +2 -0
  97. package/dist-types/ahrefs-analytics.d.ts +2 -0
  98. package/dist-types/amplitude.d.ts +2 -0
  99. package/dist-types/clearbit.d.ts +2 -0
  100. package/dist-types/cloudflare-web-analytics.d.ts +2 -0
  101. package/dist-types/cloudflare-zaraz.d.ts +2 -0
  102. package/dist-types/crisp.d.ts +2 -0
  103. package/dist-types/databuddy.d.ts +2 -0
  104. package/dist-types/e2e-test-utils.d.ts +2 -0
  105. package/dist-types/engine/compile.d.ts +2 -3
  106. package/dist-types/engine/runtime.d.ts +2 -3
  107. package/dist-types/events.d.ts +2 -46
  108. package/dist-types/fathom-analytics.d.ts +2 -0
  109. package/dist-types/front-chat.d.ts +2 -0
  110. package/dist-types/google-tag-manager.d.ts +2 -0
  111. package/dist-types/google-tag.d.ts +2 -0
  112. package/dist-types/heap.d.ts +2 -0
  113. package/dist-types/hightouch.d.ts +2 -0
  114. package/dist-types/hotjar.d.ts +2 -0
  115. package/dist-types/intercom.d.ts +2 -0
  116. package/dist-types/klaviyo.d.ts +2 -0
  117. package/dist-types/linkedin-insights.d.ts +2 -0
  118. package/dist-types/logrocket.d.ts +2 -0
  119. package/dist-types/matomo-analytics.d.ts +2 -0
  120. package/dist-types/meta-pixel.d.ts +2 -0
  121. package/dist-types/microsoft-clarity.d.ts +2 -0
  122. package/dist-types/microsoft-uet.d.ts +2 -0
  123. package/dist-types/mixpanel-analytics.d.ts +2 -0
  124. package/dist-types/one-dollar-stats.d.ts +2 -0
  125. package/dist-types/openai-pixel.d.ts +2 -0
  126. package/dist-types/pinterest-tag.d.ts +2 -0
  127. package/dist-types/pirsch.d.ts +2 -0
  128. package/dist-types/plausible-analytics.d.ts +2 -0
  129. package/dist-types/posthog.d.ts +2 -0
  130. package/dist-types/promptwatch.d.ts +2 -0
  131. package/dist-types/reddit-pixel.d.ts +2 -0
  132. package/dist-types/registry.d.ts +2 -485
  133. package/dist-types/resolve.d.ts +2 -9
  134. package/dist-types/rudderstack.d.ts +2 -0
  135. package/dist-types/rybbit-analytics.d.ts +2 -0
  136. package/dist-types/segment.d.ts +2 -0
  137. package/dist-types/snapchat-pixel.d.ts +2 -0
  138. package/dist-types/tiktok-pixel.d.ts +2 -0
  139. package/dist-types/types.d.ts +2 -314
  140. package/dist-types/umami-analytics.d.ts +2 -0
  141. package/dist-types/vendors/_shared/attributes.d.ts +2 -35
  142. package/dist-types/vendors/_shared/google-consent.d.ts +2 -47
  143. package/dist-types/vendors/_shared/install-builders.d.ts +2 -30
  144. package/dist-types/vendors/_shared/required-id.d.ts +2 -0
  145. package/dist-types/vendors/_shared/script-url.d.ts +2 -75
  146. package/dist-types/vendors/ads-and-pixels/linkedin-insights.d.ts +2 -92
  147. package/dist-types/vendors/ads-and-pixels/meta-pixel.d.ts +2 -289
  148. package/dist-types/vendors/ads-and-pixels/microsoft-uet.d.ts +2 -105
  149. package/dist-types/vendors/ads-and-pixels/openai-pixel.d.ts +2 -211
  150. package/dist-types/vendors/ads-and-pixels/pinterest-tag.d.ts +2 -295
  151. package/dist-types/vendors/ads-and-pixels/reddit-pixel.d.ts +2 -210
  152. package/dist-types/vendors/ads-and-pixels/snapchat-pixel.d.ts +2 -171
  153. package/dist-types/vendors/ads-and-pixels/tiktok-pixel.d.ts +2 -106
  154. package/dist-types/vendors/ads-and-pixels/x-pixel.d.ts +2 -183
  155. package/dist-types/vendors/analytics/adobe-analytics.d.ts +2 -75
  156. package/dist-types/vendors/analytics/ahrefs-analytics.d.ts +2 -62
  157. package/dist-types/vendors/analytics/amplitude.d.ts +2 -234
  158. package/dist-types/vendors/analytics/clearbit.d.ts +2 -60
  159. package/dist-types/vendors/analytics/cloudflare-web-analytics.d.ts +2 -67
  160. package/dist-types/vendors/analytics/databuddy.d.ts +2 -147
  161. package/dist-types/vendors/analytics/fathom-analytics.d.ts +2 -90
  162. package/dist-types/vendors/analytics/google-tag.d.ts +2 -95
  163. package/dist-types/vendors/analytics/heap.d.ts +2 -316
  164. package/dist-types/vendors/analytics/hightouch.d.ts +2 -285
  165. package/dist-types/vendors/analytics/hotjar.d.ts +2 -73
  166. package/dist-types/vendors/analytics/logrocket.d.ts +2 -101
  167. package/dist-types/vendors/analytics/matomo-analytics.d.ts +2 -41
  168. package/dist-types/vendors/analytics/microsoft-clarity.d.ts +2 -97
  169. package/dist-types/vendors/analytics/mixpanel-analytics.d.ts +2 -113
  170. package/dist-types/vendors/analytics/one-dollar-stats.d.ts +2 -39
  171. package/dist-types/vendors/analytics/pirsch.d.ts +2 -96
  172. package/dist-types/vendors/analytics/plausible-analytics.d.ts +2 -122
  173. package/dist-types/vendors/analytics/posthog.d.ts +2 -175
  174. package/dist-types/vendors/analytics/promptwatch.d.ts +2 -36
  175. package/dist-types/vendors/analytics/rudderstack.d.ts +2 -330
  176. package/dist-types/vendors/analytics/rybbit-analytics.d.ts +2 -82
  177. package/dist-types/vendors/analytics/segment.d.ts +2 -164
  178. package/dist-types/vendors/analytics/umami-analytics.d.ts +2 -93
  179. package/dist-types/vendors/analytics/vercel-analytics.d.ts +2 -66
  180. package/dist-types/vendors/email-and-sms/klaviyo.d.ts +2 -0
  181. package/dist-types/vendors/functional/crisp.d.ts +2 -78
  182. package/dist-types/vendors/functional/front-chat.d.ts +2 -62
  183. package/dist-types/vendors/functional/intercom.d.ts +2 -135
  184. package/dist-types/vendors/tag-managers/cloudflare-zaraz.d.ts +2 -39
  185. package/dist-types/vendors/tag-managers/google-tag-manager.d.ts +2 -96
  186. package/dist-types/vercel-analytics.d.ts +2 -0
  187. package/dist-types/x-pixel.d.ts +2 -0
  188. package/docs/README.md +129 -63
  189. package/docs/assets/v3/bottom-bar.png +0 -0
  190. package/docs/assets/v3/brand-card.png +0 -0
  191. package/docs/assets/v3/brand-preferences.png +0 -0
  192. package/docs/assets/v3/choice-wall.png +0 -0
  193. package/docs/assets/v3/headless-bar-html.png +0 -0
  194. package/docs/assets/v3/headless-bar-mobile.png +0 -0
  195. package/docs/assets/v3/headless-bar.png +0 -0
  196. package/docs/assets/v3/slim-bar.png +0 -0
  197. package/docs/concepts/choose-your-setup.md +87 -0
  198. package/docs/concepts/consent-categories.md +84 -0
  199. package/docs/{guides → concepts}/consent-state.md +71 -101
  200. package/docs/{guides → concepts}/data-fetching.md +31 -27
  201. package/docs/concepts/how-consent-works.md +123 -0
  202. package/docs/concepts/policies.md +71 -0
  203. package/docs/customization/class-names.md +202 -0
  204. package/docs/customization/dark-mode.md +157 -0
  205. package/docs/customization/motion.md +119 -0
  206. package/docs/customization/overview.md +67 -34
  207. package/docs/customization/recipes.md +839 -49
  208. package/docs/customization/slots.md +216 -35
  209. package/docs/customization/stylesheets.md +147 -0
  210. package/docs/customization/tailwind.md +842 -0
  211. package/docs/customization/tokens.md +163 -96
  212. package/docs/customization/translations.md +61 -3
  213. package/docs/frameworks/astro/embeds.md +160 -0
  214. package/docs/frameworks/astro/network-blocker.md +86 -0
  215. package/docs/frameworks/astro/scripts.md +146 -0
  216. package/docs/frameworks/html/embeds.md +142 -0
  217. package/docs/frameworks/html/network-blocker.md +105 -0
  218. package/docs/frameworks/html/scripts.md +164 -0
  219. package/docs/frameworks/javascript/scripts.md +119 -0
  220. package/docs/frameworks/next/embeds.md +90 -0
  221. package/docs/frameworks/next/network-blocker.md +153 -0
  222. package/docs/frameworks/next/scripts.md +196 -0
  223. package/docs/frameworks/nuxt/embeds.md +81 -0
  224. package/docs/frameworks/nuxt/network-blocker.md +97 -0
  225. package/docs/frameworks/nuxt/scripts.md +89 -0
  226. package/docs/frameworks/react/embeds.md +89 -0
  227. package/docs/frameworks/react/network-blocker.md +140 -0
  228. package/docs/frameworks/react/scripts.md +115 -0
  229. package/docs/frameworks/svelte/embeds.md +96 -0
  230. package/docs/frameworks/svelte/network-blocker.md +141 -0
  231. package/docs/frameworks/svelte/scripts.md +137 -0
  232. package/docs/frameworks/sveltekit/embeds.md +103 -0
  233. package/docs/frameworks/sveltekit/network-blocker.md +149 -0
  234. package/docs/frameworks/sveltekit/scripts.md +141 -0
  235. package/docs/frameworks/tanstack-start/embeds.md +96 -0
  236. package/docs/frameworks/tanstack-start/network-blocker.md +145 -0
  237. package/docs/frameworks/tanstack-start/scripts.md +103 -0
  238. package/docs/frameworks/vue/embeds.md +84 -0
  239. package/docs/frameworks/vue/network-blocker.md +99 -0
  240. package/docs/frameworks/vue/scripts.md +93 -0
  241. package/docs/guides/banner-experiments.md +654 -0
  242. package/docs/guides/troubleshooting.md +120 -47
  243. package/docs/guides/verify-consent.md +81 -49
  244. package/docs/integrations/adobe-analytics.md +167 -159
  245. package/docs/integrations/ahrefs-analytics.md +152 -154
  246. package/docs/integrations/amplitude.md +162 -156
  247. package/docs/integrations/building-integrations.md +136 -37
  248. package/docs/integrations/clearbit.md +154 -154
  249. package/docs/integrations/cloudflare-web-analytics.md +156 -156
  250. package/docs/integrations/cloudflare-zaraz.md +209 -261
  251. package/docs/integrations/crisp.md +164 -158
  252. package/docs/integrations/databuddy.md +157 -173
  253. package/docs/integrations/fathom-analytics.md +158 -156
  254. package/docs/integrations/front-chat.md +167 -167
  255. package/docs/integrations/google-maps.md +118 -83
  256. package/docs/integrations/google-tag-manager.md +178 -163
  257. package/docs/integrations/google-tag.md +163 -160
  258. package/docs/integrations/heap.md +163 -155
  259. package/docs/integrations/hightouch.md +161 -157
  260. package/docs/integrations/hotjar.md +159 -155
  261. package/docs/integrations/intercom.md +183 -153
  262. package/docs/integrations/klaviyo.md +486 -0
  263. package/docs/integrations/linkedin-insights.md +174 -150
  264. package/docs/integrations/logrocket.md +160 -156
  265. package/docs/integrations/matomo-analytics.md +188 -178
  266. package/docs/integrations/meta-pixel.md +188 -150
  267. package/docs/integrations/microsoft-clarity.md +163 -155
  268. package/docs/integrations/microsoft-uet.md +148 -154
  269. package/docs/integrations/mixpanel-analytics.md +155 -160
  270. package/docs/integrations/one-dollar-stats.md +166 -165
  271. package/docs/integrations/openai-pixel.md +204 -301
  272. package/docs/integrations/overview.md +137 -80
  273. package/docs/integrations/pinterest-tag.md +191 -183
  274. package/docs/integrations/pirsch.md +169 -159
  275. package/docs/integrations/plausible-analytics.md +172 -158
  276. package/docs/integrations/posthog.md +226 -241
  277. package/docs/integrations/promptwatch.md +154 -154
  278. package/docs/integrations/reddit-pixel.md +185 -157
  279. package/docs/integrations/rudderstack.md +201 -186
  280. package/docs/integrations/rybbit-analytics.md +171 -160
  281. package/docs/integrations/segment.md +182 -154
  282. package/docs/integrations/snapchat-pixel.md +186 -156
  283. package/docs/integrations/tiktok-pixel.md +171 -150
  284. package/docs/integrations/umami-analytics.md +163 -158
  285. package/docs/integrations/vercel-analytics.md +166 -157
  286. package/docs/integrations/x-pixel.md +176 -150
  287. package/docs/integrations/youtube.md +121 -86
  288. package/docs/upgrade-v3.md +475 -467
  289. package/package.json +10 -257
  290. package/dist-types/__tests__/helpers.d.ts +0 -141
  291. package/docs/assets/v3/brand-bar.png +0 -0
  292. package/docs/assets/v3/mobile-card.png +0 -0
  293. package/docs/assets/v3/preferences.png +0 -0
  294. package/docs/frameworks/javascript/script-loader.md +0 -100
  295. package/docs/frameworks/next/script-loader.md +0 -216
  296. package/docs/frameworks/react/script-loader.md +0 -69
  297. package/docs/guides/deployment-modes.md +0 -75
  298. package/docs/guides/shared-consent-controls.md +0 -158
  299. package/docs/integrations/clear-on-revocation.md +0 -167
  300. package/docs/integrations/granular-consent.md +0 -210
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Scripts
3
+ description: Load vendor scripts by consent category in a Nuxt app with the c15t
4
+ Nuxt module, and what happens when a visitor withdraws consent.
5
+ group: frameworks
6
+ ---
7
+
8
+ ## Register vendor scripts
9
+
10
+ A banner does not stop a script you load with a `<script>` tag, `useHead` or a
11
+ vendor's Nuxt module. Remove those loaders and register the vendor with c15t
12
+ instead, so c15t loads it only while its category is allowed.
13
+
14
+ Create the script configuration with the helpers from `@c15t/integrations`:
15
+
16
+ ```ts title="app/consent-scripts.ts"
17
+ import { posthog } from '@c15t/integrations/posthog';
18
+ import { xPixel } from '@c15t/integrations/x-pixel';
19
+
20
+ export const scripts = [
21
+ posthog({
22
+ id: 'phc_your_project_key',
23
+ initOptions: { cookieless_mode: 'never' },
24
+ loadMode: 'after-consent',
25
+ region: 'eu',
26
+ }),
27
+ xPixel({ pixelId: 'your-pixel-id' }),
28
+ ];
29
+ ```
30
+
31
+ Register it under the `c15t` key in `app/app.config.ts`:
32
+
33
+ ```ts title="app/app.config.ts"
34
+ import { scripts } from './consent-scripts';
35
+
36
+ export default defineAppConfig({ c15t: { scripts } });
37
+ ```
38
+
39
+ The module starts one script loader in the browser after hydration, once it
40
+ has applied the visitor's stored choice and privacy signals. Do not also call
41
+ `createScriptLoader` yourself, or each script loads twice.
42
+
43
+ `scripts` must go in `app.config.ts`. Module options in `nuxt.config.ts`
44
+ reach the browser as JSON, which drops the functions inside each script.
45
+
46
+ Every vendor guide under [integrations](../../integrations/overview.md) gives the
47
+ helper and options for that vendor.
48
+
49
+ ## Embeds and other requests
50
+
51
+ Scripts cover vendor code c15t loads for you. For the rest:
52
+
53
+ * [Embeds](./embeds.md) gates iframes with `ConsentGate` or
54
+ the iframe blocker.
55
+ * [Network blocker](./network-blocker.md) holds `fetch` and
56
+ XHR calls that match a rule until their category is allowed.
57
+
58
+ ## When a visitor withdraws consent
59
+
60
+ Removing a script tag cannot stop code that already ran. When a save turns off
61
+ a category or vendor that was allowed, c15t reloads the page so the new
62
+ document starts with only permitted code. Set `reloadOnConsentRevoked: false`
63
+ to handle revocation yourself, or use the
64
+ [`onBeforeConsentRevocationReload` callback](https://c15t.com/docs/frameworks/nuxt/callbacks#before-a-revocation-reload)
65
+ to run code before the reload.
66
+
67
+ `clearOnRevocation` deletes first-party cookies and storage keys that belong to
68
+ a category when it is withdrawn. It is plain data, so it can go in
69
+ `nuxt.config.ts`. See [clear on revocation](https://c15t.com/docs/frameworks/nuxt/clear-on-revocation).
70
+
71
+ ## Let visitors turn off one vendor
72
+
73
+ A visitor can allow marketing and still switch off one vendor in it. Declare
74
+ the vendors in the `vendors` option; helpers from `@c15t/integrations` already carry
75
+ their vendor slug. See [vendor consent](https://c15t.com/docs/frameworks/nuxt/vendor-consent).
76
+
77
+ ## Content Security Policy
78
+
79
+ Allow each vendor's script host in your `script-src` directive. The module's
80
+ `nonce` option is fixed when the app builds, so prefer the host allowlist.
81
+ See [Content Security Policy](https://c15t.com/docs/frameworks/nuxt/content-security-policy).
82
+
83
+ ## Verify gating
84
+
85
+ In a private window, open the Network tab and load a page under a policy that
86
+ asks for consent. Requests to your vendors are absent.
87
+ Allow one category and save, and only that category's vendors load. Withdraw
88
+ it, and the page reloads without loading the vendor again.
89
+ [Verify consent](../../guides/verify-consent.md) has the full checklist.
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Embeds
3
+ description: Keep YouTube videos, maps and other iframes out of a React page
4
+ until their consent category is allowed, with ConsentGate or the iframe
5
+ blocker in ConsentProvider.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Pick a method
10
+
11
+ | Method | Use it when |
12
+ | ------------------ | -------------------------------------------------------------------------------------------------------- |
13
+ | `ConsentGate` | You render the iframe in a React component and want a placeholder until the visitor allows its category. |
14
+ | The iframe blocker | The iframe comes from markup you do not render with React, such as CMS or Markdown content. |
15
+
16
+ Both keep the iframe's `src` out of the page until the category is allowed, so
17
+ the embed's host receives no request before consent.
18
+
19
+ ## Gate an embed with ConsentGate
20
+
21
+ Wrap the iframe in `ConsentGate` anywhere inside `ConsentProvider`:
22
+
23
+ ```tsx title="src/video-embed.tsx"
24
+ import { ConsentDialogLink, ConsentGate } from 'c15t/react';
25
+
26
+ export const VideoEmbed = () => (
27
+ <ConsentGate
28
+ category="measurement"
29
+ placeholder={
30
+ <div className="placeholder">
31
+ <p>Allow measurement to load this YouTube video.</p>
32
+ <ConsentDialogLink>Choose video permissions</ConsentDialogLink>
33
+ </div>
34
+ }
35
+ >
36
+ <iframe
37
+ title="YouTube video"
38
+ sandbox="allow-scripts allow-same-origin allow-presentation"
39
+ src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
40
+ allow="encrypted-media; picture-in-picture"
41
+ allowFullScreen
42
+ />
43
+ </ConsentGate>
44
+ );
45
+ ```
46
+
47
+ The iframe is absent from the page until the visitor allows measurement. The
48
+ `placeholder` shows a message and a
49
+ [`ConsentDialogLink`](https://c15t.com/docs/frameworks/react/components/consent-dialog-link)
50
+ until then. When the visitor withdraws measurement, React removes the iframe.
51
+
52
+ Pick the category that matches what the embed does. The example uses
53
+ measurement for YouTube because its player measures views. A map or chat
54
+ widget usually belongs under functionality or experience.
55
+ [ConsentGate](https://c15t.com/docs/frameworks/react/components/consent-gate) documents the
56
+ built-in placeholder and every prop.
57
+
58
+ ## Gate an iframe with the iframe blocker
59
+
60
+ `ConsentProvider` runs the iframe blocker by default. Give an iframe
61
+ `data-src` instead of `src`, and a `data-category`:
62
+
63
+ ```html title="Markup from your CMS"
64
+ <iframe
65
+ data-src="https://www.google.com/maps/embed?pb=YOUR_EMBED_ID"
66
+ data-category="functionality"
67
+ title="Store map"
68
+ ></iframe>
69
+ ```
70
+
71
+ c15t sets `src` from `data-src` when the category is allowed and removes `src`
72
+ when the category is withdrawn. It watches the page, so iframes added later
73
+ are handled too. Add `data-vendor` with a vendor ID to also block the iframe
74
+ while the visitor has turned that vendor off.
75
+
76
+ To turn the blocker off, set `iframeBlocker: false` in the provider `options`.
77
+
78
+ ## Verify the embeds
79
+
80
+ Open the production build in a private window with DevTools open, under a
81
+ policy that asks for consent.
82
+
83
+ 1. No iframe has a `src` pointing at `youtube-nocookie.com` or
84
+ `google.com/maps`, and the Network panel shows no request to them.
85
+ 2. Allow the embed's category and save. The iframe loads.
86
+ 3. Withdraw the category and save. The page reloads without the embed.
87
+
88
+ The [YouTube](../../integrations/youtube.md) and
89
+ [Google Maps](../../integrations/google-maps.md) guides cover sizing and titles.
@@ -0,0 +1,140 @@
1
+ ---
2
+ title: Network blocker
3
+ description: Hold fetch and XMLHttpRequest calls in a React app until their
4
+ consent category is allowed, with networkBlocker rules in the ConsentProvider
5
+ options.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Add rules
10
+
11
+ Some SDKs are already on the page and send beacons or API calls of their own.
12
+ Add `networkBlocker` rules to the provider options to stop `fetch` and
13
+ `XMLHttpRequest` calls to those domains until the category is allowed. Load
14
+ vendor SDKs through [`scripts`](./scripts.md) first, so they
15
+ do not run at all before consent.
16
+
17
+ This partial example adds the option next to the existing `mode` and
18
+ `scripts`:
19
+
20
+ ```tsx title="src/consent.tsx"
21
+ const networkBlocker = {
22
+ rules: [
23
+ {
24
+ id: 'google-analytics',
25
+ domain: 'google-analytics.com',
26
+ category: 'measurement',
27
+ },
28
+ {
29
+ id: 'meta-pixel',
30
+ domain: 'facebook.com',
31
+ pathIncludes: '/tr',
32
+ category: 'marketing',
33
+ },
34
+ ],
35
+ };
36
+
37
+ <ConsentProvider options={{ mode, scripts, networkBlocker }}>
38
+ ```
39
+
40
+ ## Match requests with rules
41
+
42
+ Each rule names a `domain` and the consent `category` a request needs. The
43
+ domain also matches its subdomains: `google-analytics.com` covers
44
+ `www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
45
+ contain a substring, and `methods` narrows it to HTTP methods. A request is
46
+ blocked when a matching rule's condition is not met by the visitor's
47
+ effective permissions.
48
+
49
+ `category` takes the same conditions as scripts:
50
+
51
+ ```ts
52
+ { category: 'measurement' }
53
+ { category: { and: ['measurement', 'marketing'] } }
54
+ { category: { or: ['measurement', 'marketing'] } }
55
+ ```
56
+
57
+ Add `vendor` to also block the request while the visitor has turned that
58
+ vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
59
+ instead.
60
+
61
+ | Option | Default | Purpose |
62
+ | -------------------- | -------- | ------------------------------------------------------------ |
63
+ | `rules` | required | Rules described above |
64
+ | `enabled` | `true` | Set `false` to keep the rules but stop blocking |
65
+ | `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
66
+ | `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
67
+
68
+ ## What a blocked request looks like
69
+
70
+ The blocker wraps `window.fetch` and `XMLHttpRequest`. A blocked `fetch`
71
+ resolves to a `451` response with the status text
72
+ `Request blocked by consent`, and nothing is sent. A blocked XHR is aborted
73
+ and fires an `error` event. Requests that match no rule are not delayed.
74
+
75
+ ## When blocking starts
76
+
77
+ The provider holds matching requests from its first render in the browser,
78
+ before any of its children render or run effects. That covers requests from
79
+ child components, including their mount effects, and from effects in
80
+ components rendered next to the provider. The blocker module itself loads
81
+ after mount and decides each held request. Apps without `networkBlocker` do
82
+ not download it.
83
+
84
+ The standalone `useNetworkBlocker` hook works the same way from the first
85
+ render of the component that calls it. That render patches `fetch` and
86
+ `XMLHttpRequest`. If React throws the render away and never commits it, the
87
+ hold ends after 10 seconds. Nothing checked consent for the requests it held,
88
+ so they fail the way the blocker fails a blocked request: a 451 response for
89
+ `fetch`, a failed XHR. The same happens when the component unmounts before
90
+ the blocker loads.
91
+
92
+ While consent is unknown, a matching request that would be blocked waits
93
+ instead of failing. Consent is unknown until the policy has loaded, which is
94
+ also when a returning visitor's stored choice takes effect. The request is
95
+ then sent if the choice allows it and blocked otherwise. If the policy fails
96
+ to load, optional categories stay denied and waiting requests are blocked.
97
+ If the policy request never finishes, they keep waiting and are never sent.
98
+
99
+ A synchronous XHR cannot wait. Before the blocker module has loaded, a
100
+ matching one throws a `NetworkError` from `send()`. After that, one that
101
+ consent does not allow yet is blocked.
102
+
103
+ ## What the network blocker cannot stop
104
+
105
+ The blocker only sees requests made after the provider starts rendering in
106
+ the browser, through `fetch` or `XMLHttpRequest`. It cannot stop:
107
+
108
+ * Code that runs before the provider renders: inline scripts in the HTML,
109
+ third-party tags in `<head>`, scripts loaded before hydration (such as
110
+ `next/script` with `beforeInteractive`), and client modules that evaluate
111
+ earlier. Webpack builds evaluate a route's client component modules when
112
+ its chunk loads, so their top-level code runs first. Turbopack evaluates a
113
+ client component module when its first element renders, which inside the
114
+ provider is after blocking starts.
115
+ * Code that saved its own reference to `fetch` or `XMLHttpRequest` before the
116
+ provider rendered.
117
+ * `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
118
+ `<img>`, `<script>` or `<iframe>` elements, web workers and service
119
+ workers.
120
+ * With the standalone `useNetworkBlocker` hook instead of the provider
121
+ option, requests sent before the component that calls it renders.
122
+ Blocking starts in that component's first render, not the provider's.
123
+
124
+ Keep tracking calls out of that window:
125
+
126
+ * Send them from an effect or an event handler, never at module top level.
127
+ * Load vendor SDKs through `scripts` instead of a `<script>` tag or
128
+ `next/script`, so they wait for consent before they run at all.
129
+ * Check `useConsent('measurement')` (or the category you need) before you
130
+ call a vendor from your own code, and treat the blocker as a backstop.
131
+
132
+ ## Verify the blocked requests
133
+
134
+ Open DevTools Network in a private window, under a policy that asks for
135
+ consent.
136
+
137
+ 1. Before a choice, requests matching a rule are absent, and the console logs
138
+ each blocked request.
139
+ 2. Allow the rule's category and save. Matching requests go out.
140
+ 3. Reject, reload, and check that they stay absent.
@@ -0,0 +1,115 @@
1
+ ---
2
+ title: Scripts
3
+ description: Load vendor scripts by consent category in a React app with
4
+ ConsentProvider, and clear stored data or reload the page when a visitor
5
+ withdraws consent.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Register vendor scripts
10
+
11
+ Pass vendor loaders to `ConsentProvider` through `options.scripts`. The
12
+ provider loads each script when its category is allowed and removes it when
13
+ the category is denied. The [quickstart](https://c15t.com/docs/frameworks/react/quickstart)
14
+ registers PostHog and X Pixel this way:
15
+
16
+ ```ts title="src/scripts.ts"
17
+ import { posthog } from '@c15t/integrations/posthog';
18
+ import { xPixel } from '@c15t/integrations/x-pixel';
19
+
20
+ export const scripts = [
21
+ posthog({
22
+ id: 'phc_your_project_key',
23
+ initOptions: { cookieless_mode: 'never' },
24
+ loadMode: 'after-consent',
25
+ }),
26
+ xPixel({ pixelId: 'your-pixel-id' }),
27
+ ];
28
+ ```
29
+
30
+ The provider in `src/consent.tsx` receives them:
31
+
32
+ ```tsx title="src/consent.tsx"
33
+ import {
34
+ ConsentBanner,
35
+ ConsentDialog,
36
+ ConsentDialogLink,
37
+ ConsentProvider,
38
+ hosted,
39
+ } from 'c15t/react';
40
+ import type { ReactNode } from 'react';
41
+
42
+ import { scripts } from './scripts';
43
+
44
+ import 'c15t/react/styles.css';
45
+
46
+ const mode = hosted({ url: 'https://your-project.inth.app' });
47
+
48
+ export const Consent = ({ children }: { children: ReactNode }) => (
49
+ <ConsentProvider options={{ mode, scripts }}>
50
+ {children}
51
+ <ConsentBanner />
52
+ <ConsentDialog />
53
+ <footer>
54
+ <ConsentDialogLink>Privacy settings</ConsentDialogLink>
55
+ </footer>
56
+ </ConsentProvider>
57
+ );
58
+ ```
59
+
60
+ Each helper from `@c15t/integrations` sets its own category and a stable `id`.
61
+ Remove every other loader for the same vendor, such as a `<script>` tag in
62
+ `index.html` or an SDK you initialize at module level, so the vendor loads
63
+ once and only through c15t. [Integrations](../../integrations/overview.md) lists
64
+ every helper, and [building integrations](../../integrations/building-integrations.md)
65
+ covers a vendor without one.
66
+
67
+ ## What happens when consent changes
68
+
69
+ * **Allowed.** The script loads, or its SDK is started, the first time its
70
+ category is allowed.
71
+ * **Denied later.** The provider removes the script element. Code that already
72
+ ran keeps running, so after the visitor turns off a category they had
73
+ allowed, the provider reloads the page once the save finishes. Set
74
+ `reloadOnConsentRevoked: false` only if every gated vendor stops itself.
75
+ * **`alwaysLoad` helpers.** Some integrations, such as Google Consent Mode,
76
+ load before consent and pass the visitor's choice to the vendor. Read the
77
+ vendor's guide; a category on a script does not always mean zero requests.
78
+
79
+ ## Embeds and other requests
80
+
81
+ Scripts cover vendor code c15t loads for you. For the rest:
82
+
83
+ * [Embeds](./embeds.md) keeps iframes out of the page with
84
+ `ConsentGate` or the iframe blocker.
85
+ * [Network blocker](./network-blocker.md) holds `fetch` and
86
+ XHR calls that match a rule until their category is allowed.
87
+
88
+ ## Clear stored data after revocation
89
+
90
+ Removing a script does not delete the cookies or storage entries it wrote. Add
91
+ `clearOnRevocation` to the provider options to delete the entries you list for
92
+ each category when it is denied. The option is read once, when the provider
93
+ mounts. [Clear on revocation](https://c15t.com/docs/frameworks/react/clear-on-revocation) covers
94
+ configuration and browser limits.
95
+
96
+ ## Let visitors turn off one vendor
97
+
98
+ A visitor can allow marketing and still switch off one vendor in it. Pass
99
+ `vendors` to the provider options. Helpers from `@c15t/integrations` already carry
100
+ their vendor slug. See [vendor consent](https://c15t.com/docs/frameworks/react/vendor-consent).
101
+
102
+ To send your own events only to allowed integrations, see
103
+ [send events only to allowed integrations](../../integrations/overview.md#send-events-only-to-allowed-integrations).
104
+
105
+ ## Check the scripts
106
+
107
+ Open DevTools Network in a private window, filter by each vendor's domain and
108
+ reload.
109
+
110
+ 1. Before a choice under an opt-in policy, no vendor script loads.
111
+ 2. Allow one category. Only that category's vendors load.
112
+ 3. Turn the category off. The page reloads and the vendor stays absent.
113
+ 4. Reload again. The rejection holds.
114
+
115
+ [Verify consent](../../guides/verify-consent.md) has the full checklist.
@@ -0,0 +1,96 @@
1
+ ---
2
+ title: Embeds
3
+ description: Keep YouTube videos, maps and other iframes out of a Svelte page
4
+ until their consent category is allowed, with ConsentGate or the iframe
5
+ blocker.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## Pick a way to gate the embed
10
+
11
+ A YouTube video, a map or a social post in an iframe contacts its vendor as
12
+ soon as it loads. c15t offers two ways to keep it from loading before
13
+ consent:
14
+
15
+ | Your markup | Use | Placeholder |
16
+ | -------------------------------------------------------- | ------------------------------------------------------ | ----------------------------- |
17
+ | A Svelte component you write | `ConsentGate` around the iframe | Built in, or your own snippet |
18
+ | HTML you do not control, such as CMS or Markdown content | The iframe blocker with `data-category` and `data-src` | None; the iframe stays empty |
19
+
20
+ Both use the visitor's effective permission for one category, and both
21
+ remove the embed again when the visitor withdraws that category.
22
+
23
+ ## Wrap the iframe in ConsentGate
24
+
25
+ ```svelte title="src/YouTubeEmbed.svelte"
26
+ <script lang="ts">
27
+ import { ConsentDialogLink, ConsentGate } from '@c15t/svelte';
28
+ </script>
29
+
30
+ <!-- The iframe mounts only while measurement is allowed. -->
31
+ <ConsentGate category="measurement">
32
+ {#snippet placeholder()}<div class="placeholder">
33
+ <p>Allow measurement to load this YouTube video.</p>
34
+ <ConsentDialogLink>Choose video permissions</ConsentDialogLink>
35
+ </div>{/snippet}
36
+ <iframe
37
+ title="YouTube video"
38
+ src="https://www.youtube-nocookie.com/embed/czTksCF6X8Y?playsinline=1"
39
+ allow="encrypted-media; picture-in-picture"
40
+ allowfullscreen
41
+ ></iframe>
42
+ </ConsentGate>
43
+ ```
44
+
45
+ The iframe is absent from the DOM until measurement is allowed, so the
46
+ browser never requests it. The placeholder snippet replaces the built-in one
47
+ and includes a `ConsentDialogLink`, so a visitor can allow the category from
48
+ the embed's own spot. Keep `ConsentDialog` mounted for that link.
49
+ [ConsentGate](https://c15t.com/docs/frameworks/svelte/components/consent-gate) lists its props.
50
+
51
+ Pick the category the vendor's embed needs. YouTube and maps usually fit
52
+ `measurement` or `marketing`; check what each vendor sets.
53
+
54
+ ## Gate iframes you do not render
55
+
56
+ The provider's iframe blocker is on by default. It watches the page for
57
+ iframes with a `data-category` attribute:
58
+
59
+ ```html
60
+ <iframe
61
+ title="Store locations"
62
+ data-category="functionality"
63
+ data-src="https://www.google.com/maps/embed?pb=..."
64
+ ></iframe>
65
+ ```
66
+
67
+ * While the category is denied, the iframe has no `src`, so it loads nothing.
68
+ * When the category is allowed, the blocker copies `data-src` to `src`.
69
+ Only `http` and `https` URLs are used.
70
+ * When the category is withdrawn, the blocker removes `src` again.
71
+
72
+ Iframes without `data-category` or `data-vendor` are never touched. Put
73
+ `data-src` in the HTML instead of `src`; an iframe that arrives with `src`
74
+ already starts loading before the blocker sees it. Add `data-vendor` with a
75
+ vendor slug to also keep the iframe empty while the visitor has that vendor
76
+ turned off.
77
+
78
+ The blocker has no placeholder. Style the empty iframe, or place a note and a
79
+ `ConsentDialogLink` next to it.
80
+
81
+ Pass `iframeBlocker={false}` on the provider to turn the blocker off.
82
+
83
+ ## Vendor guides
84
+
85
+ The [YouTube](../../integrations/youtube.md) and
86
+ [Google Maps](../../integrations/google-maps.md) guides have ready embed
87
+ configurations. [Integrations](../../integrations/overview.md) lists the rest.
88
+
89
+ ## Verify the embeds
90
+
91
+ Clear site data and reload with DevTools open:
92
+
93
+ 1. The Network panel has no request to the embed's host, and the Elements
94
+ panel shows the placeholder or an iframe without `src`.
95
+ 2. Allow the category in preferences. The embed loads without a page reload.
96
+ 3. Withdraw the category. The embed disappears, or its iframe loses `src`.
@@ -0,0 +1,141 @@
1
+ ---
2
+ title: Network blocker
3
+ description: Hold fetch and XMLHttpRequest calls to tracking domains in a Svelte
4
+ app until their consent category is allowed, with the provider's
5
+ networkBlocker option.
6
+ group: frameworks
7
+ ---
8
+
9
+ ## When to use the network blocker
10
+
11
+ The network blocker stops `fetch` and `XMLHttpRequest` calls to domains you
12
+ list until their consent category is allowed. Use it as a backstop for
13
+ tracking calls from code that is already on the page, such as your own
14
+ analytics wrapper or an SDK you import. It is off until you configure it.
15
+
16
+ Load vendor SDKs through the provider's `scripts` prop first; see
17
+ [scripts](./scripts.md). A script that never loads sends nothing, which is
18
+ stronger than blocking its requests one by one.
19
+
20
+ ## Configure the rules
21
+
22
+ Write the configuration in its own module:
23
+
24
+ ```ts title="src/lib/network-blocker.ts"
25
+ import type { UseNetworkBlockerOptions } from '@c15t/svelte';
26
+
27
+ // Pass as `networkBlocker={networkBlocker}` on ConsentManagerProvider.
28
+ export const networkBlocker: UseNetworkBlockerOptions = {
29
+ onRequestBlocked: ({ method, url }) => {
30
+ console.info('Blocked until consent', method, url);
31
+ },
32
+ rules: [
33
+ {
34
+ category: 'measurement',
35
+ domain: 'google-analytics.com',
36
+ id: 'google-analytics',
37
+ },
38
+ {
39
+ category: 'marketing',
40
+ domain: 'connect.facebook.net',
41
+ id: 'meta-pixel',
42
+ pathIncludes: '/signals',
43
+ },
44
+ ],
45
+ };
46
+ ```
47
+
48
+ Pass it to the provider:
49
+
50
+ ```svelte
51
+ <ConsentManagerProvider {mode} {scripts} {networkBlocker}>
52
+ ```
53
+
54
+ The provider reads `networkBlocker` once, when it is created. Remount the
55
+ provider to change the rules.
56
+
57
+ ## Match requests with rules
58
+
59
+ Each rule names a `domain` and the consent `category` a request needs. The
60
+ domain also matches its subdomains: `google-analytics.com` covers
61
+ `www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
62
+ contain a substring, and `methods` narrows it to HTTP methods. A request is
63
+ blocked when a matching rule's condition is not met by the visitor's
64
+ effective permissions.
65
+
66
+ `category` takes the same conditions as scripts:
67
+
68
+ ```ts
69
+ { category: 'measurement' }
70
+ { category: { and: ['measurement', 'marketing'] } }
71
+ { category: { or: ['measurement', 'marketing'] } }
72
+ ```
73
+
74
+ Add `vendor` to also block the request while the visitor has turned that
75
+ vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
76
+ instead.
77
+
78
+ | Option | Default | Purpose |
79
+ | -------------------- | -------- | ------------------------------------------------------------ |
80
+ | `rules` | required | Rules described above |
81
+ | `enabled` | `true` | Set `false` to keep the rules but stop blocking |
82
+ | `logBlockedRequests` | `true` | Log each blocked request with `console.warn` |
83
+ | `onRequestBlocked` | none | Called with `{ method, url, rule }` for each blocked request |
84
+
85
+ ## What a blocked request looks like
86
+
87
+ A blocked `fetch` resolves to a response with status `451` and the status
88
+ text `Request blocked by consent`, and nothing is sent. A blocked
89
+ `XMLHttpRequest` is aborted and fires an `error` event. Requests that match no
90
+ rule are not delayed. With `logBlockedRequests` on, the default, each blocked
91
+ request is logged with `console.warn`; `onRequestBlocked` receives
92
+ `{ method, url, rule }`.
93
+
94
+ ## When blocking starts
95
+
96
+ The provider starts holding matching requests when it is created in the
97
+ browser, before its children run their own code. The blocker module loads
98
+ when the provider mounts and then decides each held request:
99
+
100
+ * While the policy is still loading, a matching request waits instead of
101
+ failing. When the policy arrives, with the visitor's stored choice applied,
102
+ the request is sent if its category is allowed and blocked otherwise.
103
+ * If the policy fails to load, optional categories stay denied and the
104
+ waiting requests are blocked.
105
+ * A synchronous XHR cannot wait. Before the blocker module loads, a matching
106
+ one throws a `NetworkError` from `send()`.
107
+
108
+ When the visitor allows a category later, new requests to its domains go
109
+ through. Requests blocked earlier are not replayed.
110
+
111
+ ## What it cannot stop
112
+
113
+ The blocker sees only `fetch` and `XMLHttpRequest` calls made after the
114
+ provider is created in the browser. It cannot stop:
115
+
116
+ * Scripts that run before the provider, such as tags in `index.html` or
117
+ `app.html` and code at the top level of modules that load first.
118
+ * Code that kept its own reference to `fetch` or `XMLHttpRequest` from before
119
+ the provider was created.
120
+ * `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
121
+ `<img>`, `<script>` and `<iframe>` elements, web workers and service
122
+ workers.
123
+ * Requests your server makes, such as from a SvelteKit `load` or endpoint.
124
+
125
+ Keep tracking calls out of that window. Send them from event handlers or
126
+ effects, not at a module's top level, and check
127
+ `getConsentManager().has('measurement')` before you call a vendor from your
128
+ own code.
129
+
130
+ ## Verify the blocker
131
+
132
+ Open DevTools, clear site data for your origin and reload:
133
+
134
+ 1. Before you choose, requests matching a rule do not reach the network, and
135
+ the console logs each blocked request.
136
+ 2. Allow the rule's category. New matching requests appear in the Network
137
+ panel.
138
+ 3. Reject, reload and confirm they stay blocked.
139
+
140
+ `fetch('https://www.google-analytics.com/g/collect')` in the console returns a
141
+ response with status `451` while measurement is denied.