flowgrid-sdk
v1.8.2
Published
A TypeScript SDK for tracking user events, feature usage, experiments, and feature flags with Flowgrid.
Maintainers
Readme
Flowgrid SDK
A production-grade, framework-agnostic TypeScript SDK for product analytics, e-commerce, experiments, and enterprise insights — with a single unified entry point that works in any JavaScript environment (Node, browsers, edge runtimes, React, Vue, Next, Nuxt, Svelte, Angular, etc.).
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, ecommerce, experiments, and enterprise modules as lazy properties. - 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 viafg.autoTrack({ passiveIdentity: true })) to recognise anonymous visitors from any form (PII hashed in the browser). - Framework-agnostic — zero peer dependencies. Works with React, Vue, Next.js, Nuxt, Svelte, Astro, plain Node, Cloudflare Workers, etc.
- Privacy & consent built-in — DNT/GPC support,
ConsentManager, bot filtering. - Resilient transport — retry with exponential backoff,
sendBeaconfallback, offlinelocalStoragebuffering, 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-sdkRequires 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.cart.add, …) and server calls (fgServer.trackAuth,
fgServer.trackFeature, fgServer.trackPurchase, 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("purchase", { order });
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 |
| Analytics | pageViews, sessions, events, identify, funnels, retention, attribution, heatmaps, performance, replay |
| E-commerce | products, cart, checkout, purchases, refunds, promotions, wishlist, ltv, search, 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.productView({ productId: "sku_1", name: "T-Shirt", price: 29 });
FlowGrid.addToCart({ productId: "sku_1", name: "T-Shirt", price: 29 }, 1);
</script>The script-tag API is intentionally small: init, track, page, identify,
feature, experiment, productView, addToCart, purchase, autoTrack,
consent helpers (setConsent / hasConsent), 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.
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, ecommerce / revenue, 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 });Ecommerce / revenue — record purchases, refunds, order lifecycle, and subscription MRR events from your payment webhooks, where the money is actually confirmed. Browser purchase events are structurally lossy (ad blockers, closed tabs on the checkout redirect); the server is the authoritative source for revenue:
// Stripe webhook: checkout.session.completed
await fg.trackPurchase({
order: {
orderId: session.id,
items, // CartItem[] — same shape as the browser SDK
subtotal, discountTotal: 0, shippingTotal: 0, taxTotal,
total: session.amount_total / 100,
currency: "USD",
payment: { method: "credit_card", provider: "stripe" },
},
userId: session.client_reference_id,
});
// charge.refunded — keeps net revenue accurate
await fg.trackRefund({
orderId, refundId: refund.id,
amount: refund.amount / 100, currency: "USD",
reason: "customer_request", isFullRefund: true, userId,
});
// Fulfilment webhooks
await fg.trackOrderStatus(orderId, "processing", "pending", { userId });
await fg.trackOrderShipped(orderId, { method: "express", cost: 5, trackingNumber });
await fg.trackOrderDelivered(orderId);
// 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 — webhook: read the ids back and attach them to the revenue event.
import { attributionFromMetadata } from "flowgrid-sdk/server";
// Stripe checkout.session.completed — unwraps `.metadata` automatically
const { visitorId, sessionId } = attributionFromMetadata(event.data.object);
// Lemon Squeezy order_created — unwraps `.meta.custom_data` automatically
// const { visitorId, sessionId } = attributionFromMetadata(payload);
await fg.trackPurchase({ order, userId }, { visitorId, sessionId });With the real visitorId + sessionId on the purchase, 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"); // truePackage Exports
| Import path | Description |
| -------------------------- | ------------------------------------------------ |
| flowgrid-sdk | Unified FlowGrid class + every feature module |
| flowgrid-sdk/analytics | Analytics modules only |
| flowgrid-sdk/ecommerce | Ecommerce modules only |
| flowgrid-sdk/core | Core modules (activation, experiments, prompts) |
| flowgrid-sdk/consent | ConsentManager + types |
| flowgrid-sdk/server | FlowGridServer — Node/edge tracking (identity, feature usage, revenue, errors) |
| flowgrid-sdk/types | Shared TypeScript type definitions |
Documentation
Full reference: https://flow-grid.xyz/documentation
License
MIT © Flowgrid
Contact Support [email protected]
