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

flowgrid-sdk

v2.1.2

Published

A TypeScript SDK for tracking user events, feature usage, experiments, and feature flags with Flowgrid.

Readme

Flowgrid SDK

npm license types

A production-grade TypeScript SDK for product analytics, feature flags, experiments, and enterprise insights. First-class bindings for React and Next.js; the core (track, flags, identify) is plain JavaScript and runs in any browser, Node or edge runtime.

Start for free: [https://www.flow-grid.xyz/]

import { FlowGrid } from "flowgrid-sdk";

// One line. Sane defaults. Auto-instruments sessions, page views,
// performance, heatmaps, and attribution out of the box.
const fg = FlowGrid.init({
  webId: "wx_123",
  apiKey: "pk_xxx",
  // Opt in to the extras you want at init.
  autoTrack: {
    replay: true,
    engagement: true,
    performance: true,
    passiveIdentity: true,
  },
});

await fg.track("signup_completed", { plan: "pro" });
await fg.track("feature_used", { featureId: "export", featureName: "Data Export", userId: "u_1" });
await fg.track("experiment_exposure", { experimentId: "checkout_v2", variantId: "B", userId: "u_1" });
await fg.track("add_to_cart", { productId: "sku_1", name: "T-Shirt", price: 29, currency: "USD", quantity: 1 });

Highlights

  • One class, every feature — new FlowGrid(...) exposes analytics, experiments, growth and enterprise modules as lazy properties.
  • Feature flags in one line — flags.on('new-checkout'). Synchronous, works offline, never throws; an unknown flag returns the default you pass. Declare flags in code and they appear in the dashboard, switched off.
  • Feature usage, explicit — fg.track("feature_usage", { featureId, featureName }). You say what happened; the SDK just sends it.
  • Passive identity — opt in at init with autoTrack: { passiveIdentity: true } (or later via fg.autoTrack({ passiveIdentity: true })) to recognise anonymous visitors from any form (PII hashed in the browser).
  • React & Next.js bindings — hooks, components and Server Component reads. react is the only (optional) peer dependency; the core runs in any browser, Node or edge runtime.
  • Privacy & consent built-in — DNT/GPC support, ConsentManager, bot filtering.
  • Resilient transport — retry with exponential backoff, sendBeacon fallback, offline localStorage buffering, 10KB payload guard.
  • Strict TypeScript — strong types for every config and response.

Installation

npm install flowgrid-sdk
# or
pnpm add flowgrid-sdk
# or
yarn add flowgrid-sdk

Requires Node >= 18.12. No peer dependencies.


Quick Start

FlowGrid is one platform with two runtimes. Initialise once per runtime — the browser client for everything a visitor does on the page, the server client for what happens in your backend — and both feed the same dashboard under the same identity:

// lib/flowgrid.client.ts — BROWSER: init once, import everywhere client-side
import { FlowGrid } from "flowgrid-sdk";

export const fg = FlowGrid.init({
  webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!,   // public site id (wx_…)
  apiKey: process.env.NEXT_PUBLIC_FLOWGRID_API_KEY!, // public API key

  // Who is this? (optional — identifies up-front so the very first events attribute)
  user: { userId: "u_1", email: "[email protected]", name: "Alice" },

  // What gets auto-tracked? engagement + replay are on by default;
  // pageviews/sessions/heatmaps/performance come from the script tag install.
  autoTrack: {
    engagement: true,
    replay: { sampleRate: 1.0, maskAllInputs: true },
    passiveIdentity: true, // recognise visitors from forms (PII hashed in-browser)
  },

  // What is the visitor consenting to? (see “Cookie Consent” below —
  // pair with renderConsentBanner() for an opt-in banner in one call)
  consent: { analytics: true, marketing: true },
});
// lib/flowgrid.server.ts — SERVER (Node ≥ 18 / edge): identity, feature usage, revenue, errors
import { FlowGridServer } from "flowgrid-sdk/server";

export const fgServer = FlowGridServer.init({
  webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // same site id
  apiKey: process.env.FLOWGRID_API_KEY!,           // private key — server env only
});

That's the whole setup. Browser calls (fg.track, fg.identifyUser, fg.flags.on, …) and server calls (fgServer.trackAuth, fgServer.trackFeature, fgServer.trackSubscription, fgServer.trackError, …) speak the same wire contract, so a user identified in an OAuth callback on the server and the same user clicking around in the browser resolve to one identity in the dashboard.

Unified client (browser)

import { FlowGrid } from "flowgrid-sdk";

const fg = FlowGrid.init({
  webId: "web_123",
  apiKey: "key_xxx",
  autoTrack: { heatmaps: { movement: true } },
});

// Plain custom events still work.
await fg.track("cta_click", { id: "hero" }, { userId: "u_1" });

// Feature usage — a plain event.
fg.track("feature_usage", { featureId: "data_export", featureName: "Data Export" });

// Recognized event names route to the matching feature module.
await fg.track("feature_used", { featureId: "export", featureName: "Data Export", userId: "u_1" });
await fg.track("prompt_submitted", { promptId: "p_1", promptType: "chat", userId: "u_1" });
await fg.track("experiment_conversion", { experimentId: "checkout_v2", variantId: "B", userId: "u_1", metricName: "purchase" });
await fg.track("support.ticket_created", { ticketId: "t_1", userId: "u_1", subject: "Help", category: "billing", priority: "medium", channel: "email" });

Available namespaces on FlowGrid:

| Group | Properties | | ----------- | ----------------------------------------------------------------------------------------------------------------------- | | Core | activation, features, prompts, experiments, flags | | Analytics | pageViews, sessions, events, identify, funnels, retention, attribution, heatmaps, performance, replay | | Growth | subscriptions | | Enterprise | engagement, cohorts, churn, monetization, multiPathFunnels, support, acquisition, paths, alerts, security, forecasting |

Browser script tag (CMS / no-build)

<script src="https://cdn.flow-grid.xyz/flowgrid.min.js"></script>
<script>
  FlowGrid.init({
    webId: "web_abc123",
    apiKey: "key_xxx",
  });

  FlowGrid.track("signup_clicked", { placement: "hero" });
  FlowGrid.identify("user_123", { plan: "pro" });
  FlowGrid.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" });
  FlowGrid.track("add_to_cart", { productId: "sku_1", name: "T-Shirt", price: 29, quantity: 1 });
</script>

The script-tag API is intentionally small: init, track, page, identify, signup, login, defineFeature, experiment, autoTrack, consent helpers (setConsent / hasConsent / wireConsentBanner), reset, and instance() for the full SDK. FlowGrid.init() auto-instruments sessions, page views, SPA route changes, scroll depth, time on page, performance, heatmaps, and attribution by default; session replay is opt-in with autoTrack: { replay: true }, and passive identity capture with autoTrack: { passiveIdentity: true }.


Feature Usage

Track product feature usage with a plain event — featureId and featureName are the only required props:

fg.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" });

// Optional: an action ("viewed" | "used" | "completed" | "abandoned"), a category, a known user.
fg.track("feature_usage", {
  featureId: "ai_agent_builder",
  featureName: "AI Agent Builder",
  action: "completed",
  category: "automation",
  userId: "u_1",
});
// script tag: FlowGrid.track("feature_usage", { featureId: "ai_agent_builder", featureName: "AI Agent Builder" })

action defaults to "used". No userId or category is required — it works for anonymous visitors out of the box (the backend defaults userId to the visitor and category to "general"). The event routes through the unified .track() router, so it shares routing/consent/transport with every other event.

Helpers

Don't want to hand-write track("feature_usage", …) and repeat the id/name? Bind a feature once with fg.defineFeature and call the verb that describes what happened — each maps to a lifecycle stage the dashboard funnel understands (a viewed feature is Discovered, a used one is Adopted):

const feature = fg.defineFeature("ai-agent-builder", "AI Agent Builder", { category: "automation" });

feature.viewed();                 // action: "viewed"    → Discovered + Viewed
feature.used({ model: "opus" });  // action: "used"      → Adopted
feature.completed();              // action: "completed" → Completed
feature.abandoned();              // action: "abandoned" → Abandonment
// script tag: FlowGrid.defineFeature("ai-agent-builder", "AI Agent Builder").used()

Each call is a thin, explicit wrapper over track("feature_usage", …) — no hidden state, no lifecycle guessing; you send exactly the action you name. The same verbs exist on fg.features.viewed/used/completed/abandoned(id, name) for one-off calls, and fg.features.record(id, name, action) for a dynamic action.


Feature Flags

Ship code with a feature turned off, then turn it on from the dashboard — for everyone, for a percentage, or for one customer. No deploy, no restart.

import { flags } from 'flowgrid-sdk';

if (flags.on('new-checkout')) {
  renderNewCheckout();
}

That is the whole integration. There is nothing to register and nothing to configure: FlowGrid.init() already loaded your flags.

The three rules

  1. on() is synchronous. Rules are already in memory, so a flag read is a map lookup — safe on a render path, no await, no loading state.

  2. The second argument is the default, not an options bag. flags.on('x', true) reads as "on unless told otherwise". Flag not created, bundle not loaded, network down, SDK blocked by an ad blocker — you get your default. Never an exception, never a hang.

  3. Create flags in the dashboard or in code. List keys in flags.declare and any your website doesn't have yet appear in the dashboard, switched off. Declaring never turns a flag on or changes an existing flag, so it is safe to repeat. Keys are used exactly as written.

    FlowGrid.init({ webId, flags: { declare: ['new-checkout'] } });
    await getServerFlags({ webId, declare: ['new-checkout'] }); // server

Framework bindings

Same contract everywhere, and the same shape as useExperiment:

// React
import { useFlag, Flag } from 'flowgrid-sdk/react';

const enabled = useFlag('new-checkout');
return enabled ? <NewCheckout /> : <Checkout />;

// or declaratively
<Flag name="new-checkout" fallback={<Checkout />}>
  <NewCheckout />
</Flag>
// Next.js — Server Component. No flash, no hydration mismatch.
import { getServerFlag } from 'flowgrid-sdk/server';

const enabled = await getServerFlag('new-checkout', {
  webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!,
  context: { userId: session?.user.id },
});

Errors from a flagged feature

A flag's error rate counts only errors the flagged code throws — never page-wide browser errors. <Flag> catches them for you; elsewhere use flags.run(). If the new path throws, the error is reported and the old path runs instead.

const total = await flags.run('new-pricing', () => priceV2(cart), () => price(cart));
flags.reportError('new-pricing', error);             // anything else
fg.trackError(error, { flag: 'new-pricing', visitorId }); // server

Targeting

Rules live in the dashboard, not in your bundle, so changing who gets a feature never touches this code. Everything the SDK knows about the visitor is available to a rule:

FlowGrid.init({
  webId: '…',
  // Optional. `environment` is worked out from the host you're on — localhost
  // and LAN addresses are `development`, per-branch preview URLs and a leading
  // `staging.`/`qa.` are `preview`, everything else is `production`. Pass it
  // yourself only when you know better than the hostname does.
  flagContext: { environment: process.env.NEXT_PUBLIC_VERCEL_ENV },
});

// Every trait here becomes an attribute you can target a rule on — plan, org,
// role, whatever your product actually has. User rules key off the id.
fg.identifyUser('u_123', { plan: 'pro', org: 'acme' });

In the browser, visitorId, environment and sdkVersion are filled in for you, so percentage rollouts split anonymous traffic and environment/SDK-version rules work with no setup. On a server, pass visitorId (from visitorIdFromCookies()) and environment to getServerFlags yourself. region you cannot pass meaningfully and shouldn't try — a browser has no honest answer, so a flag with a region rule is decided on FlowGrid's side from the request itself. You read it exactly like any other flag.

Each rule reads exactly one field, and a rule whose input you didn't provide simply doesn't match — so a forgotten field under-enables rather than leaking an unreleased feature.

Percentage rollouts are sticky and monotonic. A visitor's bucket never moves, and raising 10% → 20% only ever adds people — nobody loses the feature mid-rollout. Server and browser bucket identically, so an SSR render and its hydration agree.

They also work on anonymous traffic: the SDK buckets on the visitor id it already keeps, so you don't need logins to roll out gradually. After sign-in, bucketing moves to the userId you pass, so a rollout follows one person across devices — which can change whether that individual is in the slice.

Rules don't AND, so "50% of pro customers" is two flags — one for the audience, one for the pace:

// 'pro-beta-eligible' → Attribute: plan is one of [pro]
// 'new-checkout'      → Percentage: 25%
if (flags.on('pro-beta-eligible') && flags.on('new-checkout')) { /* … */ }

Flags that target individual people are never evaluated in the browser. The rule is the target list, and shipping it would hand that list to anyone with devtools. Those flags resolve on our servers and only the resulting boolean crosses the wire — you don't have to do anything to get this; it's decided when the flag is compiled.

Any framework, in four lines

The shipped React and Next bindings are conveniences, not privileged access. Underneath, a flag is a subscribable store — subscribe(listener) returning an unsubscribe, with the listener called immediately — which is the shape every reactive framework already knows how to consume:

// Solid
const [on, setOn] = createSignal(false);
onCleanup(fg.flags.store('new-checkout').subscribe(setOn));

// Angular
readonly on = toSignal(from(fg.flags.store('new-checkout')), { initialValue: false });

// Qwik
useVisibleTask$(({ cleanup }) => cleanup(fg.flags.store('new-checkout').subscribe((v) => (sig.value = v))));

// Vanilla, Alpine, Lit, htmx, a web component, anything
fg.flags.store('new-checkout').subscribe((on) => { el.hidden = !on; });

If you don't need reactivity at all, flags.on('key') is a synchronous boolean and works in any JavaScript that runs — Node, Deno, Bun, Cloudflare Workers, a Chrome extension, a build script.

Without the rest of the SDK

Flags don't require FlowGrid's analytics. If you already have analytics, or you're in a runtime that has no use for it:

import { createFlags } from 'flowgrid-sdk';

const flags = createFlags({ webId: '…', context: { environment: 'production' } });
await flags.ready();
if (flags.on('new-checkout')) { … }

Same engine, same guarantees.

Typed keys, without a code generator

// flags.ts
import { defineFlags } from 'flowgrid-sdk';
export const flags = defineFlags(['new-checkout', 'dark-mode'] as const);

// anywhere
flags.on('new-checkout');   // ✅
flags.on('new-chekcout');   // ❌ compile error

Reacting to changes

await flags.ready();               // resolves once flags have loaded (or failed)
const stop = flags.onChange(rerender);

The framework bindings do this for you.

Caching, and debugging a wrong answer

Flags are fetched once per page load and kept for a day, so a returning visitor evaluates correctly on the first render — before the network answers, and correctly enough with none at all. That is the default and most apps should leave it. It is a trade, though: a cached answer can be up to a day stale for one round trip, which is a bargain on a marketing page and unacceptable on some others.

Set this in your FlowGrid dashboard (Feature flags → Delivery). It travels with your flags, so changing it takes effect on your visitors' next load with no deploy of your app — which is the point. The options below configure the SDK for the case where you have not set it in the dashboard; the dashboard wins whenever it has an opinion, and flags.lookupPolicySource tells you which is in force.

FlowGrid.init({
  webId: '…',
  flags: {
    // 'persistenceUntilNetworkSuccess' (default) — the cached answer renders
    //   immediately and is replaced when the fetch lands.
    // 'networkFirst' — your own default renders until the fetch lands; the
    //   cache is used only if it fails.
    // 'networkOnly' — nothing is stored. A failed fetch means every flag is
    //   the default you passed.
    lookupPolicy: 'networkFirst',
    persistenceTtlMs: 60 * 60 * 1000,   // default: 24h
  },
});

Two conveniences come from the stored copy, and networkOnly gives up both: a visitor with no connection has no flags, and a route you have excluded from flag delivery can no longer be recognised locally, so every load on it asks FlowGrid instead of answering for free.

When a flag isn't doing what you expect, explain() answers both halves — which step decided, and which copy of your flags it decided against:

flags.explain('new-checkout');
// {
//   value:   false,
//   variant: null,
//   reason:  'default',      // kill_switch | rule | default | unknown_flag
//   source:  'persistence',  // network | persistence | resolve | fallback
// }
flags.lookupPolicy();        // 'networkFirst'
fg.flags.lookupPolicySource; // 'dashboard' — your init config is being overridden

reason is why; source is from where. resolve means the flag's rules were withheld from the browser on purpose and our server answered the key directly; fallback means there was no answer here at all and you got your own default. The same reason off a day-old cache and off a fresh fetch are the same reasoning and completely different bugs, which is why both are reported.


Passive Identity Capture

Recognize anonymous visitors automatically: when a visitor types their email / name / phone into any form, FlowGrid classifies the field, hashes PII in the browser, and links the identity to their visitor_id — so the dashboard shows them by name (and, with a CRM connected, matches them to the contact). No per-form instrumentation.

It's opt-in in the SDK (the flowgrid.js snippet runs it by default) and requires analytics consent:

const fg = FlowGrid.init({ webId: "...", apiKey: "..." });
FlowGrid.setConsent({ analytics: true });

const stop = fg.autoTrack({ passiveIdentity: true });
// or with options:
fg.autoTrack({ passiveIdentity: { debounceMs: 800, excludePath: /\/(checkout|admin)/i } });

Never reads password/hidden/card/cvv/ssn/pin fields, anything marked data-analytics="ignore" or .no-track, free-text (message/comment/search), or sensitive paths (/checkout, /account/settings, /admin). Raw email goes only to the server-side identity store; the event stream sees hashes.


Server-side / Edge Usage

Use the dedicated flowgrid-sdk/server entry point on the server. It is a Node/edge-safe client (global fetch only — no window, document, or storage) scoped to what makes sense off-browser: identity, feature usage, subscriptions & revenue attribution, feature flags, and API / server error tracking. It speaks the same wire contract as the browser SDK, so events land in the same pipes. Works in Node ≥ 18, Bun, Deno, Cloudflare Workers, and Vercel Edge/serverless.

// lib/flowgrid-server.ts — init once, import anywhere server-side
import { FlowGridServer } from "flowgrid-sdk/server";

export const fg = FlowGridServer.init({
  webId: process.env.NEXT_PUBLIC_WEB_ID!,
  apiKey: process.env.FLOWGRID_API_KEY!, // required on the server
});

Identity — auth-level events (identify a visitor server-side, e.g. in an OAuth callback or session endpoint). Pass the browser's visitor id when you have the request cookies so server events link to the web session; without it, a stable synthetic visitor is derived from the userId:

import { visitorIdFromCookies } from "flowgrid-sdk/server";
import { cookies } from "next/headers";

const visitorId = visitorIdFromCookies(await cookies());

await fg.trackAuth(
  { event: "login", userId: user.id, email: user.email, method: "google" },
  { visitorId },
); // identify + login_completed + active_user ping, in one call

// Or the individual operations:
await fg.identifyUser(user.id, { email: user.email, plan: "pro" }, { visitorId });
await fg.updateTraits(user.id, { plan: "enterprise" });
await fg.pingActiveUser(user.id, { email: user.email }); // DAU/WAU/MAU by identity
await fg.logout(user.id);

Feature usage from API routes, jobs, and webhooks:

await fg.trackFeature({ featureId: "csv_export", featureName: "CSV Export", userId: user.id });

// Or bind once and use semantic verbs (server twin of fg.defineFeature):
const teamUpdate = fg.defineFeature("team_update", "Team update", { category: "teams" });
await teamUpdate.used({ userId: user.id, teamId });

Revenue — purchases and refunds are not SDK calls. Connect your payment provider in Flowgrid (Website settings → Payments) and Flowgrid receives its webhook directly, where the money is actually confirmed. What the SDK owns is the subscription / MRR lifecycle, sent from your billing webhooks:

// Subscription / MRR lifecycle — one method, discriminated on `event`:
await fg.trackSubscription({
  event: "start", // "change" | "cancel" | "renew" | "trial_converted" | "trial_expired"
  subscriptionId: sub.id,
  userId,
  plan: "professional",
  mrr: { amount: 9900, currency: "USD" }, // smallest currency unit
  billingCycle: "monthly",
  trialDays: 14,
});

Revenue attribution — payment webhooks arrive with no cookies, so the visitor/session ids ride through the provider's checkout metadata and come back out in the webhook. Two legs:

// LEG 1 — checkout creation: embed the ids from the request cookies.
import { checkoutAttributionFromCookies } from "flowgrid-sdk/server";
import { cookies } from "next/headers";

// Stripe
const session = await stripe.checkout.sessions.create({
  // …
  metadata: { ...checkoutAttributionFromCookies(await cookies()) },
});

// Lemon Squeezy
const checkout = await createCheckout(storeId, variantId, {
  checkoutData: { custom: { ...checkoutAttributionFromCookies(await cookies()) } },
});

// (Creating the checkout client-side instead? Use fg.checkoutAttribution()
//  from the browser SDK — same keys.)
// LEG 2 — payments: Flowgrid reads the ids from the provider's webhook itself.
// Subscription events you send from your own webhook carry them explicitly:
import { attributionFromMetadata } from "flowgrid-sdk/server";

// Stripe customer.subscription.created — unwraps `.metadata` automatically
const { visitorId, sessionId } = attributionFromMetadata(event.data.object);

// Lemon Squeezy — unwraps `.meta.custom_data` automatically
// const { visitorId, sessionId } = attributionFromMetadata(payload);

await fg.trackSubscription({ event: "start", subscriptionId, userId, plan, mrr }, { visitorId, sessionId });

With the real visitorId + sessionId on the payment, revenue joins the visitor's full journey — the session, landing page and UTM campaign that earned it — instead of a synthetic server visitor.

API + server-side error tracking — errors become error_events with route/method/status context; FlowGrid-internal noise is filtered out:

try { … } catch (err) {
  await fg.trackError(err, { route: "/api/teams/edit/[teamId]", method: "PATCH", statusCode: 500, userId });
  throw err;
}

// Or wrap a handler — tracks and rethrows, framework behaviour unchanged:
export const PATCH = fg.withErrorTracking(handler, { route: "/api/teams/edit/[teamId]", method: "PATCH" });

// Opt-in process-level capture (Node only; observes, never swallows):
const detach = fg.captureUncaught();

Like the browser transport, the server client never throws for delivery problems — every method resolves { ok, status?, reason? }.


Cookie Consent

What you're asking consent for

When a visitor accepts cookies, this is exactly what each category permits FlowGrid to do (this is what the SDK enforces, not aspirational copy — each category maps to a hard gate in the transport).

Default posture: implied consent. Every category starts granted, and tracking runs out of the box — the "declined" behaviour below only applies when you've installed a consent gate (requireExplicitConsent: true or the opt-in banner) and the visitor actively rejects a category:

| Category | Can the visitor decline? | What it covers | | ------------- | ------------------------ | -------------- | | necessary | No — always on | The visitor/session identifiers (visitor_id cookie, fg_session_id) and the consent cookies themselves (fg_consent, fg_tracking_consent). Required for the service to function; no behavioural profiling. | | analytics | Yes | All event tracking. Page views, sessions, clicks, forms, searches, feature usage, funnels, performance/Web Vitals, engagement, identity events (identifyUser, active-user pings), and session replay recordings — plus the device/page context attached to each event (screen size, device type, language, URL, path, title, referrer). Declined → no events leave the browser. | | marketing | Yes | Campaign attribution. The utm_source/medium/campaign/term/content and ref/via/source parameters read from the URL and the __flowgrid_* landing-page cookies, attached to events as _utm_*/_ref properties. Declined → events (if analytics is granted) are sent without any campaign attribution. | | preferences | Yes | UI preferences your own app stores (locale, theme). FlowGrid itself stores nothing in this category — it exists so your banner can offer it. |

Independent of the banner, visitors sending Do-Not-Track or Global Privacy Control are treated as having declined non-essential tracking by default (respectDNT: true), and localhost traffic is never tracked unless explicitly enabled.

Server-side events (flowgrid-sdk/server) are outside the browser consent gates by design: they represent your backend's own records (auth events, API feature usage, server errors) under your contractual/legitimate-interest basis. If you want browser consent to extend to server calls, check your stored consent state before calling the server client.

No-code (one tag) — recommended

Drop a single element on the page and FlowGrid renders a styled, opt-in, GDPR/CCPA-friendly consent banner that blocks analytics, marketing and session-replay tracking until the visitor accepts — with zero JavaScript:

<script src="https://cdn.flow-grid.xyz/flowgrid.min.js" data-site="YOUR_WEBSITE_ID"></script>
<div data-cookie-ccbanner></div>

The banner auto-initializes ahead of (and independently of) tracking, so consent gates engage before the first event fires. It re-scans for late-mounted markers (page builders / SPA routes) and is idempotent.

Tier 1 — attributes (no CSS). Tune copy, colours, position via data-cc-*:

<div data-cookie-ccbanner
     data-cc-position="bottom-right"
     data-cc-primary="#EBF212"
     data-cc-radius="16px"
     data-cc-privacy-url="/privacy"></div>

Common attributes: data-cc-mode (opt-in|opt-out), data-cc-title, data-cc-description, data-cc-accept-label, data-cc-reject-label, data-cc-position, data-cc-primary / -bg / -text, data-cc-radius, data-cc-categories (analytics,marketing), data-cc-cookie-name / -days / -domain, data-cc-respect-dnt, data-cc-theme (flowgrid|light|dark), data-cc-manage-selector (re-open hook). Add data-cc-manual to a tag to skip auto-init.

Tier 2 — your CSS. Add data-cc-unstyled to drop the default look and style the stable class hooks (.fg-cc-banner, .fg-cc-card, .fg-cc-btn--accept, …) and CSS variables (--fg-cc-bg, --fg-cc-primary, …) from your own stylesheet.

Tier 3 — bring your own markup. Put your own HTML inside the marker; FlowGrid binds your buttons by data-role and never injects DOM:

<div data-cookie-ccbanner style="display:none">
  <h2>Cookies?</h2>
  <button data-cc-accept>Sure</button>
  <button data-cc-reject>No thanks</button>
</div>

Global defaults can be set on window.FlowGridConfig.consentBanner before the tag; per-element data-cc-* attributes override them. SPA apps can re-scan after a client-side navigation with FlowGrid.wireConsentBanner() (or import wireDeclarativeBanners() directly).

Programmatic banner

Render the same banner from code with renderConsentBanner():

import { renderConsentBanner } from "flowgrid-sdk";

const banner = renderConsentBanner({
  mode: "opt-in",
  title: "We value your privacy",
  privacyPolicyUrl: "/privacy",
  theme: { position: "bottom-right", primaryColor: "#EBF212" },
});

document.getElementById("manage-cookies")?.addEventListener("click", () => banner?.show());

Core ConsentManager

The SDK ships a framework-agnostic ConsentManager. Render your own banner UI in whatever framework you use, and call into the manager to persist preferences.

import { ConsentManager, FlowGridTransport } from "flowgrid-sdk";

const consent = new ConsentManager({
  cookieDomain: ".example.com",
  cookieExpiry: 365,
  respectDNT: true,
  onChange: (prefs) => {
    FlowGridTransport.setConsent({
      analytics: prefs.analytics,
      marketing: prefs.marketing,
    });
  },
});

consent.acceptAll();
consent.rejectNonEssential();
consent.update({ analytics: true, marketing: false, preferences: true });
consent.hasCategory("analytics"); // boolean
consent.reset();

CookieBannerConfig and CookieBannerTheme types are exported from flowgrid-sdk/consent if you want a typed config object to pass to your own banner component.

You can also gate the transport directly without ConsentManager:

import { FlowGridTransport } from "flowgrid-sdk";

FlowGridTransport.setConsent({ analytics: true, marketing: false });
FlowGridTransport.hasConsent("analytics"); // true

Package Exports

| Import path | Description | | -------------------------- | ------------------------------------------------ | | flowgrid-sdk | Unified FlowGrid class + every feature module | | flowgrid-sdk/analytics | Analytics modules only | | flowgrid-sdk/core | Core modules (activation, experiments, feature flags, prompts) | | flowgrid-sdk/react | useFlag, <Flag>, useExperiment, <Experiment> | | flowgrid-sdk/next | getServerFlag/getServerFlags, getVariant (server-side) | | flowgrid-sdk/consent | ConsentManager + types | | flowgrid-sdk/server | FlowGridServer — Node/edge tracking (identity, feature usage, subscriptions, errors) + getServerFlags | | flowgrid-sdk/types | Shared TypeScript type definitions |


Documentation

Full reference: https://flow-grid.xyz/documentation

License

MIT © Flowgrid

Contact Support [email protected]