/** * Batches captured signals and ships them to the Drengr ingest endpoint, * authenticated by a publishable key. Port of the proven Dart IngestSink, * carrying both device-run lessons from birth: * - delivery uses the PRE-PATCH fetch (structurally invisible to capture — * the self-capture loop cannot exist); * - the persistence scheduler uses the writer-loops-until-clean pattern * (an overlap marks dirty and returns; never reschedules a microtask, * which starved the event loop and froze the Flutter demo). * * Best-effort and non-blocking: never throws into the app, drops oldest on * overflow, retries with exponential backoff + full jitter, persists the * queue through a pluggable storage adapter (localStorage on web, * AsyncStorage-compatible on React Native, in-memory fallback anywhere). */ import { type NetworkEvent } from './capture.js'; import { type CrashEvent, type ScreenViewEvent, type ScrollUiEvent, type UiEvent } from './behavior.js'; import type { VaultConfig } from './vault-config.js'; export type { VaultConfig } from './vault-config.js'; /** Minimal async-tolerant KV storage. localStorage satisfies it directly; * React Native's AsyncStorage satisfies it via its promise API. */ export interface StorageAdapter { getItem(key: string): string | null | Promise; setItem(key: string, value: string): void | Promise; removeItem(key: string): void | Promise; } export interface IngestSinkOptions { /** Full ingest URL, e.g. https://.supabase.co/functions/v1/ingest */ url: string; /** Publishable key (drengr_pk_…) sent as Authorization: Bearer. */ publishableKey: string; /** Shared envelope context (app_package, os, install_id, session_id, …). */ context: Record; storage?: StorageAdapter; maxBatch?: number; maxQueue?: number; flushIntervalMs?: number; vault?: VaultConfig; } export declare function defaultStorage(): StorageAdapter; export declare class IngestSink { private readonly url; private readonly key; private readonly context; private readonly storage; private readonly maxBatch; private readonly maxQueue; private readonly flushIntervalMs; private vault?; private queue; private timer; private sending; private retries; private persistScheduled; private persisting; private persistDirty; private externalId; private experiments; constructor(opts: IngestSinkOptions); /** Activate (or clear) on-device sealing after the signed config verifies. Until * set, the sink is fail-closed: PII ships as typed placeholders, never raw. */ setVault(v: VaultConfig | null): void; /** Map a captured exchange to an ingest event and enqueue it. */ addNetwork: (e: NetworkEvent) => void; private sealAndEnqueue; /** Sets the session's external_id (attached to every event hereafter) and emits * one identify event. traits go through the same redact+project pipeline as * bodies. Fail-open: invalid externalId is a no-op. */ identify: (externalId: string, traits?: Record) => void; /** Emit a semantic UI tap — the accessible label of the tapped element, PII * redacted. The "what the user did" half that fuses with net events. Fail-open. */ addTap: (e: UiEvent) => void; /** Emit a screen_view — the screen now shown + the one left. Route names can * carry PII (ids, emails in paths), so both run through the redactor. Fail-open. */ addScreenView: (e: ScreenViewEvent) => void; /** Emit a crash: the error's type + first stack frame only (never the message). * error_kind is the contract field; exception_type mirrors it because the * pipeline's crash grouping (read-events, releases-view) reads exception_type * — same value, both are existing schema columns. Fail-open. */ addCrash: (e: CrashEvent) => void; /** Emit a scroll friction signal — possible_rage_scroll (≥3 direction reversals = * thrashing) or dead_scroll (scrolled at a boundary, content did not move). Same * normalized 0–1 coordinate shape as taps; screen redacted. Fail-open. */ addScroll: (e: ScrollUiEvent) => void; /** app_foreground / app_background — process lifecycle markers so the backend can * subtract backgrounded time from screen dwell (accurate dwell is the signal). * Fail-open. */ addAppForeground: (screen: string) => void; addAppBackground: (screen: string) => void; private addLifecycle; /** Sets/clears a session-scoped experiment variant (attached to every event * hereafter as `experiments`). variant null/empty clears the key. Fail-open. */ setExperiment: (key: string, variant: string | null | undefined) => void; private toNet; private enqueue; flush(): Promise; private armBackoff; private schedulePersist; private persist; private restore; }