Skip to content

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.

Terminal window
npm install @canarylytics/sdk
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.

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" });

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_…)"
}
}

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"],
});
Terminal window
npm install @canarylytics/sdk react
import { 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.