Skip to main content

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.

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:

GoodAvoid
signup_startedclick1
onboarding.step_completedButton Click
export_csvlong free-form sentences

Reserved built-in names (used by helpers):

  • $pageview — emitted by pageview() 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).

FieldRequiredDescription
eventYesEvent name (max 128 characters)
propertiesNoArbitrary JSON metadata (plan, step, error code, …)
sessionNoSession token (tag:jwt) — overrides identify() for this event only
product / envNoOverride client defaults
contextNoOverride auto-collected page context (url, path, referrer, locale, screen)

Auto-collected context (browser only, merged into each event):

  • url, path, referrer
  • locale (navigator.language)
  • screen ({ width, height })

analytics.pageview(options?)​

Sends a $pageview event with the current URL and path (or overrides you pass).

FieldDescription
pathDefaults to window.location.pathname
titleDefaults to document.title
propertiesExtra metadata (e.g. section: 'billing')
sessionOptional 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.

OptionDefaultBehavior
pageviewstrueInitial $pageview + $pageview on popstate (browser back/forward)
clicksfalse$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.

What you can measure​

CategoryExamples
NavigationPage views, SPA route changes, referrer
EngagementButton clicks, CTA taps, [data-track] elements
Product flowssignup_started, onboarding_step_2, checkout_started
Feature adoptionexport_csv, invite_teammate, dark_mode_enabled
Conversiontrial_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​

  • publishableKey on the client (browser SDK only)
  • product tag (from client config or per-call override)
  • Optional env (defaults to prd)
  • Optional session JWT from your backend after login — see Sessions

Record the analytics decision​

Every Ductape frontend must explicitly record whether product analytics is enabled, deferred, or prohibited. Silent omission is not a completed integration. Store the decision in ductape/analytics/frontend.json, keyed by each deployable application's path:

{
"applications": {
"apps/customer": {
"status": "enabled",
"privacyReviewed": true,
"autoCapture": "disabled"
},
"apps/admin": {
"status": "deferred",
"rationale": "Consent policy is awaiting approval."
}
}
}

When working through the Ductape MCP, run ductape_frontend_analytics_validate_project({ project_dir: "/absolute/project/path" }). The read-only validator finds deployable React, Vue, and browser-client applications and checks the recorded decision. For enabled applications it also checks for analytics initialization, router pageviews, named events, authenticated identify(), logout/account-switch clearSession(), and auto-capture privacy review. It reports findings without rewriting application code.

Keep auto-capture disabled until consent, text masking, selectors, and sensitive application states have been reviewed.

Viewing events in Workbench​

Open Logs for your product and filter by type Frontend. Each row represents one analytics event:

FieldMeaning
child_tagEvent name ($pageview, checkout_started, …)
visitor_idStable anonymous id (pre-login)
session_id / session_user_idPresent after identify()
dataJSON payload: properties, context (URL, path, screen, referrer)

Query tips:

  • Anonymous funnel — filter visitor_id to follow one browser before sign-in
  • Signed-in user — filter session_user_id or session_id
  • Full journey — after identify(), events include both visitor_id and session fields

Privacy​

  • Auto-capture is off by default — enable explicitly with enableAutoCapture().
  • Click text is truncated to 80 characters; use maskTextSelectors for sensitive UI.
  • Do not put passwords, tokens, or PII in properties without user consent.
  • Prefer semantic event names (pricing_cta_click) over raw DOM text when tracking sensitive flows.

See also​