@gyne-antpack/tracking-sdk
v1.2.1
Published
Enterprise behavioral tracking SDK for web and Node.js
Maintainers
Readme
@gyne/tracking-sdk
Enterprise behavioral tracking SDK for web and Node.js. Similar to Segment/RudderStack but fully owned and private.
Installation
# npm
npm install @gyne/tracking-sdk
# pnpm
pnpm add @gyne/tracking-sdk
# yarn
yarn add @gyne/tracking-sdkConfigure your .npmrc to pull from GitHub Packages:
@gyne:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKENQuick Start
Vanilla JS / Browser
import { createClient } from "@gyne/tracking-sdk";
const client = await createClient({
writeKey: "wk_your_key_here",
autoSessionTracking: true,
});
// Track an event
await client.track("button_click", { label: "Hero CTA" });
// Identify a user
await client.identify("user_123", {
email: "[email protected]",
plan: "pro",
});
// Track a page view
await client.page("Home Page");Next.js App Router
app/layout.tsx
import { GyneScript } from "@gyne/tracking-sdk/nextjs";
export default function RootLayout({ children }) {
return (
<html>
<body>
<GyneScript config={{ writeKey: process.env.NEXT_PUBLIC_GYNE_WRITE_KEY }}>
{children}
</GyneScript>
</body>
</html>
);
}components/MyButton.tsx
"use client";
import { useTrack } from "@gyne/tracking-sdk/react";
export function MyButton() {
const track = useTrack();
return (
<button onClick={() => track("button_click", { label: "Buy Now" })}>
Buy Now
</button>
);
}Server Components / API Routes
import { createServerClient } from "@gyne/tracking-sdk/nextjs";
const analytics = createServerClient({ writeKey: process.env.GYNE_WRITE_KEY });
// In a Server Action
export async function serverAction() {
await analytics.init();
await analytics.track("server_event", { source: "checkout" });
}API Reference
createClient(config) → Promise<GyneClient>
Creates and initializes a client. Subsequent calls with the same tenantId return the existing instance.
client.track(event, properties?)
await client.track("purchase", {
orderId: "ORD-001",
total: 99.99,
currency: "USD",
});client.identify(userId, traits?)
await client.identify("usr_123", {
email: "[email protected]",
name: "Jane Doe",
plan: "enterprise",
});client.page(name?, properties?)
await client.page("Checkout", {
url: "https://shop.acme.com/checkout",
referrer: "https://shop.acme.com/cart",
});client.reset()
Clears identity, creates a new anonymous ID, and flushes the queue.
client.flush()
Force-flushes the event queue immediately.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| writeKey | string | required | API key |
| apiUrl | string | https://api.gyne.io/v1/batch | Collector endpoint |
| tenantId | string | "" | Multi-tenant identifier |
| debug | boolean | false | Enable console logging |
| flushAt | number | 20 | Events before auto-flush |
| flushInterval | number | 5000 | ms between flushes |
| maxBatchSize | number | 100 | Max events per batch |
| maxQueueSize | number | 1000 | Max queued events |
| maxRetries | number | 3 | Retry limit per event |
| retryBaseDelay | number | 1000 | Exponential backoff base (ms) |
| requireConsent | boolean | false | Gate events on consent |
| autoPageView | boolean | false | Auto-track page views |
| autoSessionTracking | boolean | true | Auto session start/end |
| sessionTimeout | number | 1800000 | Inactivity timeout (ms) |
| cookieDomain | string | "" | Cross-subdomain cookies |
| isServer | boolean | auto | Disable browser APIs |
| plugins | Plugin[] | [] | Plugin array |
| middlewares | MiddlewareFn[] | [] | Middleware array |
| transport | Transport | HTTP+Beacon | Custom transport |
Consent Management (GDPR)
const client = await createClient({
writeKey: "wk_...",
requireConsent: true,
consent: {
categories: {
analytics: false,
marketing: false,
functional: true, // always on
performance: false,
},
},
});
// Later, when user accepts:
client.consent().grant(["analytics", "marketing"]);
// OneTrust integration example:
window.OneTrust?.OnConsentChanged(() => {
const groups = window.OnetrustActiveGroups?.split(",") ?? [];
if (groups.includes("C0002")) client.consent().grant("analytics");
});Plugin System
import type { Plugin, PluginContext } from "@gyne/tracking-sdk";
const myPlugin: Plugin = {
name: "MyPlugin",
version: "1.0",
initialize(config, anonymousId) {
console.log("Plugin initialized with writeKey:", config.writeKey, "anonymousId:", anonymousId);
},
before(ctx: PluginContext): PluginContext {
// Enrich or transform the event before it's sent
return {
...ctx,
event: {
...ctx.event,
properties: {
...(ctx.event.type === "track" ? ctx.event.properties : {}),
app_version: "2.1.0",
},
},
};
},
after(ctx: PluginContext): PluginContext {
// React to events after they're queued (e.g., forward to 3rd party)
return ctx;
},
onConsent(consent) {
console.log("Consent updated:", consent);
},
};
const client = await createClient({
writeKey: "wk_...",
plugins: [myPlugin],
});Autocapture
AutocapturePlugin automatically captures user interactions — clicks, form submissions, and input changes — without any manual instrumentation. Each interaction fires a $autocapture track event with full element metadata.
import { createClient, createAutocapturePlugin } from "@gyne/tracking-sdk";
const client = await createClient({
writeKey: "wk_your_key_here",
plugins: [
createAutocapturePlugin({
capture: ["click", "submit", "change"],
maskInputs: true, // never capture input values (default)
blockSelector: ".private, [data-gyne-ignore]",
captureAttributes: ["data-product-id", "aria-label"],
}),
],
});
// From this point on, all clicks on links, buttons, and form submissions
// are tracked automatically — no client.track() calls needed.Options
| Option | Type | Default | Description |
|---|---|---|---|
| capture | Array<"click"\|"submit"\|"change"> | ["click","submit","change"] | DOM event types to listen for |
| maskInputs | boolean | true | Replace input values with [masked] (recommended for GDPR) |
| blockSelector | string | "[data-gyne-ignore]" | CSS selector — elements matching this are never captured |
| maxTextLength | number | 100 | Truncate element text content to this length |
| captureAttributes | string[] | [] | Extra HTML attributes to include (e.g. data-*, aria-*) |
Emitted properties
Every $autocapture event includes:
| Property | Example |
|---|---|
| $event_type | "click" |
| $el_tag | "button" |
| $el_id | "hero-cta" |
| $el_classes | ["btn-primary","btn-lg"] |
| $el_text | "Buy Now" |
| $el_href | "https://..." (anchors only) |
| $el_name | "email" (inputs only) |
| $el_type | "submit" (inputs only) |
| $el_value | "[masked]" or actual value |
| $css_selector | "main > section > button#hero-cta" |
| $attr_data-product-id | "prod_42" (custom attributes) |
Opting out individual elements
Add data-gyne-ignore to any element (or a container) to exclude it from autocapture:
<div data-gyne-ignore>
<!-- nothing inside here will be captured -->
<input type="text" placeholder="Private field" />
<button>Internal action</button>
</div>GDPR considerations
maskInputs: true(default) ensures no user-typed data ever leaves the browser- Password fields are always masked regardless of
maskInputs - Combine with
requireConsent: trueto hold all events until the user accepts analytics cookies - Use
blockSelectorto exclude sensitive sections of your UI
Segmentation
The SDK can automatically fetch the behavioral segment of the current user and cache it locally, enabling instant personalization without repeated API calls.
How it works
On every page load, the SDK calls the segmentation endpoint with the user's anonymousId. The resolved segment is stored in localStorage under the key gyne_user_segment. Subsequent reads are synchronous and instant — no network involved.
page load → fetch segment (background) → localStorage["gyne_user_segment"] = "interesado"Setup
Add createSegmentationPlugin() to the plugins array when initializing the client:
import { createClient, createSegmentationPlugin } from '@gyne-antpack/tracking-sdk';
const client = await createClient({
writeKey: 'wk_...',
plugins: [createSegmentationPlugin()],
});Reading the segment
getUserSegment() reads the cached value synchronously. It does not call the API.
import { getUserSegment } from '@gyne-antpack/tracking-sdk';
const segment = getUserSegment(); // "interesado" | "explorador" | nullReturns null if the segment hasn't been fetched yet (e.g., first visit before the background call resolves).
Personalization example
const segment = getUserSegment() ?? 'anonymous_user';
if (segment === 'interesado') {
showPricingBanner();
} else if (segment === 'explorador') {
showWelcomeTour();
}Options
createSegmentationPlugin({
// Override the endpoint URL (useful for self-hosted APIs)
segmentationUrl: 'https://your-api.com/v1/segment-lookup',
// Value to store when the lookup fails. Default: "anonymous_user"
defaultSegment: 'anonymous_user',
})You can also set these globally in createClient config:
| Option | Type | Default | Description |
|---|---|---|---|
| segmentationUrl | string | derived from apiUrl | Override segmentation endpoint |
| segmentDefaultValue | string \| null | "anonymous_user" | Fallback when lookup fails |
Storage
| Key | Value | Description |
|---|---|---|
| gyne_user_segment | "interesado" | Current segment key |
| gyne_user_segment_meta | JSON | Response time, success flag, timestamp |
Performance notes
- The API is called once per full page load — never on SPA route changes.
- The fetch runs asynchronously in the background and never blocks rendering.
- Reads via
getUserSegment()are synchronous and instantaneous (localStorage). - The
writeKeyis sent as an internal header — it is never exposed to user-facing code.
A/B Experiments
The SDK can resolve which variant of an experiment the current user should see, and automatically tracks the exposure event.
How it works
client.experiment("key") → GET /v1/experiment-lookup (anonymous_id, experiment_key)
→ sessionStorage["gyne_exp_key"] = result (cached for the tab session)
→ auto-tracks "experiment_viewed"
→ returns { key: "A" | "B", experimentId, content } or null- Resolution happens live on every call — there's no propagation delay between activating an experiment in the dashboard and it being picked up by the SDK.
- Within the same browser session, the same user keeps the same variant (cached in
sessionStorage), soclient.experiment()is safe to call multiple times without re-bucketing the user. - Returns
nullwhen there's no active experiment for that key, the SDK is running server-side, or the user doesn't belong to the experiment's target segment (if one is configured). contentis a free-formRecord<string, string>— whatever fields were set on the variant when the experiment was created (e.g.headline,cta,body).
Usage
The SDK does not auto-inject anything into the DOM — you fetch the variant and apply it yourself.
const result = await client.experiment("hero-cta-julio-2026");
if (result) {
document.querySelector("#hero-headline")!.textContent = result.content.headline;
document.querySelector("#hero-cta")!.textContent = result.content.cta;
document.querySelector("#hero-body")!.textContent = result.content.body;
}
// If result is null, leave the page's default content as-is.React example:
const [variant, setVariant] = useState<ExperimentResult | null>(null);
useEffect(() => {
client.experiment("hero-cta-julio-2026").then(setVariant);
}, []);
return <h1>{variant?.content.headline ?? "Default headline"}</h1>;Tracking the goal event
Exposure (experiment_viewed) is tracked automatically inside client.experiment(). The conversion/goal event is not automatic — fire it yourself with client.track() using the same event name configured as the experiment's goal_event (e.g. purchase, form_completed):
await client.track("purchase", { orderId: "ORD-001", total: 99.99 });API
client.experiment(experimentKey: string): Promise<ExperimentResult | null>
interface ExperimentResult {
key: "A" | "B";
experimentId: string;
content: Record<string, string>;
}Middleware
Middleware receives the event and a next function. Call next(event) to continue,
or call next(null) / omit the call to drop the event.
const client = await createClient({
writeKey: "wk_...",
middlewares: [
// Enrich every event with app version
(event, next) => {
next({ ...event, properties: { ...event.properties, app_version: "2.0" } });
},
// Drop events from internal users
(event, next) => {
if (event.userId?.startsWith("internal_")) return; // drop
next(event);
},
],
});Multi-Tenant
// Tenant A
const clientA = await createClient({ writeKey: "wk_a", tenantId: "tenant_a" });
// Tenant B — separate queue, identity, and session
const clientB = await createClient({ writeKey: "wk_b", tenantId: "tenant_b" });
// Retrieve a client by tenantId elsewhere
import { getClient } from "@gyne/tracking-sdk";
const clientA = getClient("tenant_a");Event Queue & Reliability
The SDK guarantees at-least-once delivery:
- Events are persisted to
localStorageimmediately on enqueue - On page unload, unflushed events are sent via the Beacon API
- Failed batches are retried with exponential backoff + jitter
- On page reload, persisted events resume from where they left off
- When the queue is full, oldest pending events are dropped (backpressure)
Handling millions of events
For high-volume sites:
- Deploy a collector proxy (e.g., a Vercel Edge Function at
/api/gyne/batch) to batch before forwarding to Gyne servers - Set
flushAt: 50andmaxBatchSize: 200to reduce HTTP round-trips - Use
requireConsent: trueto avoid sending events for anonymous/opted-out users - Enable the
PiiScrubberPluginto strip PII before it leaves the browser
Security
- The
writeKeyis a write-only key — it cannot read data from the API - Use HTTPS exclusively (
Secureflag on identity cookies) - The
PiiScrubberPluginstrips emails, credit cards, SSNs, and phone numbers - Set
cookieDomainto restrict cookies to your domain SameSite=Laxcookies by default
Tree Shaking
The SDK is fully tree-shakeable. Import only what you need:
// Only ships the core client — no React adapter
import { createClient } from "@gyne/tracking-sdk";
// Only ships React hooks — no Node.js transport
import { useTrack } from "@gyne/tracking-sdk/react";