npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cookieyes/scripts

v0.2.4

Published

Ready-made, consent-gated third-party integrations for CookieYes (Segment, Google, Meta, and more)

Downloads

1,230

Readme

@cookieyes/scripts

Ready-made, consent-gated third-party integrations for CookieYes. Guides for every integration: developers.cookieyes.com.

Nothing loads until the visitor grants the matching consent category. When they withdraw it, the script is removed or silenced — no page reload, nothing the visitor was doing is lost.

import { initCookieYes } from "@cookieyes/react"; // or "@cookieyes/core"
import { segment } from "@cookieyes/scripts";

initCookieYes({
  mode: "cookie-only",
  integrations: [segment({ writeKey: "YOUR_WRITE_KEY" })],
});

Segment now loads only after the visitor grants analytics, and is fully removed if they later withdraw it.

Install

npm install @cookieyes/scripts

It works with either @cookieyes/core or @cookieyes/react — you pass the presets to the integrations option of the same initCookieYes(config) you already use.

What's in the box

| Export | What it's for | |---|---| | segment(config) | Segment (analytics.js), gated behind consent. | | metaPixel(config) | Meta Pixel (fbq), gated behind consent. | | ga4() / googleAds() / googleTagManager() | Google via Consent Mode (GA4, Ads, GTM). | | posthog(config) / posthogSync(config) | PostHog (posthog-js) — we load it, or sync consent with yours. | | clarity(config) | Microsoft Clarity (session recording + heatmaps), consent-gated. | | customScript(config) | Gate any third-party <script> that has no dedicated preset. | | createQueue() / flushQueue() | The queue/stub pattern most tracking scripts use, so early calls aren't lost. |

How an integration behaves

Every integration is described by two plain choices:

  • load — when it starts: "afterConsent" (only once its category is granted) or "immediately".
  • onRevoke — what happens when consent is withdrawn:

| onRevoke | On withdrawal | Use for | |---|---|---| | "remove" | Script removed, identifiers cleared. Re-loads on re-grant. | Trackers that must fully stop (Segment). | | "silence" | Script stays, told to go quiet; resumes on re-grant. | Vendors with a quiet/resume API (Meta fbq). | | "keep" | Nothing — the vendor manages its own consent signal. | Google Consent Mode. |

The presets set these for you. The correct mode is vendor knowledge — it depends on how each vendor behaves, not on your app — so segment() locks it in and you can't pick the wrong one. Only customScript lets you choose, because only you know how your own script behaves.

Custom categories

Every preset accepts a category to match your taxonomy. If you use a custom categories list (not the built-in five), pass the matching id — e.g. segment({ writeKey, category: "stats" }). A preset left on a default category that doesn't exist in your taxonomy would wait for consent that never comes and silently never load — so the SDK warns when it detects this.

Presets

segment(config)

segment({
  writeKey: "YOUR_WRITE_KEY", // required — public by design, safe in browser code
  category: "analytics",      // optional — the category that gates it (default "analytics")
  id: "segment",              // optional — only needed if you run more than one
});
  • onRevoke: "remove" — on withdrawal the script and Segment's own identifiers (ajs_anonymous_id, ajs_user_id) are removed. Withdrawal really means "stop and wipe."

  • Send events through window.analytics (analytics.track(...), analytics.identify(...)); the snippet queues calls until the library loads. But because remove deletes window.analytics on withdrawal, a later analytics.track(...) throws. Use safeCall to call from anywhere without guarding — it's a no-op when Segment is gone (i.e. no consent), so it never throws and never tracks without consent:

    import { safeCall } from "@cookieyes/scripts";
    safeCall("analytics", "track", "Signup", { plan: "pro" });
  • Expected: each re-grant starts a fresh Segment session, so Segment sends a new page view. That's normal Segment behaviour, not a double-count of your own tracked events.

  • Attach the consent choice to the profile (if you want it in Segment): do it yourself on a consent change — getCookieYes().manager.subscribe(...) then analytics.identify(id, { consent: {...} }). We don't do it for you, so you control the traits and timing.

  • Account setting: turn Segment's own consent/Consent-Management features off — this preset is the gate. Two consent layers fighting each other is the usual cause of "events don't show up."

  • Server-side boundary: this is a browser package. It stops client tracking and clears client ids. It cannot delete data already sent to Segment — that's Segment's server-side deletion/suppression API (a secret key, your backend), out of scope here.

