Svelte
Svelte with Vite, and SvelteKit with a same-origin proxy route and server-side tracking.
There is no Svelte adapter. The core client is created once in a module, and the default pageviews plugin follows client-side navigation, which in SvelteKit uses pushState.
Install
npm install @spoar/sdk@nextEvents
src/lib/events.ts:
import type { NoProps } from "@spoar/sdk";
export type Events = {
signup: { plan: "free" | "pro" };
newsletter_subscribed: NoProps;
};Svelte with Vite
.env holds the browser options, with the public key only:
VITE_RA_CONFIG='{"project":"example.com","key":"pk_...","endpoint":"https://api.example.com/v2/events"}'src/lib/analytics.ts:
import { createAnalytics } from "@spoar/sdk";
import { errors, speedInsights } from "@spoar/sdk/plugins";
import type { Events } from "./events";
export const analytics = createAnalytics<Events>({
plugins: [speedInsights(), errors()],
});Import it from src/main.ts so it starts with the app. A static build sends to the API directly; when a server on the same origin runs createProxy, use /_ra instead (see Server).
SvelteKit
.env:
PUBLIC_RA_KEY=pk_...
RA_SECRET=sk_...
RA_ENDPOINT=https://api.example.comRA_SECRET has no PUBLIC_ prefix, so SvelteKit keeps it out of browser code. These names are used on this page only; the SDK does not read them itself.
src/lib/analytics.ts:
import { PUBLIC_RA_KEY } from "$env/static/public";
import { createAnalytics } from "@spoar/sdk";
import { errors, speedInsights } from "@spoar/sdk/plugins";
import type { Events } from "./events";
export const analytics = createAnalytics<Events>({
project: "example.com",
key: PUBLIC_RA_KEY,
endpoint: "/ra",
pageviews: false,
plugins: [speedInsights(), errors()],
});The client starts itself only where window exists, so this module is safe to import during server rendering and starts once in the browser.
src/routes/+layout.svelte reports each navigation with SvelteKit's route id, such as /blog/[slug], as the route template:
<script lang="ts">
import { afterNavigate } from "$app/navigation";
import { analytics } from "$lib/analytics";
let { children } = $props();
afterNavigate((navigation) => {
analytics.route(navigation.to?.route.id ?? null);
analytics.page();
});
</script>
{@render children()}afterNavigate runs after the first render and after every navigation, which is why the client is created with pageviews: false. Without this layout code, leave pageviews on and each pageview carries the path only.
Proxy route
src/routes/ra/+server.ts serves /ra on your own domain, so ad blockers see a first-party request:
import { RA_ENDPOINT, RA_SECRET } from "$env/static/private";
import { createProxy } from "@spoar/sdk/proxy";
import type { RequestHandler } from "./$types";
const proxy = createProxy({ secret: RA_SECRET, endpoint: RA_ENDPOINT });
export const POST: RequestHandler = ({ request }) => proxy(request);Any same-origin path works; the client's endpoint has to match it. See Proxy.
Server-side tracking
src/lib/server/analytics.ts:
import { RA_ENDPOINT, RA_SECRET } from "$env/static/private";
import { createServerAnalytics } from "@spoar/sdk/server";
import type { Events } from "$lib/events";
export const serverAnalytics = createServerAnalytics<Events>({
secret: RA_SECRET,
endpoint: RA_ENDPOINT,
});In a form action or +server.ts, pass the incoming request so the visitor's IP and user agent are forwarded:
import { serverAnalytics } from "$lib/server/analytics";
import type { Actions } from "./$types";
export const actions = {
default: async ({ request }) => {
await serverAnalytics.track("newsletter_subscribed", {}, { request });
},
} satisfies Actions;Every method resolves to { ok, error, accepted, duplicates, failed } and never throws. See Server.
Custom events
<script lang="ts">
import { analytics } from "$lib/analytics";
</script>
<button onclick={() => analytics.track("signup", { plan: "pro" })}>Upgrade</button>The clicks() plugin sends click events from data-ra-click attributes without code; see Plugins.
Consent and identity
analytics.consent.grant();
analytics.identify("user_123", { plan: "pro" });
analytics.reset();With consent: "required", events are held until consent.grant(). Call identify after sign-in and reset on logout. See Consent and opt-out.