Product Analytics
Capture how people use your product in the browser — page views, UI interactions, and custom business events — without shipping a separate analytics vendor.
Events land in your Ductape workspace and are visible in Workbench → Logs (filter by Frontend). You can track anonymous visitors before login and authenticated users after your backend issues a session JWT.
- @ductape/client
- React
- Vue 3
Overview
ductape.analytics is the browser-side product analytics API. It batches events and sends them to Ductape over the analytics ingest endpoint (/v1/analytics/track). Use it to answer questions like:
- Which pages do visitors land on before signing up?
- Where do users drop off in onboarding?
- Which features are used after launch?
- What path did a specific user take before converting?
Anonymous and authenticated tracking
You can track users before they log in.
On first visit the client creates a stable visitor_id (UUID stored in localStorage). Every event includes this id until the user clears site data.
After your backend returns a session JWT, call identify(sessionToken) so subsequent events also carry session_id and session_user_id. Events recorded after identify include both the visitor id and session fields — useful for stitching pre-login browsing to post-login activity.
On logout, call clearSession() so new events are anonymous again (the same visitor_id is retained).
// Logout handler
ductape.analytics.clearSession();
Quick start
import { Ductape } from '@ductape/client';
const ductape = new Ductape({
publishableKey: 'pk_…',
product: 'my-product',
env: 'prd',
});
// Anonymous — no session required
await ductape.analytics.track({
event: 'pricing_viewed',
properties: { plan: 'pro' },
});
// After your backend returns a session JWT
ductape.analytics.identify(sessionTokenFromBackend);
await ductape.analytics.track({
event: 'checkout_started',
properties: { cartTotal: 99, itemCount: 3 },
});
// Standard pageview (manual)
await ductape.analytics.pageview({ title: 'Checkout' });
Event naming
Use lowercase snake_case or dot-separated names that describe user intent:
| Good | Avoid |
|---|---|
signup_started | click1 |
onboarding.step_completed | Button Click |
export_csv | long free-form sentences |
Reserved built-in names (used by helpers):
$pageview— emitted bypageview()and auto-capture$click— emitted when click auto-capture is enabled
API reference
analytics.track(options)
Send a custom event. Events are queued and flushed automatically (every 5 seconds or 20 events).
| Field | Required | Description |
|---|---|---|
event | Yes | Event name (max 128 characters) |
properties | No | Arbitrary JSON metadata (plan, step, error code, …) |
session | No | Session token (tag:jwt) — overrides identify() for this event only |
product / env | No | Override client defaults |
context | No | Override auto-collected page context (url, path, referrer, locale, screen) |
Auto-collected context (browser only, merged into each event):
url,path,referrerlocale(navigator.language)screen({ width, height })
analytics.pageview(options?)
Sends a $pageview event with the current URL and path (or overrides you pass).
| Field | Description |
|---|---|
path | Defaults to window.location.pathname |
title | Defaults to document.title |
properties | Extra metadata (e.g. section: 'billing') |
session | Optional session override |
analytics.identify(session)
Attach a session JWT to all subsequent events until clearSession() is called. Call this once after login succeeds.
analytics.clearSession()
Remove the session from analytics only (does not revoke the JWT on the server). Call on logout.
analytics.getVisitorId()
Returns the stable anonymous visitor id (creates one if missing).
analytics.enableAutoCapture(options?)
Opt in to automatic DOM capture. Disabled by default.
| Option | Default | Behavior |
|---|---|---|
pageviews | true | Initial $pageview + $pageview on popstate (browser back/forward) |
clicks | false | $click on button, a, [role="button"], and [data-track] |
session | — | () => string resolver for session on auto-captured events |
maskTextSelectors | ['[data-private]', '.password'] | Replace click text with [masked] |
Returns a cleanup function — call it to remove listeners (also available via disableAutoCapture()).
const stopCapture = ductape.analytics.enableAutoCapture({
clicks: true,
pageviews: true,
session: () => getSessionFromApp(),
maskTextSelectors: ['.password', '[data-private]', '.card-number'],
});
// Later, e.g. on route leave or app teardown
stopCapture();
Mark elements you care about explicitly:
<button data-track="upgrade-cta">Upgrade to Pro</button>
Auto-captured clicks record element (from data-track or tag name), truncated text, and id when present.
analytics.disableAutoCapture()
Tear down listeners registered by enableAutoCapture().
analytics.flush()
Send the queued batch immediately. Useful before page unload or after a critical conversion event.
Common patterns
SPA route changes
enableAutoCapture listens to popstate only. For client-side routers (React Router, Vue Router), call pageview() in your navigation guard:
router.afterEach((to) => {
ductape.analytics.pageview({ path: to.fullPath, title: String(to.meta.title ?? '') });
});
Funnel steps
Emit one event per step with a shared funnel property:
await ductape.analytics.track({
event: 'onboarding_step_completed',
properties: { funnel: 'signup', step: 2, stepName: 'workspace_name' },
});
Feature flags and experiments
await ductape.analytics.track({
event: 'feature_used',
properties: { feature: 'bulk_export', variant: 'v2_layout' },
});
Per-event session (without identify)
await ductape.analytics.track({
event: 'settings_saved',
session: tokenFromMemory,
properties: { section: 'notifications' },
});
Batching and delivery
- Events queue in memory and flush every 5 seconds or when the queue reaches 20 events.
- The ingest endpoint accepts up to 50 events per HTTP request (client batches at 20).
- Failed flushes are not retried automatically — call
flush()again if you need a hard guarantee before navigation.
Use the useAnalytics hook from @ductape/react for the same analytics API with React-friendly bindings.
import { useEffect } from 'react';
import { useAnalytics } from '@ductape/react';
import { useLocation } from 'react-router-dom';
function AppAnalytics() {
const { pageview, enableAutoCapture } = useAnalytics();
useEffect(() => {
return enableAutoCapture({ clicks: true, pageviews: true });
}, [enableAutoCapture]);
const location = useLocation();
useEffect(() => {
pageview({ path: location.pathname });
}, [location.pathname, pageview]);
return null;
}
function CheckoutButton() {
const { track, identify, visitorId } = useAnalytics();
return (
<button
data-track="checkout-cta"
onClick={() => track({ event: 'checkout_click', properties: { visitorId } })}
>
Checkout
</button>
);
}
Hook return value: track, pageview, identify, visitorId, enableAutoCapture, flush.
Call identify(sessionToken) after login so events include session fields. On logout, use client.analytics.clearSession() from useDuctape() if you need to stop attributing events to the signed-in user.
Use the useAnalytics composable from @ductape/vue for the same analytics API in Vue 3.
<script setup>
import { watch, onMounted, onUnmounted } from 'vue';
import { useRoute } from 'vue-router';
import { useAnalytics } from '@ductape/vue';
const route = useRoute();
const { track, pageview, identify, enableAutoCapture } = useAnalytics();
let stopCapture;
onMounted(() => {
stopCapture = enableAutoCapture({ clicks: true, pageviews: true });
});
onUnmounted(() => stopCapture?.());
watch(
() => route.fullPath,
(path) => pageview({ path, title: String(route.meta?.title ?? '') }),
);
</script>
<template>
<button data-track="checkout-cta" @click="track({ event: 'checkout_click' })">
Checkout
</button>
</template>
Composable return value: track, pageview, identify, visitorId, enableAutoCapture, flush.
Call identify(sessionToken) after login so events include session fields. On logout, use client.analytics.clearSession() from useDuctape() if you need to stop attributing events to the signed-in user.
What you can measure
| Category | Examples |
|---|---|
| Navigation | Page views, SPA route changes, referrer |
| Engagement | Button clicks, CTA taps, [data-track] elements |
| Product flows | signup_started, onboarding_step_2, checkout_started |
| Feature adoption | export_csv, invite_teammate, dark_mode_enabled |
| Conversion | trial_started, plan_selected, payment_submitted |
Use custom event names for anything specific to your product. Built-in helpers emit $pageview and $click when auto-capture is enabled.
Requirements
publishableKeyon the client (browser SDK only)producttag (from client config or per-call override)- Optional
env(defaults toprd) - Optional
sessionJWT from your backend after login — see Sessions
Viewing events in Workbench
Open Logs for your product and filter by type Frontend. Each row represents one analytics event:
| Field | Meaning |
|---|---|
child_tag | Event name ($pageview, checkout_started, …) |
visitor_id | Stable anonymous id (pre-login) |
session_id / session_user_id | Present after identify() |
data | JSON payload: properties, context (URL, path, screen, referrer) |
Query tips:
- Anonymous funnel — filter
visitor_idto follow one browser before sign-in - Signed-in user — filter
session_user_idorsession_id - Full journey — after
identify(), events include bothvisitor_idand session fields
Privacy
- Auto-capture is off by default — enable explicitly with
enableAutoCapture(). - Click text is truncated to 80 characters; use
maskTextSelectorsfor sensitive UI. - Do not put passwords, tokens, or PII in
propertieswithout user consent. - Prefer semantic event names (
pricing_cta_click) over raw DOM text when tracking sensitive flows.
See also
- Sessions — how your backend issues session tokens for
identify() - Frontend access key strategies