import { ReactNode } from 'react'; /** * The consent pair on native, where it is a REAL gate, not a no-op: the * native transport has its own persisted opt-out, and `init()` wires * `optOutCapturingByDefault` to it. A native app under GDPR needs a consent * banner exactly as much as a web app does. * * Synchronous, matching the web spelling, so one signature serves shared * code. Nothing awaits the write - the transport queues captures behind its * own storage load - but a failure is reported rather than dropped. * * No-op before `init()`. */ declare function optInCapturing(): void; /** The withdraw half. No-op before `init()`. */ declare function optOutCapturing(): void; /** * Event properties a customer's app can send with `track`. * * Deliberately not `unknown`: these values are serialised onto the wire and * rendered in a dashboard, so a nested object or a function would either be * dropped upstream or arrive as `[object Object]`. Restricting the type puts * that failure at the call site, in the customer's editor. */ type HeyCatchProperties = Record; /** * Person properties, with the canonical keys the dashboard reads. * * The named keys are the contract: `email` and `name` drive how a person is * displayed, `plan` powers plan-level breakdowns, `signup_date` anchors * account age. Use these exact names whenever the app has the value - * customer A writing `plan` while customer B writes `tier` is what makes * every cross-project feature impossible. * * Everything else is deliberately open: extra context about the user * (payment state, feature usage, whatever is useful) passes through as-is, * nested values included - an unlisted key must never be a type error at * the customer's call site. */ interface HeyCatchPersonProperties { /** How the dashboard shows the person. A property - never the identity. */ email?: string; /** Display name, alongside `email`. */ name?: string; /** The app's own plan name (`free`, `pro`, …). Values are yours; the key is the contract. */ plan?: string; /** ISO 8601. Belongs in set-once, so a later sign-in cannot move it. */ signup_date?: string; /** Anything else useful about the user - passes through untouched. */ [key: string]: unknown; } /** * Person properties, in the two flavours every analytics backend distinguishes. * * - `set` overwrites on every send - use for values that change (`plan`, * `last_seen_at`). * - `setOnce` writes only if the person does not already have the key - use for * values that describe the beginning and must not be overwritten by a later * visit (`signup_date`, `initial_referrer`). * * Named `set` / `setOnce` rather than the transport's own `$set` / `$set_once`: * the `$` names are an implementation detail of who we send to, and this is a * published customer-facing API that must not change if that ever does. */ interface PersonPropertyUpdate { set?: HeyCatchPersonProperties; setOnce?: HeyCatchPersonProperties; } /** * Options for `trackEvent`, as bundler-resolved projects see them. This is * the shape behind the `browser` and `default` type conditions - and * `moduleResolution: "bundler"` (Next.js, Vite) type-checks a whole * project, route handlers included, against it, which is why `userId` must * exist here even though the browser ignores it: one tsconfig cannot hold * two type surfaces per file. Pure-Node backends (`nodenext`) resolve the * `node` condition instead and get {@link ServerTrackOptions}, where the * requirement is enforced at compile time. * * Delivery timing is deliberately not an option: the browser batches (and * the transport flushes the queue on page unload), the server sends * immediately - per-environment behaviour is the SDK's call, not the * caller's. */ interface TrackOptions extends PersonPropertyUpdate { /** * Who the event belongs to. REQUIRED on the server - there is no ambient * person in a webhook, and the server entry's own types (and its runtime * guard) enforce it. Ignored in the browser, where the session already * knows. Must be the same stable internal user id passed to * `setIdentity`. */ userId?: string; /** * Server only, and only meaningful when the event happens during the * user's OWN request (an API route, a server action): pass the incoming * request and the event joins their live browser session - the web SDK * stamps a session header on same-origin requests automatically. A * webhook has no request to pass. Ignored in the browser. */ request?: TracingRequest; } /** * The slice of an incoming request the server entry reads. Both shapes in * the wild satisfy it: a Fetch `Request` (Next route handlers, Hono, Bun - * `headers.get`) and a Node/Express `req` (`headers` as a plain * lowercase-keyed object), so the same call works in either. */ interface TracingRequest { headers: { get: (name: string) => string | null; } | Record; } /** * Options for the server `trackEvent` - REQUIRED, with a REQUIRED * `userId`: a server process has no ambient person, so every event must * say who it belongs to. Must be the same stable internal id the app * passes to `setIdentity` in the browser; that is what joins the two. The * server entry exports this shape as its `TrackOptions`. */ interface ServerTrackOptions extends TrackOptions { userId: string; } /** * Link the device's anonymous visitor to the authenticated user. Same * contract as the web entry: a stable internal user id - never an email or * a session token - with email/name in the PROPERTIES. No-op before * `init()`, mirroring the web entry. Switching to a different user than * the one already identified resets first, as on the web. */ declare function setIdentity(userId: string, properties?: HeyCatchPersonProperties, propertiesOnce?: HeyCatchPersonProperties): void; /** * Clear the current identity. Call on sign-out so the next user on this * device starts anonymous. */ declare function resetIdentity(): void; /** * Which HeyCatch backend this bundle was built against - `dev` or `prod`. * Introspection for debugging ("which stage bundle is this page running?"), * and stamped on every event as `heycatch_sdk_stage`. Describes OUR backend, * not the customer's own app environment. */ declare const STAGE: "dev" | "prod"; declare const SDK_VERSION: string; /** * One captured event, as {@link HeyCatchBeforeSend} sees it just before it * is queued for sending. * * Declared structurally, listing only the fields the SDK can promise. The * index signature carries the underlying transport's own extra keys through * untouched, so nothing is lost and nothing here has to move when they * change. */ interface HeyCatchCapturedEvent { /** Event name - an autocapture name like `$autocapture`, or your own. */ event: string; /** The event's property bag, as it will be sent. */ properties: Record; [key: string]: unknown; } /** * Runs on every event right before it is queued. Return the event to keep * it (mutated or not), or `null` to drop it. * * The SDK never calls this itself and never reads its return value - it * forwards the function to the transport, whose own code calls it. */ type HeyCatchBeforeSend = (event: HeyCatchCapturedEvent | null) => HeyCatchCapturedEvent | null; /** The transport's older, deprecated hook. Prefer {@link HeyCatchBeforeSend}. */ type HeyCatchSanitizeProperties = (properties: Record, eventName: string) => Record; /** Where the SDK may persist the distinct id and super properties. */ type HeyCatchPersistence = 'localStorage' | 'cookie' | 'memory' | 'localStorage+cookie' | 'sessionStorage'; /** * Frameworks the install guides can detect. **Written out as a union, not * derived from the array below**, because * `apps/landing-web/tests/agent-install-guides.spec.ts` matches this * declaration as source text to assert the set agrees with the guide's * detection table. Collapsing it to `(typeof …)[number]` leaves that guard * matching nothing. * * For the same reason, do not restate the declaration's opening line anywhere * above it - including in a comment. The guard takes the FIRST match in the * file, so a prose copy of it wins and yields zero ids. (Written here after * doing exactly that; the spec's vacuity check is what caught it.) */ type KnownFramework = 'nextjs' | 'vite-react' | 'react' | 'vue' | 'svelte' | 'astro' | 'angular' | 'react-native' | 'web'; /** Coding agents that run the install. `other` is the catch-all. */ type KnownInstallAgent = 'lovable' | 'bolt' | 'v0' | 'replit' | 'cursor' | 'claude-code' | 'codex' | 'windsurf' | 'other'; /** * Who installed the SDK and into what. Stamped as super-properties on * every event - the dashboard reads them off the event stream, so there * is no separate install call to make or to fail. * * `(string & {})` keeps the known ids as editor completions while still * accepting anything: a framework we haven't listed yet must not block * an install. */ interface HeyCatchInstall { /** Framework id from the install guide, e.g. `nextjs`. */ framework?: KnownFramework | (string & {}); /** That framework's major version, e.g. `15`. */ frameworkVersion?: string; /** The agent doing the install, e.g. `claude-code`. */ agent?: KnownInstallAgent | (string & {}); } /** Configuration for {@link init}. */ interface HeyCatchConfig { /** Your project's publishable key (`hck_pk_...`). */ projectKey: string; /** Install metadata, stamped on every event. Omit any field you can't determine. */ install?: HeyCatchInstall; /** * Hostnames of the app's own backends on OTHER origins (e.g. * `api.example.com`) whose requests should carry the session header for * server-side `trackEvent({ request })` linking. Same-origin requests * are covered automatically - omit this unless the API lives elsewhere. * Each listed backend must allow {@link TRACING_HEADERS} in its CORS * preflight; one that refuses them fails that first request, and the * headers stop going to it for the rest of the page (logged once). */ tracingHosts?: string[]; /** * Turn off autocapture (clicks and form submissions). Pageviews, * page-leaves, and rage-clicks are captured separately and are NOT * affected by this one. Costs the recap's friction signals - the web * entry logs this once at `init()` so the tradeoff isn't silent. */ autocapture?: boolean; /** * Strip autocaptured elements' text content. Costs the recap's friction * labels - logged once at `init()`. */ maskAllText?: boolean; /** * Strip autocaptured elements' attributes. Costs debugging context on * autocaptured events - logged once at `init()`. */ maskAllElementAttributes?: boolean; /** * Property keys to drop from every captured event. HeyCatch's own * `heycatch_*` attribution keys cannot be denylisted - they are what * attributes an event to your project, so they are filtered back out * (and named in the console) rather than silently honoured. */ propertyDenylist?: string[]; /** @deprecated Superseded by {@link HeyCatchConfig.beforeSend}. */ sanitizeProperties?: HeyCatchSanitizeProperties; /** Runs on every event right before it is queued; return `null` to drop it. */ beforeSend?: HeyCatchBeforeSend | HeyCatchBeforeSend[]; /** Where the SDK persists the distinct id and super properties. */ persistence?: HeyCatchPersistence; /** Disable persistence entirely - every pageload starts a new anonymous person. */ disablePersistence?: boolean; /** Days before the persistence cookie expires. Defaults to 365. */ cookieExpirationDays?: number; /** * Start with capturing off until the app calls * `analytics.optInCapturing()` - a proper consent gate instead of the * "don't call init" workaround. Browser storage is held off too, not * just sending, so no distinct id is written before consent. */ optOutCapturingByDefault?: boolean; /** Honor the browser's Do Not Track signal. */ respectDnt?: boolean; /** * Batch captured events for a few seconds before sending (the default). * Set `false` on a multi-page site - one where each click is a full page * load - because a visitor who navigates away inside the batch window * takes the batch with them; iOS Safari in particular does not reliably * let the unload send through. A single-page app can leave it unset. * Browser only. */ requestBatching?: boolean; /** * How long captured events wait for a batch, in milliseconds. Defaults to * 3000; the accepted range is 250-5000, and a value outside it falls back * to the default with a console warning. The gentler alternative to * `requestBatching: false` when you want to keep batching but shrink the * window. Browser only. */ flushIntervalMs?: number; } declare function init(config: HeyCatchConfig): void; /** * Record a screen view. `HeyCatchProvider` calls this automatically on * Expo Router route changes; call it manually only on a non-Expo-Router * navigator (pass the current route name from your navigation container). * * Manual by design: the transport's own screen autocapture is broken on * React Navigation v7 (what current Expo Router runs on) - its hooks may * not run above the navigator, which is exactly where a provider sits. */ declare function trackScreen(name: string, properties?: HeyCatchProperties): void; /** * Record a product event. Touch autocapture (via `HeyCatchProvider`) and * lifecycle events cover behaviour - this is for the moments only the app * knows about. Same validation contract as the web entry; `userId` in the * options is ignored here, the device session already knows who. */ declare function trackEvent(event: string, properties?: HeyCatchProperties, options?: TrackOptions): void; /** * Update the current person's properties without sending a product event - * same surface as the web entry. The `$set` event is the transport's * person-update carrier; it is a built-in, not a custom event, so no * `heycatch_custom_user_event` stamp. */ declare function setPersonProperties(set?: HeyCatchPersonProperties, setOnce?: HeyCatchPersonProperties): void; /** * Mount once in the root layout (`app/_layout.tsx`), wrapping the layout's * children. It adds the two things `init` alone cannot: touch autocapture * (the transport hooks touches via a root View, so a component is * structurally required) and automatic screen views on Expo Router. * Everything else - lifecycle events, identity, business events - works * from `init` alone. * * `trackScreens={false}` turns the automatic screen views off, for an app * that already calls `analytics.trackScreen` itself and would otherwise * count every screen twice. */ declare function HeyCatchProvider({ children, trackScreens, }: { children: ReactNode; trackScreens?: boolean; }): ReactNode; /** * The React Native entry. The `react-native` export condition routes a * bare `@heycatch/sdk` import here under Metro (newest Expo enables * package exports with that condition), so the customer API stays ONE * surface across web, server, and native. * * Wraps posthog-react-native - a regular dependency, deliberately NOT * bundled: bundling breaks its optional-Expo-peer probing. Not a peer * either: a peer would force the agent guide to name the vendor in its * install line (the vendor-ban spec forbids exactly that); the dependency * resolves silently instead. The Expo companions are app-level installs * the guide's RN line carries (`expo-file-system` above all - without it * persistence silently falls back to memory and every launch becomes a * new anonymous person). * * Flags, surveys, replay, and error tracking are never fetched or enabled. * Native-only additions to the namespace: `trackScreen`, plus the * `HeyCatchProvider` component for touch autocapture + Expo Router screen * views. */ /** * The same-shaped `analytics` namespace as the web and server entries, * plus the native-only `trackScreen`. */ declare const analytics: { readonly init: typeof init; readonly optInCapturing: typeof optInCapturing; readonly optOutCapturing: typeof optOutCapturing; readonly resetIdentity: typeof resetIdentity; readonly setIdentity: typeof setIdentity; readonly setPersonProperties: typeof setPersonProperties; readonly trackEvent: typeof trackEvent; readonly trackScreen: typeof trackScreen; }; export { type HeyCatchBeforeSend, type HeyCatchCapturedEvent, type HeyCatchConfig, type HeyCatchPersistence, type HeyCatchPersonProperties, type HeyCatchProperties, HeyCatchProvider, type HeyCatchSanitizeProperties, type PersonPropertyUpdate, SDK_VERSION, STAGE, type ServerTrackOptions, type TrackOptions, analytics };