flowgrid-sdk 1.7.7 → 1.7.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -62,7 +62,54 @@ Requires Node `>= 18.12`. No peer dependencies.
62
62
 
63
63
  ## Quick Start
64
64
 
65
- ### Unified client (recommended)
65
+ FlowGrid is one platform with two runtimes. Initialise **once per runtime** —
66
+ the browser client for everything a visitor does on the page, the server
67
+ client for what happens in your backend — and both feed the same dashboard
68
+ under the same identity:
69
+
70
+ ```ts
71
+ // lib/flowgrid.client.ts — BROWSER: init once, import everywhere client-side
72
+ import { FlowGrid } from "flowgrid-sdk";
73
+
74
+ export const fg = FlowGrid.init({
75
+ webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // public site id (wx_…)
76
+ apiKey: process.env.NEXT_PUBLIC_FLOWGRID_API_KEY!, // public API key
77
+
78
+ // Who is this? (optional — identifies up-front so the very first events attribute)
79
+ user: { userId: "u_1", email: "alice@example.com", name: "Alice" },
80
+
81
+ // What gets auto-tracked? engagement + replay are on by default;
82
+ // pageviews/sessions/heatmaps/performance come from the script tag install.
83
+ autoTrack: {
84
+ engagement: true,
85
+ replay: { sampleRate: 1.0, maskAllInputs: true },
86
+ passiveIdentity: true, // recognise visitors from forms (PII hashed in-browser)
87
+ },
88
+
89
+ // What is the visitor consenting to? (see “Cookie Consent” below —
90
+ // pair with renderConsentBanner() for an opt-in banner in one call)
91
+ consent: { analytics: true, marketing: true },
92
+ });
93
+ ```
94
+
95
+ ```ts
96
+ // lib/flowgrid.server.ts — SERVER (Node ≥ 18 / edge): identity, feature usage, revenue, errors
97
+ import { FlowGridServer } from "flowgrid-sdk/server";
98
+
99
+ export const fgServer = FlowGridServer.init({
100
+ webId: process.env.NEXT_PUBLIC_FLOWGRID_WEB_ID!, // same site id
101
+ apiKey: process.env.FLOWGRID_API_KEY!, // private key — server env only
102
+ });
103
+ ```
104
+
105
+ That's the whole setup. Browser calls (`fg.track`, `fg.identifyUser`,
106
+ `fg.cart.add`, …) and server calls (`fgServer.trackAuth`,
107
+ `fgServer.trackFeature`, `fgServer.trackPurchase`, `fgServer.trackError`, …) speak the same wire
108
+ contract, so a user identified in an OAuth callback on the server and the
109
+ same user clicking around in the browser resolve to **one identity** in the
110
+ dashboard.
111
+
112
+ ### Unified client (browser)
66
113
 
67
114
  ```ts
68
115
  import { FlowGrid } from "flowgrid-sdk";
@@ -202,25 +249,193 @@ to the server-side identity store; the event stream sees hashes.
202
249
 
203
250
  ## Server-side / Edge Usage
204
251
 
205
- Works in any JavaScript runtime that supports `fetch` Node, Bun, Deno, Cloudflare Workers, Vercel Edge, etc.
252
+ Use the dedicated **`flowgrid-sdk/server`** entry point on the server. It is a
253
+ Node/edge-safe client (global `fetch` only — no `window`, `document`, or
254
+ storage) scoped to what makes sense off-browser: **identity**, **feature
255
+ usage**, **ecommerce / revenue**, and **API / server error tracking**. It speaks the same wire contract
256
+ as the browser SDK, so events land in the same pipes. Works in Node ≥ 18, Bun,
257
+ Deno, Cloudflare Workers, and Vercel Edge/serverless.
206
258
 
207
259
  ```ts