metaPixel(config)

metaPixel({
  pixelId: "123456789",       // required — your Meta Pixel ID
  category: "advertisement",  // optional — the category that gates it (default "advertisement")
  id: "meta",                 // optional — only needed if you run more than one
  autoPageView: true,         // optional — send the first-page PageView (default true; false for SPAs)
  limitedDataUse: undefined,  // optional — omit to auto-enable for US opt-out (CCPA); true/false forces it
});
  • onRevoke: "silence" — on withdrawal we call Meta's own fbq('consent', 'revoke') rather than removing the script, and clear Meta's _fbp / _fbc cookies (Meta's own revoke leaves them). A re-grant calls fbq('consent', 'grant'), so tracking resumes without re-downloading fbevents.js.
  • Track events with fbq(...) as usual. Calls made after a revoke are held by Meta's consent mechanism and delivered if consent is re-granted — not dropped. To call before it has loaded, guard it: window.fbq?.("track", "Purchase").
  • autoPageView: false — turn off the automatic first-page PageView and send page views yourself (single-page apps).
  • limitedDataUse — Meta's US privacy flag. Omit it and it's enabled automatically for a US opt-out (CCPA) visitor via the detected region; pass true/false to force it. It's sent before init, as Meta requires.
  • Server-side boundary: browser-side event deduplication with the Conversions API (a shared event_id between the pixel and your server) needs a server sending matching events — out of scope for this browser package.

Google — ga4(), googleAds(), googleTagManager()

Google works through Consent Mode, which is different from a gated script. Every Google tag shares one dataLayer, and consent must deny by default before any tag runs and before initCookieYes — then the SDK broadcasts the visitor's real choice on top. That default belongs in the page <head>:

// Next.js — app/layout.tsx
import { GoogleConsentMode } from "@cookieyes/nextjs/server";
<body><GoogleConsentMode />{children}</body>

For a non-Next app, paste the snippet in <head>, or call the runtime form before initCookieYes:

import { googleConsentModeSnippet, bootstrapGoogleConsentMode } from "@cookieyes/scripts";

// (a) inline in <head>:  <script>${googleConsentModeSnippet()}</script>
// (b) or, before initCookieYes:
bootstrapGoogleConsentMode();

Why before init: the SDK broadcasts the consent update once at startup. If the dataLayer and its deny-default aren't set yet, a returning visitor who consented last time would be stuck denied until they act again. The head snippet is what prevents that. If you skip it, the preset falls back to a deny-default and warns.

Then load the tags on the client — they're load: "immediately", onRevoke: "keep" (Consent Mode governs the gating, and the SDK broadcasts every change):

import { ga4, googleAds, googleTagManager } from "@cookieyes/scripts";

initCookieYes({
  mode: "cookie-only",
  integrations: [
    ga4({ measurementId: "G-XXXXXXX" }),   // GA4
    googleAds({ conversionId: "AW-XXXXXXX" }), // Google Ads
    // or, if you manage tags through a container instead:
    // googleTagManager({ containerId: "GTM-XXXXXXX" }),
  ],
});
  • GA4 and Ads share one gtag.js — loaded once, configured per product, so running both doesn't load the library twice.
  • GTM loads the container; the tags inside it (GA4, Ads, …) are governed by the same Consent Mode signals — so use googleTagManager() instead of ga4()/googleAds() when those products live in your container.
  • Each takes an optional category and id override.
  • params on ga4()/googleAds() passes extra gtag('config', …) options — e.g. ga4({ measurementId, params: { send_page_view: false } }) for a single-page app, or { debug_mode: true } for DebugView.
  • consentMode: "basic" loads the tag only after consent and removes it on withdrawal — so no tag and no cookieless pings whenever consent isn't given. Default "advanced" loads immediately and lets Google send cookieless pings while denied. (Basic removes the shared gtag.js, so use it for a single Google product per page; for several, prefer "advanced".)
  • restrictedDataProcessing on googleAds() — Google's US/California flag, the Ads counterpart of Meta's LDU. Omit it and it's on automatically for a US opt-out (CCPA) visitor; true/false forces it.
  • urlPassthrough / adsDataRedaction on the Consent Mode options (googleConsentModeSnippet / bootstrapGoogleConsentMode) turn on Google's recommended ad-performance behaviour while ad_storage is denied. Off by default.
  • Don't run the same product twice: if you load GA4/Ads inside your GTM container, use googleTagManager() alone — not also ga4()/googleAds() for the same id. The SDK warns if it sees a container and a standalone tag together.
  • If a custom taxonomy maps more than one category to the same Google signal, set googleConsentMatch: "all" on initCookieYes to grant the signal only when all of them are granted (default is "any" — grant if any one is).
  • Server-side boundary: a GTM server container or the Google Ads API run on your backend — out of scope for this browser package.

posthog(config) / posthogSync(config)

PostHog makes you decide one thing up front, because it's a legal choice, not a technical one: what does rejecting consent mean?

  • onReject: "stop" — nothing loads until consent; on withdrawal PostHog and its data are removed. Reject means no tracking at all.
  • onReject: "anonymous" — PostHog keeps counting visits with no cookie (a privacy-preserving server-side hash), and upgrades to normal cookie-based tracking only on Accept. Reject means keep counting, without a cookie.

onReject is required — there is no default. (A plain-JS caller who omits it gets a console warning and falls back to the safe "stop".)

Which setup style?

  • posthog() — we load PostHog for you, from your project API key.
  • posthogSync() — you already load and init PostHog yourself; we only keep consent in sync (opt_in / opt_out) and inject nothing. Initialise PostHog before initCookieYes runs, and init it opted-out so nothing is captured before consent — pass opt_out_capturing_by_default: true (stop-style; we opt in on grant) or cookieless_mode: "on_reject" (cookie-free / anonymous) in your own init. Otherwise PostHog captures with a cookie the moment it loads.
// (a) we load it for you:
import { posthog } from "@cookieyes/scripts";
initCookieYes({
  mode: "cookie-only",
  integrations: [
    posthog({
      apiKey: "phc_XXXXXXXX",  // required — public by design, safe in browser code
      onReject: "stop",        // required — "stop" or "anonymous"
      region: "us",            // optional — "us" (default) or "eu"; sets api_host
      // apiHost: "https://ph.example.com", // optional — self-hosted; overrides region
      category: "analytics",   // optional — the category that gates it (default "analytics")
    }),
  ],
});

// (b) you already load PostHog yourself:
import { posthogSync } from "@cookieyes/scripts";
initCookieYes({ mode: "cookie-only", integrations: [posthogSync()] });
  • Anonymous loads on page load, before the banner is answered. To count cookie-free it must run right away — so a request carrying the visitor's IP reaches PostHog before any choice is made. No cookie is ever set until Accept. If that pre-consent request isn't acceptable for you, use "stop".
  • Account setting (anonymous only, required): turn on Cookieless in your PostHog project — Settings → Web analytics → "Enable cookieless tracking". It lives in your own PostHog account, not in this code (same as Segment's own consent setting), and anonymous mode does nothing without it.
  • region / apiHost — one value keeps everything aligned: region: "eu" sends data to the EU (eu.i.posthog.com); apiHost overrides it for a self-hosted or reverse-proxied PostHog.
  • Send events with posthog.capture(...) as usual. In "stop" mode window.posthog isn't there before the first grant, so guard it — safeCall("posthog", "capture", "Signup") is a no-op until it loads and never throws.
  • Check consent with our record, never PostHog's. PostHog's own "has this visitor decided?" check has a known bug that can report yes when nobody has — which has made real banners never show. Read consent from the SDK, not from posthog.has_opted_in_capturing().
  • Honest limits:
    • A consent change can make PostHog count one visit as two — it starts a new session when it switches cookie modes. This is a known PostHog behaviour, not a fault in the gating.
    • Cookie-free tracking is not full compliance on its own — you still need a lawful basis, clear wording, data-minimisation, IP handling, and a real deletion process.
  • Verify your reject choice — the one thing worth checking directly. Reject in the banner and watch the Network tab + document.cookie:
    • "stop" → no requests to *.posthog.com, no ph_ cookie.
    • "anonymous" → requests to *.posthog.com continue, but still no ph_ cookie (posthog.get_distinct_id() is "$posthog_cookieless").

clarity(config)

Microsoft Clarity (session recording + heatmaps), consent-gated.

clarity({
  projectId: "abcd123xyz",   // required — the id in clarity.ms/tag/<id>
  category: "analytics",     // optional — the category that gates it (default "analytics")
  id: "clarity",             // optional — only needed if you run more than one
});
  • onRevoke: "silence" — nothing loads until consent (load: "afterConsent"); on grant we call Clarity's Consent v2 API (cookies allowed), on withdrawal we call it with consent denied. Clarity then deletes its own _clck / _clsk cookies and keeps running cookie-free — the tag is not removed.

  • Required account setting: in your Clarity project, turn the Cookies toggle off — Settings → Setup → Advanced settings → Cookies. That's Clarity's Consent Mode: it makes Clarity wait for consent instead of setting cookies on load. Otherwise this gate does nothing. (Auto-on for EEA/UK/CH visitors.) It lives in your own Clarity account, not in this code — same as PostHog's cookieless toggle.

  • Honest limit: unlike Segment/Meta, Clarity keeps sending after a revoke — it goes cookie-free, not silent — so its network calls don't stop. That's Clarity's own behaviour, not a gating failure. If you need "load nothing at all," keeping afterConsent already means it never loads until consent is granted.

  • Session recording is higher-risk than ordinary analytics — a recording can capture what a visitor typed, their screen, names, order totals. Consider gating it behind its own category (e.g. clarity({ projectId, category: "session_recording" })) rather than sharing the general analytics permission.

  • Hide sensitive content — mostly your job. Clarity masks form fields and passwords by default, but not ordinary page text (names, totals, health/financial details). Mark those with Clarity's own attributes, especially on login, payment, and health pages:

    <div data-clarity-mask="True"> <!-- never recorded: name, card, diagnosis --> …</div>
    <div data-clarity-unmask="true"> …safe to record… </div>
  • GPC is handled by the SDK's consent flow, not here: under CCPA a Global Privacy Control signal opts the visitor out, so the gating category is denied and Clarity — being afterConsent — never loads. Under GDPR nothing loads until explicit consent anyway.

  • Send events with clarity(...); before the first grant it isn't on the page, so guard it: window.clarity?.("event", "signup").

  • Good to state in your privacy notice: Clarity keeps recordings ~30 days and aggregated heatmap data up to 13 months, and Microsoft has enforced consent for EU/UK/Swiss visitors since 31 October 2025. See Microsoft's Clarity consent docs for the details.

  • Verify: load the page (nothing should load pre-consent), Accept → _clck/_clsk appear, Reject → they're gone but Clarity keeps sending cookie-free.

customScript(config)

Gate any one-off third-party script behind consent.

customScript({
  id: "widget",                        // unique id (used for de-dup + the debug view)
  src: "https://example.com/w.js",     // script URL, loaded once consent is granted
  category: "functional",              // the category that gates it (or an array — see below)
  onRevoke: "remove",                  // "remove" (default) or "keep"
  attrs: { "data-id": "abc" },         // optional extra <script> attributes (e.g. nonce)
  stub: { global: "myTag", methods: ["track"] }, // optional queue stub (see below)
});
  • The script always loads after its category is granted. Loading before consent isn't offered here on purpose — a gated script that loads immediately would run with no consent. If you genuinely need immediate load, write a raw integration.
  • onRevoke: "remove" (default) takes the script off the page on withdrawal; "keep" leaves it. Only choose "keep" if the script manages its own consent.
  • category can be an array to require more than one — e.g. category: ["functional", "analytics"]. Combine with match: "all" (default, needs every one) or match: "any" (needs at least one).
  • attrs are set on the <script> before it's added — so nonce works for a strict CSP.

Queue helpers — createQueue / flushQueue

Most tracking scripts install a placeholder that queues calls made before the real library has loaded, then replays them once it arrives — so nothing is lost, and a queued call can never fail. These helpers give your own integration the same behaviour without hand-rolling it.

import { createQueue, flushQueue } from "@cookieyes/scripts";

// Create the global stub once (returns the existing one if already present):
const analytics = createQueue("analytics", ["track", "identify", "page"]);
analytics.track("Signup"); // queued now, delivered once the real script loads

// When your real library is ready, replay the queue and route later calls to it:
flushQueue("analytics", (method, ...args) => realAnalytics[method](...args));

customScript's stub option is a shortcut for createQueue.

Writing your own integration

A preset just returns an Integration object — the same shape the engine runs. For a vendor with no preset, and when customScript isn't enough (you need immediately load, or silence), write it by hand:

import type { Integration } from "@cookieyes/core";

const myVendor: Integration = {
  id: "my-vendor",
  category: "analytics",
  version: 1,
  load: "afterConsent",
  onRevoke: "remove",
  // setup runs when the category is granted; return a cleanup for "remove".
  setup: () => {
    const el = document.createElement("script");
    el.src = "https://example.com/v.js";
    document.head.appendChild(el);
    return () => el.remove(); // the cleanup — run on withdrawal
  },
};

initCookieYes({ mode: "cookie-only", integrations: [myVendor] });

setup receives a context with granted(), onConsentChange(fn) (auto-released on teardown), and the resolved region. The return type is enforced by onRevoke: remove → a cleanup function, silence → { silence, resume }, keep → nothing.

Choosing load and onRevoke safely

The safe defaults are load: "afterConsent" + onRevoke: "remove" — nothing runs before consent, and everything is torn down after. The other choices are for specific cases and are only safe when they match how the script behaves:

  • load: "immediately" — runs before consent. Safe only with onRevoke: "keep" and a script that stays silent until told otherwise (Google Consent Mode, which denies by default). immediately + remove means "runs with no consent, and is only removed after a granted-then-revoked" — fine under an opt-out regime (CCPA), a footgun under opt-in (GDPR).
  • onRevoke: "keep" — leaves the script running on withdrawal. Only safe if the script manages its own consent signal; otherwise it keeps tracking after the visitor said no.
  • onRevoke: "silence" — the script stays loaded but is told to go quiet. Only possible if the vendor has a real quiet/resume API (Meta's fbq).

The engine also flags common mistakes with a console warning: an unknown format version, a duplicate id, the same vendor in both integrations and builtInIntegrations, an old { vendor } entry, and a category that isn't in your taxonomy (or an empty one).

Sending events safely (the vendors compared)

Whether you must guard a vendor call depends on what the preset does on revoke:

| Vendor | Global | Calling it directly | |---|---|---| | Segment | window.analytics | Always guard — remove deletes it on revoke, so a bare .track() throws. Use safeCall("analytics", "track", …). | | Meta | window.fbq | Guard before it loads — window.fbq?.("track", …). After load it persists (silence keeps it), and calls while denied are held by Meta. | | Google | window.gtag | Safe — gtag pushes to the dataLayer, which persists, so a direct call never throws. | | PostHog | window.posthog | Guard before the first grant in "stop" mode (remove — absent until consent): safeCall("posthog", "capture", …). In "anonymous" mode it's present from load. | | Clarity | window.clarity | Guard before the first grant — afterConsent, so it's absent until consent: window.clarity?.("event", …). After that it persists (silence keeps it) and keeps sending cookie-free even while denied. |

Testing your integration

A quick checklist for a custom integration or preset:

  1. Before consent — nothing loads: no network request, no script tag, no cookie/identifier.
  2. On grant — the script loads; status goes loading → active.
  3. On revoke — remove takes the tag off and clears identifiers; silence goes quiet (script stays); keep does nothing. Status reflects it.
  4. On re-grant — remove re-loads once (no duplicate tag); silence resumes without re-downloading.
  5. Multiple products / the same vendor twice — no double-load, and the SDK warns on an overlap.

Drive it with real banner clicks, not just programmatic calls, so the UI wiring is exercised too. In a browser, watch the Network tab and document.cookie.

Debugging

The runtime exposes getIntegrations() — config + live status for each integration — as the data for a debug view:

import { getCookieYes } from "@cookieyes/react"; // or the runtime initCookieYes returns

console.table(getCookieYes().getIntegrations());
// [{ id: "segment", category: "analytics", load: "afterConsent", onRevoke: "remove", status: "active" }, …]

Good to know

  • Removing a loaded script can't un-run it. On revoke we remove the tag and clear identifiers, but code already executed stays in memory until the next page load — where the script never loads again without consent. That's the real, full block; the client-side removal is the best-effort in-between.
  • Don't configure the same vendor twice — a preset in integrations and the same vendor in the deprecated builtInIntegrations would load it twice. The SDK warns if it sees this.

License

MIT © CookieYes