Install

JavaScript SDK

The npm package for web apps, React Native, Electron and Node servers.

@anyanalytics/analytics is one package for every JavaScript runtime: browsers, React Native, Electron and Node servers. It batches events, keeps them while the device is offline, retries with backoff, and a retried event is never counted twice. It ships with TypeScript types.

terminalsh
npm install @anyanalytics/analytics

Web apps

For React, Next.js, Vue, Svelte or any app with a build step. Import it once on the client: main.tsx (Vite/React), app/layout.tsx in a client component (Next.js), or main.ts (Vue, Svelte).

analytics.tsjs
import { analytics } from "@anyanalytics/analytics";

analytics.init({
  writeKey: "vxw_YOUR_WRITE_KEY",
  host: "https://api.anyanalytics.org",
  capturePageviews: true, // page loads + client-side navigations
});

With capturePageviews on, the SDK records a $pageview on load and on every client-side navigation. Add captureOutboundClicks: true to record clicks on links to other sites as $outbound_click. Both are off by default in the npm package (the script tag turns them on).

Note

Recorded page URLs drop the fragment and every query parameter except utm_* and ref, since they often hold tokens or personal data. List any others you want to keep in urlQueryParams.

Custom routers

Pageview capture listens to history.pushState and the back button, which covers the common routers. If yours works differently, leave capturePageviews off and call analytics.page() after each navigation.

Across subdomains

In browsers the visitor's anonymous id and session are kept in a first-party cookie on your parent domain, so shop.example.com and blog.example.com count one visitor with one session. The domain is detected; pass cookieDomain: "example.com" to pin it, or cookieDomain: false to keep ids per subdomain in storage. Unsent events always stay in storage.

Without storing anything

Pass cookieless: true and the SDK writes nothing to the visitor's browser. Visitors are then counted per day, and events queued while offline are lost on reload. See Cookieless mode.

Track and identify

Import the shared analytics instance anywhere and track what people do:

anywhere.tsjs
import { analytics } from "@anyanalytics/analytics";

analytics.track("signup", { plan: "pro", source: "pricing_page" });

Once someone signs in, identify ties their events to your user id. Their earlier anonymous events are linked to the same person. Traits are saved on their profile.

auth.tsjs
// After login: future events belong to this user
analytics.identify("user_123", { email: "[email protected]", plan: "pro" });

// On logout: start a fresh anonymous visitor
analytics.reset();

React Native

React Native has no localStorage, so pass AsyncStorage as the storage option. It keeps the visitor id, the session and unsent events between launches.

terminalsh
npm install @anyanalytics/analytics @react-native-async-storage/async-storage
analytics.tsjs
import AsyncStorage from "@react-native-async-storage/async-storage";
import { analytics } from "@anyanalytics/analytics";

analytics.init({
  writeKey: "vxw_YOUR_WRITE_KEY",
  host: "https://api.anyanalytics.org",
  // Persist the visitor id and offline queue between launches
  storage: AsyncStorage,
  context: { platform: "react-native", app_version: "1.0.0" },
});

analytics.screen("Home");

Import this file once from App.tsx (or index.js) so it runs on startup.

  • Call analytics.screen(name) from your navigation listener to record screen views as $screen events.
  • Events are sent every 10 seconds, or as soon as 20 are queued. To send right away when the app goes to the background, call analytics.flush() from an AppState listener.

Any object with getItem, setItem and removeItem works as storage. They may return values or promises.

Electron and desktop apps

In Electron or Tauri, initialize the SDK in the renderer, where it stores its state in localStorage. Import it from your renderer entry (Electron) or main frontend file (Tauri) so it runs once.

analytics.tsjs
import { analytics } from "@anyanalytics/analytics";

analytics.init({
  writeKey: "vxw_YOUR_WRITE_KEY",
  host: "https://api.anyanalytics.org",
  context: { platform: "desktop", app_version: "1.0.0" },
});

analytics.screen("Main window");

Events are queued while offline and sent when the connection comes back.

Node servers

On a server, one process handles many users at once. Create one instance and pass the user on every call.

analytics.tsjs
import { Analytics, memoryStorage } from "@anyanalytics/analytics";

export const analytics = new Analytics().init({
  writeKey: "vxw_YOUR_WRITE_KEY",
  host: "https://api.anyanalytics.org",
  storage: memoryStorage(),
  context: { platform: "server", app_version: process.env.npm_package_version },
});