208
- // app/api/track/route.ts (Next.js example, but works anywhere)
209
- import { fg } from "@/lib/utils";
260
+ // lib/flowgrid-server.ts init once, import anywhere server-side
261
+ import { FlowGridServer } from "flowgrid-sdk/server";
262
+
263
+ export const fg = FlowGridServer.init({
264
+ webId: process.env.NEXT_PUBLIC_WEB_ID!,
265
+ apiKey: process.env.FLOWGRID_API_KEY!, // required on the server
266
+ });
267
+ ```
268
+
269
+ **Identity — auth-level events** (identify a visitor server-side, e.g. in an
270
+ OAuth callback or session endpoint). Pass the browser's visitor id when you
271
+ have the request cookies so server events link to the web session; without it,
272
+ a stable synthetic visitor is derived from the `userId`:
273
+
274
+ ```ts
275
+ import { visitorIdFromCookies } from "flowgrid-sdk/server";
276
+ import { cookies } from "next/headers";
277
+
278
+ const visitorId = visitorIdFromCookies(await cookies());
279
+
280
+ await fg.trackAuth(
281
+ { event: "login", userId: user.id, email: user.email, method: "google" },
282
+ { visitorId },
283
+ ); // identify + login_completed + active_user ping, in one call
284
+
285
+ // Or the individual operations:
286
+ await fg.identifyUser(user.id, { email: user.email, plan: "pro" }, { visitorId });
287
+ await fg.updateTraits(user.id, { plan: "enterprise" });
288
+ await fg.pingActiveUser(user.id, { email: user.email }); // DAU/WAU/MAU by identity
289
+ await fg.logout(user.id);
290
+ ```
291
+
292
+ **Feature usage** from API routes, jobs, and webhooks:
293
+
294
+ ```ts
295
+ await fg.trackFeature({ featureId: "csv_export", featureName: "CSV Export", userId: user.id });
296
+
297
+ // Or bind once and use semantic verbs (server twin of fg.defineFeature):
298
+ const teamUpdate = fg.defineFeature("team_update", "Team update", { category: "teams" });
299
+ await teamUpdate.used({ userId: user.id, teamId });
300
+ ```
301
+
302
+ **Ecommerce / revenue** — record purchases, refunds, order lifecycle, and
303
+ subscription MRR events from your **payment webhooks**, where the money is
304
+ actually confirmed. Browser purchase events are structurally lossy (ad
305
+ blockers, closed tabs on the checkout redirect); the server is the
306
+ authoritative source for revenue:
210
307
 
211
- export async function POST(req: Request) {
212
- const { userId } = await req.json();
213
- await fg.events.track({ eventName: "server_event", properties: { userId } });
214
- return Response.json({ ok: true });
308
+ ```ts
309
+ // Stripe webhook: checkout.session.completed
310
+ await fg.trackPurchase({
311
+ order: {
312
+ orderId: session.id,
313
+ items, // CartItem[] — same shape as the browser SDK
314
+ subtotal, discountTotal: 0, shippingTotal: 0, taxTotal,
315
+ total: session.amount_total / 100,
316
+ currency: "USD",
317
+ payment: { method: "credit_card", provider: "stripe" },
318
+ },
319
+ userId: session.client_reference_id,
320
+ });
321
+
322
+ // charge.refunded — keeps net revenue accurate
323
+ await fg.trackRefund({
324
+ orderId, refundId: refund.id,
325
+ amount: refund.amount / 100, currency: "USD",
326
+ reason: "customer_request", isFullRefund: true, userId,
327
+ });
328
+
329
+ // Fulfilment webhooks
330
+ await fg.trackOrderStatus(orderId, "processing", "pending", { userId });
331
+ await fg.trackOrderShipped(orderId, { method: "express", cost: 5, trackingNumber });
332
+ await fg.trackOrderDelivered(orderId);
333
+
334
+ // Subscription / MRR lifecycle — one method, discriminated on `event`:
335
+ await fg.trackSubscription({
336
+ event: "start", // "change" | "cancel" | "renew" | "trial_converted" | "trial_expired"
337
+ subscriptionId: sub.id,
338
+ userId,
339
+ plan: "professional",
340
+ mrr: { amount: 9900, currency: "USD" }, // smallest currency unit
341
+ billingCycle: "monthly",
342
+ trialDays: 14,
343
+ });
344
+ ```
345
+
346
+ **Revenue attribution** — payment webhooks arrive with no cookies, so the
347
+ visitor/session ids ride through the provider's checkout metadata and come
348
+ back out in the webhook. Two legs:
349
+
350
+ ```ts
351
+ // LEG 1 — checkout creation: embed the ids from the request cookies.
352
+ import { checkoutAttributionFromCookies } from "flowgrid-sdk/server";
353
+ import { cookies } from "next/headers";
354
+
355
+ // Stripe
356
+ const session = await stripe.checkout.sessions.create({
357
+ // …
358
+ metadata: { ...checkoutAttributionFromCookies(await cookies()) },
359
+ });
360
+
361
+ // Lemon Squeezy
362
+ const checkout = await createCheckout(storeId, variantId, {
363
+ checkoutData: { custom: { ...checkoutAttributionFromCookies(await cookies()) } },
364
+ });
365
+
366
+ // (Creating the checkout client-side instead? Use fg.checkoutAttribution()
367
+ // from the browser SDK — same keys.)
368
+ ```
369
+
370
+ ```ts
371
+ // LEG 2 — webhook: read the ids back and attach them to the revenue event.
372
+ import { attributionFromMetadata } from "flowgrid-sdk/server";
373
+
374
+ // Stripe checkout.session.completed — unwraps `.metadata` automatically
375
+ const { visitorId, sessionId } = attributionFromMetadata(event.data.object);
376
+
377
+ // Lemon Squeezy order_created — unwraps `.meta.custom_data` automatically
378
+ // const { visitorId, sessionId } = attributionFromMetadata(payload);
379
+
380
+ await fg.trackPurchase({ order, userId }, { visitorId, sessionId });
381
+ ```
382
+
383
+ With the real `visitorId` + `sessionId` on the purchase, revenue joins the
384
+ visitor's full journey — the session, landing page and UTM campaign that
385
+ earned it — instead of a synthetic server visitor.
386
+
387
+ **API + server-side error tracking** — errors become `error_event`s with
388
+ route/method/status context; FlowGrid-internal noise is filtered out:
389
+
390
+ ```ts
391
+ try { … } catch (err) {
392
+ await fg.trackError(err, { route: "/api/teams/edit/[teamId]", method: "PATCH", statusCode: 500, userId });
393
+ throw err;
215
394
  }
