{"_id":"@agoodway/goodanalytics-client","_rev":"2-a300d0070a521442a3e8b76463393632","name":"@agoodway/goodanalytics-client","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@agoodway/goodanalytics-client","version":"0.1.0","license":"MIT","_id":"@agoodway/goodanalytics-client@0.1.0","maintainers":[{"name":"thomasgoodway","email":"thomas@goodway.dev"}],"dist":{"shasum":"35c403e37d41ecb84d02f4a6f98abd290321dacc","tarball":"https://registry.npmjs.org/@agoodway/goodanalytics-client/-/goodanalytics-client-0.1.0.tgz","fileCount":19,"integrity":"sha512-+syW/nU+ypS/dlDTlyyUEiP1NNoxo4ZaxHlpiAwgVXfjoJXpXN/KZVMxoQol5ufJBOF0SOZZvkJ+UwqRAwQZ8w==","signatures":[{"sig":"MEYCIQCtx5MDoAwzQnEP9/+l1fv0PVWpLWJLFYxL9B6FT08WwgIhALZQ0XtwNc83xCmnTWTFqfam2AsTLqKeKiOggxAIk0Nh","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":29076},"type":"module","types":"./dist/index.d.ts","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"209f91c1e3e8c9b3f3360ef6d9a9e4e5454648de","scripts":{"test":"bun test","build":"bun run clean && bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external && tsc --emitDeclarationOnly","clean":"rm -rf dist","prepack":"bun run build","typecheck":"tsc --noEmit"},"_npmUser":{"name":"thomasgoodway","email":"thomas@goodway.dev"},"_npmVersion":"11.12.1","description":"Official GoodAnalytics JavaScript and TypeScript API client.","directories":{},"sideEffects":false,"_nodeVersion":"25.9.0","dependencies":{},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/goodanalytics-client_0.1.0_1778691016508_0.9538460502649868","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@agoodway/goodanalytics-client","version":"0.1.1","description":"Official GoodAnalytics JavaScript and TypeScript API client.","type":"module","license":"MIT","sideEffects":false,"engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"types":"./dist/index.d.ts","scripts":{"build":"bun run clean && bun build ./src/index.ts --outdir ./dist --target node --format esm --packages external && tsc --emitDeclarationOnly","clean":"rm -rf dist","test":"bun test","typecheck":"tsc --noEmit","prepack":"bun run build"},"dependencies":{},"devDependencies":{"typescript":"^5.9.3"},"gitHead":"209f91c1e3e8c9b3f3360ef6d9a9e4e5454648de","_id":"@agoodway/goodanalytics-client@0.1.1","_nodeVersion":"25.9.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-Amn3LaEyGqy6j5i+kF8zj5+3dw1O4Qx4iu2Fm5xD4hmiGDnzMXcp7DdsxStmVXsVjKqCEM1i4Mnjg+sSGeJNUQ==","shasum":"ffcec5284aaeeb9e19ff472cd5d640f4be2c8c2a","tarball":"https://registry.npmjs.org/@agoodway/goodanalytics-client/-/goodanalytics-client-0.1.1.tgz","fileCount":19,"unpackedSize":31652,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCx8loJMzThQ6ocAN+xR/qdluTbhgVUVn2tFWOrNNX2gAIhAJENf4bCKUE7S/QgXJhUNkWRMSAehQE5bsLwEv2YPzI5"}]},"_npmUser":{"name":"thomasgoodway","email":"thomas@goodway.dev"},"directories":{},"maintainers":[{"name":"thomasgoodway","email":"thomas@goodway.dev"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/goodanalytics-client_0.1.1_1778711716167_0.3368392956528814"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-13T16:50:16.353Z","modified":"2026-05-13T22:35:16.473Z","0.1.0":"2026-05-13T16:50:16.650Z","0.1.1":"2026-05-13T22:35:16.341Z"},"license":"MIT","description":"Official GoodAnalytics JavaScript and TypeScript API client.","maintainers":[{"name":"thomasgoodway","email":"thomas@goodway.dev"}],"readme":"# GoodAnalytics JavaScript Client\n\nOfficial TypeScript client for the GoodAnalytics REST API.\n\n## Runtime Requirements\n\n- Node.js 18+ with native `fetch`\n- Bun\n- Deno\n\nThe package is ESM-only and has zero runtime dependencies.\n\n## Install\n\n```sh\nnpm install @goodanalytics/client\n```\n\n## Quick Start\n\n```ts\nimport { createClient } from \"@goodanalytics/client\";\n\nconst ga = createClient({ apiKey: process.env.GOODANALYTICS_API_KEY! });\n\nconst link = await ga.links.create({\n  domain: \"go.example.com\",\n  key: \"launch\",\n  url: \"https://example.com/launch\",\n});\n\nconsole.log(link.id);\n```\n\nThe default API base URL is `https://goodanalytics.dev/ga/api`. Pass `baseUrl` when targeting another deployment or local server.\n\n```ts\nconst ga = createClient({\n  apiKey: \"sk_test_123\",\n  baseUrl: \"http://localhost:4009/ga/api\",\n});\n```\n\n## Authentication\n\nUse exactly one auth mode per client.\n\n```ts\ncreateClient({ apiKey: \"sk_test_123\" });\ncreateClient({ token: \"bearer_abc\" });\ncreateClient({ token: async () => await getFreshToken() });\n```\n\nRequests with `apiKey` send `X-Api-Key`. Requests with `token` send `Authorization: Bearer <token>`. Dynamic token callbacks run before every request.\n\n## Events\n\nThe public types match the API JSON shape, including snake_case fields.\n\n```ts\nawait ga.events.create({\n  visitor_id: \"visitor_123\",\n  event_type: \"purchase\",\n  amount_cents: 10000,\n  currency: \"USD\",\n  properties: { plan: \"pro\" },\n});\n\nawait ga.events.batch({\n  events: [\n    { person_external_id: \"user_123\", event_type: \"lead\" },\n    { person_external_id: \"user_456\", event_type: \"custom\", event_name: \"demo_booked\" },\n  ],\n});\n```\n\n## Links\n\n```ts\nconst link = await ga.links.get(\"link_uuid\");\n\nawait ga.links.update(link.id, { url: \"https://example.com/new\" });\n\nconst stats = await ga.links.stats(link.id);\n\nawait ga.links.archive(link.id);\n```\n\n## Visitors\n\n```ts\nconst visitor = await ga.visitors.lookup(\"external_user_123\");\nconst timeline = await ga.visitors.timeline(visitor.id);\nconst attribution = await ga.visitors.attribution(visitor.id);\n```\n\n## Server-Side Visitor Identity\n\nThe GoodAnalytics frontend tracker sets first-party cookies that your server can read to connect browser visitors with backend events. This is useful for tying form submissions, signups, or purchases back to the visitor who triggered them — even before the visitor is identified.\n\n| Cookie | Name | Contents |\n|--------|------|----------|\n| Identity | `_ga_good` | Attribution cookie (`ga_id`) set after a tracked link click or redirect |\n| Anonymous | `_ga_anon` | Client-generated anonymous ID set on first page view |\n\nThe events API accepts these cookie values directly as `ga_id` and `anonymous_id` fields. It resolves them to the matching visitor using the same identity resolution the tracking pixel uses.\n\nVisitor resolution priority: `visitor_id` > `person_external_id` > `ga_id` > `anonymous_id`.\n\n### Anonymous form submission\n\nWhen you don't know who the user is yet, read the tracking cookies and pass them as identity signals:\n\n```ts\nimport { createClient } from \"@goodanalytics/client\";\n\nconst ga = createClient({ apiKey: process.env.GOODANALYTICS_API_KEY! });\n\nexport function trackFormSubmitted(\n  request: Request,\n  formName: string,\n  email: string,\n  properties?: Record<string, unknown>,\n) {\n  const cookies = parseCookies(request.headers.get(\"cookie\") || \"\");\n  const gaId = cookies[\"_ga_good\"];\n  const anonId = cookies[\"_ga_anon\"];\n\n  ga.events.create({\n    ...(gaId ? { ga_id: gaId } : anonId ? { anonymous_id: anonId } : {}),\n    person_external_id: email,\n    event_type: \"custom\",\n    event_name: \"form_submitted\",\n    properties: { form_name: formName, ...properties },\n  }).catch((err) => {\n    console.warn(`GoodAnalytics event failed for ${formName}:`, err);\n  });\n}\n\nfunction parseCookies(header: string): Record<string, string> {\n  const cookies: Record<string, string> = {};\n  for (const pair of header.split(\";\")) {\n    const idx = pair.indexOf(\"=\");\n    if (idx === -1) continue;\n    const key = pair.slice(0, idx).trim();\n    const value = pair.slice(idx + 1).trim();\n    cookies[key] = decodeURIComponent(value);\n  }\n  return cookies;\n}\n```\n\nThis works in any server environment that receives a standard `Request` object (Next.js API routes, Remix loaders, Astro endpoints, Cloudflare Workers, etc.).\n\n## Pagination\n\nSingle-page list methods return arrays.\n\n```ts\nconst links = await ga.links.list({ limit: 50, offset: 0 });\nconst visitors = await ga.visitors.list({ limit: 20 });\n```\n\nAuto-pagination helpers return async iterators. Limits above the API max of 200 are clamped before requests and offset increments.\n\n```ts\nfor await (const link of ga.links.listAll({ limit: 100 })) {\n  console.log(link.id);\n}\n\nfor await (const click of ga.links.clicksAll(\"link_uuid\", { limit: 100 })) {\n  console.log(click.visitor_id);\n}\n```\n\n## Error Handling\n\nNon-2xx responses throw `GoodAnalyticsError`.\n\n```ts\nimport { GoodAnalyticsError } from \"@goodanalytics/client\";\n\ntry {\n  await ga.links.get(\"missing\");\n} catch (error) {\n  if (error instanceof GoodAnalyticsError) {\n    console.error(error.status, error.message, error.errors);\n  }\n}\n```\n\nThe client retries once on 5xx responses and network errors after a 500ms delay. It does not retry 4xx responses. Network errors are re-thrown as native errors after retry exhaustion.\n","readmeFilename":"README.md"}