{"_id":"@brandkarma/tracker","name":"@brandkarma/tracker","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@brandkarma/tracker","version":"0.1.0","description":"Cookieless web analytics and attribution SDK for BrandKarma. Image-beacon transport, no cookies, no device storage. Framework-free core plus React and Next.js App Router bindings.","type":"module","license":"MIT","homepage":"https://getbrandkarma.com","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./next":{"types":"./dist/next/index.d.ts","default":"./dist/next/index.js"}},"sideEffects":false,"publishConfig":{"access":"public"},"scripts":{"build":"tsc && node scripts/check-use-client.mjs","test":"vitest run","prepack":"pnpm build"},"peerDependencies":{"next":">=14","react":">=18"},"peerDependenciesMeta":{"next":{"optional":true},"react":{"optional":true}},"devDependencies":{"@types/react":"^19.1.0","jsdom":"^26.1.0","typescript":"^5.9.2","vitest":"^3.2.4"},"keywords":["analytics","attribution","cookieless","brandkarma","tracking","beacon","react","nextjs"],"_id":"@brandkarma/tracker@0.1.0","_nodeVersion":"25.6.1","_npmVersion":"11.9.0","dist":{"integrity":"sha512-st8N2JJQYSE+KY4gTmrfkV0AR6scYlXHYy8op6qcPt7SpE9AVhSJIsqlOkS5AO+tRB93Ny5pCSkRbgyI88IHnA==","shasum":"717a0cf90abab6b736fff37d00080ec534cbee05","tarball":"https://registry.npmjs.org/@brandkarma/tracker/-/tracker-0.1.0.tgz","fileCount":17,"unpackedSize":51401,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEsrl0HXYptKL9J726FCXxrT/F7E9tBa6nYyTPxtrpTqAiBCX07bMwfmOhx2hJgNjbB97WMf16mX82j7Gqvksj6fLQ=="}]},"_npmUser":{"name":"autarky","email":"npm@autarky.dev"},"directories":{},"maintainers":[{"name":"autarky","email":"npm@autarky.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/tracker_0.1.0_1786879424722_0.04472040212527362"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-16T11:23:44.592Z","0.1.0":"2026-08-16T11:23:44.868Z","modified":"2026-08-16T11:23:45.085Z"},"maintainers":[{"name":"autarky","email":"npm@autarky.dev"}],"description":"Cookieless web analytics and attribution SDK for BrandKarma. Image-beacon transport, no cookies, no device storage. Framework-free core plus React and Next.js App Router bindings.","homepage":"https://getbrandkarma.com","keywords":["analytics","attribution","cookieless","brandkarma","tracking","beacon","react","nextjs"],"license":"MIT","readme":"# @brandkarma/tracker\n\nCookieless web analytics and attribution SDK for [BrandKarma](https://getbrandkarma.com).\nImage-beacon transport, no cookies, no device storage of any kind. Framework-free core with\nReact and Next.js App Router bindings.\n\n- `@brandkarma/tracker` — core, framework-free, valid Node ESM (usable anywhere)\n- `@brandkarma/tracker/react` — generic React client components (optional peer: `react >= 18`)\n- `@brandkarma/tracker/next` — Next.js App Router bindings, everything from `/react` plus\n  `Analytics` (optional peers: `react`, `next >= 14`)\n\nESM only.\n\n## Install\n\n```bash\npnpm add @brandkarma/tracker\n# or: npm install @brandkarma/tracker\n```\n\n## Quickstart (Next.js App Router)\n\n**1. Define your site's event taxonomy** (see [Event taxonomy](#event-taxonomy) below):\n\n```ts\n// lib/events.ts\nexport const EVENTS = {\n  PAGEVIEW: \"pageview\", // REQUIRED literal — the server's pageview metrics match it exactly\n  CTA_CLICK: \"cta_click\",\n  CONTACT_CLICK: \"contact_click\",\n} as const;\n\nexport type EventName = (typeof EVENTS)[keyof typeof EVENTS];\n```\n\n**2. Configure the tracker ONCE in a single `\"use client\"` module:**\n\n```ts\n// lib/tracker.ts\n\"use client\";\nimport { createBrandKarma } from \"@brandkarma/tracker/next\";\nimport type { EventName } from \"./events\";\n\nexport const { track, withAttribution, getAttribution, Analytics, TrackedOutboundLink, ContactLink, TrackView } =\n  createBrandKarma<EventName>({\n    projectId: \"<your BrandKarma Brand id>\",\n    canonicalOrigin: \"https://www.example.com\",\n  });\n```\n\n**3. Mount `Analytics` once in your root (or locale) layout,** inside your providers, after\n`{children}`:\n\n```tsx\n// app/[locale]/layout.tsx (server component)\nimport { Analytics } from \"@/lib/tracker\";\n\n// ... inside the provider tree, after {children}:\n<Analytics />\n```\n\n`Analytics` fires a `pageview` on mount and on every client-side route change. It reads the\nroute via `usePathname()` and deliberately never calls `useSearchParams()` — that would opt\nevery page out of static rendering. It also guards against React StrictMode's double effect\ninvocation, so no duplicate pageviews land in your data.\n\n**4. Track from anywhere:**\n\n```tsx\n// In a SERVER component: render the components (they are client references).\n<TrackView event=\"pricing_view\" />\n<TrackedOutboundLink href=\"https://app.example.com/auth/register\" event=\"cta_click\" location=\"hero\">\n  Start free\n</TrackedOutboundLink>\n<ContactLink href=\"mailto:hello@example.com\" event=\"contact_click\" location=\"footer\">\n  hello@example.com\n</ContactLink>\n\n// In a CLIENT component: call the functions.\ntrack(\"cta_click\", { location: \"navbar\" });\n```\n\n### Server components vs client components\n\nThe **component** exports (`Analytics`, `TrackView`, `TrackedOutboundLink`, `ContactLink`)\nare client references — render them directly from server components; they need no wrapper.\n\nThe **function** exports (`track`, `withAttribution`, `captureAttribution`, `getAttribution`)\nare import-legal everywhere but **callable only inside client components**. Calling `track()`\nfrom a server component fails the build with \"It's not possible to invoke a client function\nfrom the server.\" If a server page needs to record a view, render `<TrackView />` instead of\ncalling `track()`.\n\n### Plain React (no Next.js)\n\nUse `@brandkarma/tracker/react` — the same factory without `Analytics` (which depends on\nNext's router). Fire pageviews yourself from your router of choice, with your own\n\"did the path actually change\" guard.\n\n### No framework\n\nUse the core directly:\n\n```ts\nimport { createTracker } from \"@brandkarma/tracker\";\n\nconst tracker = createTracker({ projectId: \"...\", canonicalOrigin: \"https://www.example.com\" });\ntracker.captureAttribution(); // on landing\ntracker.track(\"pageview\");\n```\n\n## Getting a projectId (it's a Brand id)\n\n`projectId` **is your BrandKarma Brand id** — the id of the Brand document, shown in the\nBrandKarma app under *Brand Details → Tracking Pixel*. It is public by design (it ships in\nevery beacon URL); the read side of the analytics is what's protected.\n\n**Warning: unknown or mistyped ids fail SILENTLY.** The tracking endpoint accepts any\nprojectId without validation — events for a wrong id are stored but unreadable by anyone.\nAfter setup, always verify that events appear in your BrandKarma dashboard before trusting\nthe integration.\n\n## Event taxonomy\n\nThe SDK is generic over your event names (`createBrandKarma<EventName>(...)`); the taxonomy\nitself stays in your site's code, not in the SDK. Conventions that work well:\n\n- A single `lib/events.ts` with a const map + derived union type (see Quickstart).\n- `pageview` must be the literal string `\"pageview\"` — server-side pageview metrics compare\n  it exactly.\n- Name events by intent (`cta_click`, `contact_click`, `tool_use`, `signup`), and pass the\n  *placement* as a `location` prop (`hero`, `navbar`, `pricing:not-sure`) so one event can be\n  compared across placements.\n- If you assign monetary values to conversions, keep a `CONVERSION_VALUES` map next to the\n  taxonomy in the same file.\n\n## Privacy posture (cookieless, no device storage)\n\nThis SDK **never** uses cookies, localStorage, sessionStorage, or any other terminal-device\nstorage — reading or writing. All three count as terminal-device storage under §25 TDDDG\n(and equivalent EU consent regimes) and would require an opt-in banner. An in-memory\nvariable does not. This is a load-bearing product promise, not an implementation detail.\n\nHow it works instead:\n\n- **Attribution** is captured once per JS context from the landing URL and held in memory.\n  It survives client-side navigation (SPA routers keep one JS context) but is lost on a hard\n  reload — an accepted trade-off, since the realistic ad journey (land with `?gclid` →\n  browse → click the CTA) stays within one context.\n- **Sessions and unique visitors** are stitched server-side by BrandKarma from a\n  daily-salted hash of user agent + IP, held in memory for a 30-minute sliding window. The\n  salt is random per server process and never persisted; IP addresses are **never stored**.\n  Visitors cannot be joined across days or server restarts.\n- The SDK sends **no session IDs, no visitor IDs, no timestamps** — such parameters don't\n  exist on the server. Do not batch or delay events either: the server timestamps at\n  receipt, so deferral skews sessionization.\n\n## Attribution capture and click-ID semantics\n\nCaptured parameters (exactly the server's binding list, exported as `ATTRIBUTION_PARAMS`):\n\n| Group | Params | Meaning |\n|---|---|---|\n| UTM | `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `utm_id` | Campaign labeling. A paid-style `utm_medium` (cpc, ppc, paid…) classifies traffic as paid. |\n| Paid click IDs | `gclid`, `gbraid`, `wbraid`, `msclkid` | Google/Microsoft Ads. Their presence classifies the session as **paid** and they are the keys for ad-platform conversion import. |\n| Organic-capable click IDs | `li_fat_id`, `fbclid`, `ttclid` | LinkedIn/Meta/TikTok. Captured and stored, but deliberately **not** treated as paid evidence (they appear on organic shares too). |\n\nSemantics:\n\n- **Capture-once, first-non-empty-wins.** The landing-page capture is kept for the life of\n  the JS context and re-sent with every event, so attribution survives navigation to URLs\n  that no longer carry the params. Explicit beacon params outrank the server's fallback\n  parsing of the page URL. An empty capture does not lock; a later navigation carrying\n  params can still be captured.\n- **`?ref=` shorthand** (common on inbound links) is mapped client-side to `utm_source` when\n  no `utm_source` is present; `ref` itself is never sent.\n- **Values** are trimmed and truncated to 512 chars (matching the server cap).\n- **`withAttribution(url)`** appends the captured params to an outbound URL — use it (or\n  `TrackedOutboundLink`, which does it for you) to carry click IDs across a domain boundary\n  to your app, where they can be stored server-side against the account and conversions\n  imported back to the ad platform without any ad cookie. Params already on the target URL\n  are never overwritten. During SSR it returns the URL unchanged (`TrackedOutboundLink`\n  server-renders the clean href and upgrades after hydration).\n- **`referrer`** is sent as an explicit query param (from `document.referrer`, only when\n  non-empty) because the HTTP Referer header of an image beacon identifies the page the\n  pixel is on, not where the visitor came from. Traffic-source classification\n  (paid/search/llm/social/referral/direct) is derived server-side, first-touch per session.\n\n## Custom props: contract and limits\n\nThe optional second argument to `track(event, props)`:\n\n```ts\ntrack(\"tool_use\", { tool: \"gtin-validator\" });\ntrack(\"cta_click\", { location: \"hero\", destination: \"/auth/register\" });\n```\n\n- Type: `Record<string, string | number | boolean | undefined>`.\n- On the wire each prop becomes a `p_`-prefixed query param (`p_tool=gtin-validator`), which\n  is what the server recognizes as a custom prop. The prefix makes collisions with reserved\n  params (`projectId`, `event`, `url`, `referrer`, attribution params, …) impossible.\n- **Keys** must match `^[A-Za-z0-9_-]{1,64}$`. Non-conforming keys (dots, `$`, spaces,\n  empty, over-long) are dropped silently — `track` never throws.\n- **Values** are stringified and truncated to 512 chars. `undefined`, `null` and `\"\"` values\n  are skipped. `false` and `0` are sent.\n- **Max 20 props** per event; extras are dropped.\n- Props are the first thing shed if the beacon URL approaches the server's 8 KB request-line\n  limit (see below).\n\n## Transport and limits\n\n- Transport is a 1×1 image GET: immune to CORS, stays in flight across page unload (matters\n  for outbound clicks), can't read a response. Fire-and-forget — there is no delivery\n  confirmation and no retry, and `track()` is a silent no-op during SSR.\n- A `_t` cache-buster is appended (the server sets no Cache-Control on the pixel), unique\n  even for identical events fired in the same millisecond.\n- The server silently drops request lines over 8 KB, so the SDK measures the final encoded\n  URL and sheds in order until it fits: custom props → page-URL query string →\n  `utm_term`/`utm_content` → referrer path → remaining non-paid attribution params. The paid\n  click IDs (`gclid`/`gbraid`/`wbraid`/`msclkid`) are last to go and in practice never\n  dropped.\n\n## API notes\n\n- **Single tracker per page.** Attribution capture state is module-level and shared;\n  configure exactly one tracker per site (the factory-module pattern above enforces this\n  naturally).\n- `resetAttribution()` (core export) clears the captured attribution. **Test-only** — it\n  exists because capture-once semantics are otherwise untestable. Never call it in\n  production code.\n- `createTracker` (core) returns plain closures — destructuring is safe, nothing depends on\n  `this`.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-9a96ac0cd45c561206dd8cda26789465"}