@agoodway/goodanalytics-client
v0.1.1
Published
Official GoodAnalytics JavaScript and TypeScript API client.
Readme
GoodAnalytics JavaScript Client
Official TypeScript client for the GoodAnalytics REST API.
Runtime Requirements
- Node.js 18+ with native
fetch - Bun
- Deno
The package is ESM-only and has zero runtime dependencies.
Install
npm install @goodanalytics/clientQuick Start
import { createClient } from "@goodanalytics/client";
const ga = createClient({ apiKey: process.env.GOODANALYTICS_API_KEY! });
const link = await ga.links.create({
domain: "go.example.com",
key: "launch",
url: "https://example.com/launch",
});
console.log(link.id);The default API base URL is https://goodanalytics.dev/ga/api. Pass baseUrl when targeting another deployment or local server.
const ga = createClient({
apiKey: "sk_test_123",
baseUrl: "http://localhost:4009/ga/api",
});Authentication
Use exactly one auth mode per client.
createClient({ apiKey: "sk_test_123" });
createClient({ token: "bearer_abc" });
createClient({ token: async () => await getFreshToken() });Requests with apiKey send X-Api-Key. Requests with token send Authorization: Bearer <token>. Dynamic token callbacks run before every request.
Events
The public types match the API JSON shape, including snake_case fields.
await ga.events.create({
visitor_id: "visitor_123",
event_type: "purchase",
amount_cents: 10000,
currency: "USD",
properties: { plan: "pro" },
});
await ga.events.batch({
events: [
{ person_external_id: "user_123", event_type: "lead" },
{ person_external_id: "user_456", event_type: "custom", event_name: "demo_booked" },
],
});Links
const link = await ga.links.get("link_uuid");
await ga.links.update(link.id, { url: "https://example.com/new" });
const stats = await ga.links.stats(link.id);
await ga.links.archive(link.id);Visitors
const visitor = await ga.visitors.lookup("external_user_123");
const timeline = await ga.visitors.timeline(visitor.id);
const attribution = await ga.visitors.attribution(visitor.id);Server-Side Visitor Identity
The 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.
| Cookie | Name | Contents |
|--------|------|----------|
| Identity | _ga_good | Attribution cookie (ga_id) set after a tracked link click or redirect |
| Anonymous | _ga_anon | Client-generated anonymous ID set on first page view |
The 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.
Visitor resolution priority: visitor_id > person_external_id > ga_id > anonymous_id.
Anonymous form submission
When you don't know who the user is yet, read the tracking cookies and pass them as identity signals:
import { createClient } from "@goodanalytics/client";
const ga = createClient({ apiKey: process.env.GOODANALYTICS_API_KEY! });
export function trackFormSubmitted(
request: Request,
formName: string,
email: string,
properties?: Record<string, unknown>,
) {
const cookies = parseCookies(request.headers.get("cookie") || "");
const gaId = cookies["_ga_good"];
const anonId = cookies["_ga_anon"];
ga.events.create({
...(gaId ? { ga_id: gaId } : anonId ? { anonymous_id: anonId } : {}),
person_external_id: email,
event_type: "custom",
event_name: "form_submitted",
properties: { form_name: formName, ...properties },
}).catch((err) => {
console.warn(`GoodAnalytics event failed for ${formName}:`, err);
});
}
function parseCookies(header: string): Record<string, string> {
const cookies: Record<string, string> = {};
for (const pair of header.split(";")) {
const idx = pair.indexOf("=");
if (idx === -1) continue;
const key = pair.slice(0, idx).trim();
const value = pair.slice(idx + 1).trim();
cookies[key] = decodeURIComponent(value);
}
return cookies;
}This works in any server environment that receives a standard Request object (Next.js API routes, Remix loaders, Astro endpoints, Cloudflare Workers, etc.).
Pagination
Single-page list methods return arrays.
const links = await ga.links.list({ limit: 50, offset: 0 });
const visitors = await ga.visitors.list({ limit: 20 });Auto-pagination helpers return async iterators. Limits above the API max of 200 are clamped before requests and offset increments.
for await (const link of ga.links.listAll({ limit: 100 })) {
console.log(link.id);
}
for await (const click of ga.links.clicksAll("link_uuid", { limit: 100 })) {
console.log(click.visitor_id);
}Error Handling
Non-2xx responses throw GoodAnalyticsError.
import { GoodAnalyticsError } from "@goodanalytics/client";
try {
await ga.links.get("missing");
} catch (error) {
if (error instanceof GoodAnalyticsError) {
console.error(error.status, error.message, error.errors);
}
}The 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.