// Send whatever is still queued before the process exits
process.on("SIGTERM", () => void analytics.shutdown());

// On a server, call analytics.capture(event, { distinctId }) with the user of
// each request. Never set a process-wide identity: requests run concurrently.

capture(event, options) takes the identity per call, so concurrent requests can't mix up users:

handler.tsjs
import { analytics } from "./analytics";

// distinctId: the user (or anonymous visitor id) this request is for
analytics.capture("order_completed", {
  distinctId: user.id,
  properties: { order_id: "ord_123", total: 49.9 },
});
OptionWhat it is
distinctIdRequired. Your user id, or an anonymous id you keep.
propertiesEvent properties.
sessionIdOptional. Groups events into a session you manage. capture() never starts sessions itself.
timestampOptional. A Date or epoch milliseconds. Defaults to now.

To set traits on a user from the server, capture a $identify event with $set:

handler.tsjs
// Set traits on the user making the request (no shared identity state)
analytics.capture("$identify", {
  distinctId: user.id,
  properties: { $set: { email: user.email, plan: user.plan } },
});

Heads up

Don't call identify() or track() on a server. They keep one identity for the whole process, so with concurrent requests one user's events would be attributed to another.

  • Events are sent in the background. In short-lived scripts and serverless functions, await analytics.flush() before returning.
  • Server events with platform set to "server" or "node" don't get a location from the request, since it comes from your data center, not the user.

API

MethodWhat it does
init(options)Starts the SDK. Call it once; later calls are ignored.
identify(userId, traits?)Ties this device's events to a user id and saves the traits on their profile.
track(event, properties?)Records a custom event.
page(properties?)Records a $pageview for the current URL (browsers).
screen(name, properties?)Records a $screen event (apps and games).
capture(event, options)Records an event for the user you pass. Use it on servers.
setContext(context)Merges fields into the context sent with every event, such as app_version once it's known.
reset()Forgets the user (on logout): new anonymous id, new session.
flush()Sends everything queued now. Returns a promise.
shutdown({ timeoutMs? })Stops timers and sends what it can (waits up to 5 s by default). Resolves to { pending }, the number of events left unsent.
distinctIdThe current user id, or the anonymous id before identify().
sessionIdThe current session's id. Pass it with distinctId to server-side flows, such as checkout metadata, so payments are attributed to the visit. See Revenue.

Sessions are handled for you: a new one starts after 30 minutes without events, and a $session_start event is recorded each time. Call init() before anything else.

Need more than one project in the same app? Create separate instances with new Analytics().init({ … }).

Options

OptionDefaultWhat it does
writeKeyYour project's write key. Required.
hostThe ingestion address, https://api.anyanalytics.org. Required unless you set endpoint.
endpoint{host}/v1/batchThe full URL events are posted to, such as a path on your own domain. Relative URLs resolve against the page. See Ad blockers and proxies.
capturePageviewsfalseBrowsers: record $pageview on load and on client-side navigation.
captureOutboundClicksfalseBrowsers: record clicks on links to other sites as $outbound_click.
urlQueryParams[]Query parameters to keep in recorded URLs, besides utm_* and ref.
cookielessfalseStore nothing on the device. Visitors are counted per day.
cookieDomaindetectedBrowsers: the parent domain whose subdomains share the visitor and session, or false to keep them per subdomain.
storagelocalStorage, or memoryWhere the visitor id, session and unsent events are kept.
contextFields sent with every event: platform, app_version, app_build, os, device and so on.
flushAt20Send as soon as this many events are queued.
flushIntervalMs10000Send at least this often.
maxBatchSize100Events per request (at most 500).
maxQueueSize1000Unsent events kept. The oldest are dropped beyond this.
sessionTimeoutMs1800000Inactivity after which a new session starts (30 minutes).
flushOnHidetrueBrowsers: send queued events when the tab is hidden or closed.
fetchglobal fetchReplace fetch, for tests or custom runtimes.
debugfalseLog what the SDK does to the console.

Delivery

  • Unsent events survive reloads and restarts when storage persists (localStorage in browsers, AsyncStorage in React Native). On a server they're kept in memory, so call shutdown() before the process exits.
  • Failed requests are retried with backoff, honoring the server's Retry-After. Every event has its own id, so a retried event is never counted twice.
  • Events the server rejects are logged to the console with the reason. You'll also see them in Live events.