395
+
396
+ // Or wrap a handler — tracks and rethrows, framework behaviour unchanged:
397
+ export const PATCH = fg.withErrorTracking(handler, { route: "/api/teams/edit/[teamId]", method: "PATCH" });
398
+
399
+ // Opt-in process-level capture (Node only; observes, never swallows):
400
+ const detach = fg.captureUncaught();
216
401
  ```
217
402
 
218
- For Node-style batch jobs, simply create a `FlowGrid` instance, fire your tracking calls, and let the process exit events use `sendBeacon`/`fetch` and a localStorage-style buffer for resilience.
403
+ Like the browser transport, the server client **never throws** for delivery
404
+ problems — every method resolves `{ ok, status?, reason? }`.
219
405
 
220
406
  ---
221
407
 
222
408
  ## Cookie Consent
223
409
 
410
+ ### What you're asking consent for
411
+
412
+ When a visitor accepts cookies, this is exactly what each category permits
413
+ FlowGrid to do (this is what the SDK enforces, not aspirational copy — each
414
+ category maps to a hard gate in the transport).
415
+
416
+ **Default posture: implied consent.** Every category starts granted, and
417
+ tracking runs out of the box — the "declined" behaviour below only applies
418
+ when you've installed a consent gate (`requireExplicitConsent: true` or the
419
+ opt-in banner) and the visitor actively rejects a category:
420
+
421
+ | Category | Can the visitor decline? | What it covers |
422
+ | ------------- | ------------------------ | -------------- |
423
+ | `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. |
424
+ | `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. |
425
+ | `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. |
426
+ | `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. |
427
+
428
+ Independent of the banner, visitors sending **Do-Not-Track** or **Global
429
+ Privacy Control** are treated as having declined non-essential tracking by
430
+ default (`respectDNT: true`), and localhost traffic is never tracked unless
431
+ explicitly enabled.
432
+
433
+ Server-side events (`flowgrid-sdk/server`) are outside the browser consent
434
+ gates by design: they represent your backend's own records (auth events,
435
+ API feature usage, server errors) under your contractual/legitimate-interest
436
+ basis. If you want browser consent to extend to server calls, check your
437
+ stored consent state before calling the server client.
438
+
224
439
  ### No-code (one tag) — recommended
225
440
 
226
441
  Drop a single element on the page and FlowGrid renders a styled, **opt-in**,
@@ -339,6 +554,7 @@ FlowGridTransport.hasConsent("analytics"); // true
339
554
  | `flowgrid-sdk/ecommerce` | Ecommerce modules only |
340
555
  | `flowgrid-sdk/core` | Core modules (activation, experiments, prompts) |
341
556
  | `flowgrid-sdk/consent` | `ConsentManager` + types |
557
+ | `flowgrid-sdk/server` | `FlowGridServer` — Node/edge tracking (identity, feature usage, revenue, errors) |
342
558
  | `flowgrid-sdk/types` | Shared TypeScript type definitions |
343
559
 
344
560
  ---