Docs

Events & Screen Tracking

Track custom events and screen views in your web app with the Grovs SDK

Custom events and screen tracking require Grovs Web SDK 2.0 or later. 1.x reported no events at all — see the 2.0 upgrade guide.

Tracking custom events

Use track to record an event with optional properties and tags:

Grovs.track("purchase", { item: "sku-42", price: 19.99 }, ["checkout"]);
Grovs.track("level_complete", { level: 5, score: 1200 });
Grovs.track("button_tap");

Every custom event carries the most recently viewed screen as screen_name, so events can be segmented by screen in the dashboard.

Event name rules

An event name must be non-empty and must not collide with a reserved system event name:

install, reinstall, app_open, view, open, time_spent, reactivation, user_referred

Use trackScreenView for screen views. A rejected name is dropped with a warning, visible at debugLevel: "warn" or "info".

Property values

Property values may be strings, numbers, booleans, null, or nested arrays and objects of those. The SDK sanitizes each value before sending:

  • Date, URL, and BigInt are coerced to strings.
  • Values that cannot be represented in JSON — NaN, Infinity, functions, symbols, circular references — are dropped for that key only. The remaining properties are still sent.
  • If the encoded properties exceed 8 KB of UTF-8, all properties are dropped and the event is still recorded.
  • Sanitization stops after 10,000 values, so a large object graph cannot stall the page. Past that budget all properties are dropped and the event is still recorded.

Tags

Tags are capped at 20 per event, each up to 255 characters. Set global tags to attach them to every subsequent event, including system events such as time_spent:

Grovs.setGlobalTags(["beta"]);
 
Grovs.setGlobalTags(null);  // clear

Tracking screen views

Grovs.trackScreenView("Checkout", { section: "payment" });

Screen names are capped at 255 characters.

Automatic screen tracking

autoTrackScreenViews defaults to true, so single-page-app route changes fire screen views on their own. pushState, replaceState, popstate, and hashchange are all observed.

To turn it off:

await Grovs.configure({ apiKey: "your-api-key", autoTrackScreenViews: false });

How a screen name is chosen

The SDK resolves names in this order, taking the first that produces one:

  1. Grovs.screenNameProvider, if you set one.
  2. The alias map.
  3. document.title.
  4. The pathname.

Name resolution is deferred one animation frame, so document.title reflects the page the user is on rather than the one they left. If your framework commits its render later than that, name the screen explicitly with the resolver or an alias.

Custom resolver

Grovs.screenNameProvider receives the new URL and returns a name, "suppress" to skip the navigation, or "automatic" to fall through:

Grovs.screenNameProvider = (url) => {
  if (url.pathname.startsWith("/legal")) return "suppress";
  if (url.pathname.startsWith("/product/")) return "Product";
  return "automatic";
};

Screen name aliases

URLs are high-cardinality, so collapse them with alias patterns. Otherwise a catalogue of any size produces one dashboard row per id:

Grovs.setScreenAliases({
  "/product/:id": "Product",
  "/docs/*": "Documentation",
  "/checkout": "Checkout",
});

When several patterns match, the more specific one wins regardless of order: more literal text first, then :param over *. A bare * is a catch-all that every other pattern beats. A pattern with more than two * wildcards is ignored, with a warning at debugLevel: "warn"; use :name for inner segments.

Aliases are synced to the backend after authentication. A sync that fails is retried on the next configure or re-enable.

Delivery

Events are queued, persisted to localStorage, and sent in batches of 50. The first batch goes as soon as configure has authenticated and resolved attribution, and carries anything an earlier visit left undelivered. After that, events batch every five seconds, immediately at 50 events, and again when the page is hidden. A tick with an empty queue makes no request.

Requests that fail with a network error, a 429, or a 5xx are attempted up to three times with exponential backoff and jitter; a timeout, any other 4xx, and the tab-close batch get a single attempt. A Retry-After from the server is honored: a delay longer than the retry cap ends the retries, scheduled batches wait out the interval it names, and an explicit flush() still sends. Events the backend rejects as permanently invalid are dropped rather than retried. The queue holds 1,000 events or one million characters, and discards anything older than seven days.

The final batch of a visit is sent with keepalive when the page is hidden and stays stored until acknowledged, so a page that closes before the answer sends it again from the next load. Each event carries an event_id, and Grovs cloud deduplicates on it. If you point baseURL at your own backend, deduplicating on event_id is your responsibility.

Flushing

To drain the queue before a hard navigation:

await Grovs.flush();

When nothing is in flight, flush() drains until the queue is empty, ignoring the interval and any server cooldown. If a scheduled delivery is already running it joins that one instead, and resolves when that send completes even if the server's cooldown leaves events queued. Before authentication, before attribution has resolved, or while consent is pending it resolves immediately without sending.

Sessions

Sessions are shared across tabs and rotate after 30 minutes of combined idle time. Every event carries its session id.

Next steps

Edit this page on GitHubLast updated 2026-09-15