SDK
@canarylytics/sdk is a typed, isomorphic client for the REST API. It only needs a global fetch, so it runs unmodified in browsers, Node ≥ 18, Cloudflare Workers, and an Electron main process. Every wire type ships in the package itself (ProjectInfo, StatsOverview, SessionDetail, …), so responses are fully typed.
npm install @canarylytics/sdkCreate a client
Section titled “Create a client”import { createClient } from "@canarylytics/sdk";
const lytics = createClient({ apiKey: process.env.CANARYLYTICS_KEY!, // cly_live_… or cly_read_… baseUrl: "https://api.analytics.canarycoders.es", // optional, this is the default});createClient throws immediately if apiKey is missing, and again at call time if no global fetch is available and you didn’t pass one via config.fetch.
Call the API
Section titled “Call the API”Every method maps one-to-one onto a REST endpoint:
await lytics.project.info();
await lytics.stats.overview({ window: "7d" });await lytics.stats.timeseries({ window: "30d" });await lytics.stats.pages({ window: "7d" });await lytics.stats.sources({ window: "7d" });await lytics.stats.countries({ window: "7d" });await lytics.stats.regions({ window: "7d", country: "ES" });await lytics.stats.events({ window: "7d" });await lytics.stats.utm({ window: "7d" });await lytics.stats.devices({ window: "7d" });await lytics.stats.goals({ window: "7d" });await lytics.stats.realtime();
await lytics.sessions.list({ window: "24h", limit: 50 });await lytics.sessions.get(sessionId); // null on 404, doesn't throw
await lytics.validate({ domain: "myapp.com", origin: "https://myapp.com" });await lytics.exportEvents({ window: "7d", format: "ndjson" });
await lytics.events.track("signup", { plan: "pro" }, { path: "/onboarding" });Handle errors
Section titled “Handle errors”Every non-2xx response throws a typed LyticsApiError:
import { LyticsApiError } from "@canarylytics/sdk";
try { await lytics.events.track("signup");} catch (err) { if (err instanceof LyticsApiError) { // err.status (e.g. 403), err.code (e.g. "read_only_key"), err.message // 401 → code "unauthorized", message // "A valid API key is required (Authorization: Bearer cly_live_…)" }}Provision a project
Section titled “Provision a project”Creating a project happens before any key exists, so it is a standalone function rather than a client method:
import { provisionProject } from "@canarylytics/sdk";
const { apiKey, project } = await provisionProject("https://api.analytics.canarycoders.es", { inviteCode: "...", name: "My App", domains: ["myapp.com"],});React hooks
Section titled “React hooks”npm install @canarylytics/sdk reactimport { useLyticsClient, useOverview, useRealtime } from "@canarylytics/sdk/react";
function Dashboard() { const client = useLyticsClient({ apiKey: readOnlyKey }); const overview = useOverview(client, { window: "7d", refreshInterval: 5000 }); const live = useRealtime(client, { refreshInterval: 2000 });
if (overview.isLoading) return <p>Loading…</p>; if (overview.error) return <p>{overview.error.message}</p>; return <p>{overview.data?.totals.sessions} sessions · {live.data?.activeSessions} live</p>;}Every hook (useOverview, useTimeseries, useTopPages, useSources, useCountries, useCustomEvents, useUtm, useDevices, useGoals, useRealtime, useSessions) returns { data, error, isLoading, refresh } and accepts the matching stats query plus refreshInterval (milliseconds; omit or pass 0 to fetch once). Polling pauses while the tab is hidden and refetches immediately when it becomes visible again, so data doesn’t go stale in a backgrounded tab.
react >= 18 is an optional peer dependency, needed only if you import from the /react subpath.