@lucida-analytics/react-native 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +169 -2
- package/android/src/main/kotlin/com/lucida/reactnative/LucidaReplayModule.kt +96 -0
- package/dist/index.d.ts +838 -0
- package/dist/index.js +1904 -0
- package/ios/LucidaReplay.m +10 -0
- package/ios/LucidaReplay.swift +61 -0
- package/package.json +44 -3
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,838 @@
|
|
|
1
|
+
import { Storage, Transport, Core, WireEvent, FlagValue, Props, FlagResult, Intervention, FlagDefinition, EvalContext } from '@lucida-analytics/sdk-core';
|
|
2
|
+
export { FlagResult, FlagValue, Intervention } from '@lucida-analytics/sdk-core';
|
|
3
|
+
import * as react from 'react';
|
|
4
|
+
import { ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* First-open install attribution for `$app_installed`. Native contract (LucidaReplay.installInfo, a promise method):
|
|
8
|
+
* Android resolves `{platform: "android", referrer?, clickTs?, beginTs?}` (Google Play Install Referrer),
|
|
9
|
+
* iOS resolves `{platform: "ios", token?}` (AdServices `AAAttribution` token). The iOS token is resolved here over `fetch`.
|
|
10
|
+
*/
|
|
11
|
+
interface InstallInfo {
|
|
12
|
+
platform: "android" | "ios";
|
|
13
|
+
referrer?: string | null;
|
|
14
|
+
clickTs?: number;
|
|
15
|
+
beginTs?: number;
|
|
16
|
+
token?: string | null;
|
|
17
|
+
}
|
|
18
|
+
interface NativeInstallModule {
|
|
19
|
+
installInfo?(): Promise<InstallInfo | null>;
|
|
20
|
+
}
|
|
21
|
+
type InstallProps = Record<string, unknown>;
|
|
22
|
+
/** Play Install Referrer string (`utm_source=..&gclid=..`) plus timestamps (seconds) -> `$app_installed` props. */
|
|
23
|
+
declare function installReferrerProps(raw: string, clickTs?: number, beginTs?: number): InstallProps;
|
|
24
|
+
/** Apple Search Ads attribution response -> `$app_installed` props; null/garbage -> store only. */
|
|
25
|
+
declare function appStoreInstallProps(json: unknown): InstallProps;
|
|
26
|
+
interface InstallDeps {
|
|
27
|
+
fetch: typeof fetch;
|
|
28
|
+
retryDelayMs?: number;
|
|
29
|
+
attempts?: number;
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Resolves install props from the native module; never rejects (any failure -> `{}` or store only). */
|
|
33
|
+
declare function resolveInstallProps(native: () => Promise<InstallInfo | null>, d: InstallDeps): Promise<InstallProps>;
|
|
34
|
+
|
|
35
|
+
/** The slice of @react-native-async-storage/async-storage the SDK uses (inject the real module). */
|
|
36
|
+
interface AsyncStorageLike {
|
|
37
|
+
multiGet(keys: readonly string[]): Promise<ReadonlyArray<readonly [string, string | null]>>;
|
|
38
|
+
setItem(key: string, value: string): Promise<void>;
|
|
39
|
+
removeItem(key: string): Promise<void>;
|
|
40
|
+
/** The real module has it; used to find spilled replay chunks (`lc_replay.<id>`) to hydrate. Without it they are not reloaded after a restart. */
|
|
41
|
+
getAllKeys?(): Promise<readonly string[]>;
|
|
42
|
+
}
|
|
43
|
+
/** Keys the SDK persists; hydrated up front because the core's Storage port is synchronous. */
|
|
44
|
+
declare const PERSISTED_KEYS: readonly ["lc_identity", "lc_session", "lc_props", "lc_groups", "lc_consent", "lc_flags", "lc_offline", "lc_app_version", "lc_guide_seen", "lc_person_identified"];
|
|
45
|
+
/** Memory cache hydrated from AsyncStorage with write-behind. Await `ready` before use. */
|
|
46
|
+
declare function asyncStorageAdapter(as: AsyncStorageLike): {
|
|
47
|
+
storage: Storage;
|
|
48
|
+
ready: Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
/** In-memory storage (tests, no AsyncStorage injected). */
|
|
51
|
+
declare function memoryStorage(): Storage;
|
|
52
|
+
/** fetch-based transport. No gzip: React Native has no CompressionStream; batches are small. */
|
|
53
|
+
declare function fetchTransport(fetchImpl: typeof fetch, timeoutMs?: number): Transport;
|
|
54
|
+
/** Sends batches of at most `max` events: a larger `{api_key, batch, sent_at}` body is split; the first failing status wins. */
|
|
55
|
+
declare function splitBatches(inner: Transport, max: number): Transport;
|
|
56
|
+
type AppStateStatus = "active" | "background" | "inactive" | "unknown" | "extension";
|
|
57
|
+
/** The slice of react-native's AppState the SDK uses. */
|
|
58
|
+
interface AppStateLike {
|
|
59
|
+
currentState?: AppStateStatus;
|
|
60
|
+
addEventListener(type: "change", fn: (s: AppStateStatus) => void): {
|
|
61
|
+
remove(): void;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/** Maps foreground/background transitions to callbacks; returns an unsubscribe. */
|
|
65
|
+
declare function appLifecycle(app: AppStateLike, h: {
|
|
66
|
+
onForeground(): void;
|
|
67
|
+
onBackground(): void;
|
|
68
|
+
}): () => void;
|
|
69
|
+
/** Aborts nothing (a custom transport owns its sockets) but stops waiting after `ms`: the send then counts as a network failure and retries. */
|
|
70
|
+
declare function withTimeout(inner: Transport, ms: number): Transport;
|
|
71
|
+
/** Port: at-rest encryption for the persisted queue. `nativeQueueCipher()` backs it with the iOS Keychain / Android Keystore. */
|
|
72
|
+
interface QueueCipher {
|
|
73
|
+
encrypt(plain: string): string;
|
|
74
|
+
/** null when the value cannot be decrypted (key rotated, corrupt): the queue then starts empty. */
|
|
75
|
+
decrypt(cipher: string): string | null;
|
|
76
|
+
}
|
|
77
|
+
/** Decorator: values under `keys` (or a key starting with one of `prefixes`) are stored encrypted, everything else passes through. */
|
|
78
|
+
declare function encryptedStorage(inner: Storage, cipher: QueueCipher, keys: readonly string[], prefixes?: readonly string[]): Storage;
|
|
79
|
+
|
|
80
|
+
interface RageTapConfig {
|
|
81
|
+
enabled?: boolean;
|
|
82
|
+
minTaps?: number;
|
|
83
|
+
windowMs?: number;
|
|
84
|
+
radius?: number;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Element autocapture (`$autocapture` taps and changes). Off by default.
|
|
88
|
+
* Privacy: text-field values are never collected; `propsAllowList` is the only way extra props leave the device.
|
|
89
|
+
*/
|
|
90
|
+
interface AutocaptureConfig {
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
ignoreLabels?: readonly string[];
|
|
93
|
+
maxElements?: number;
|
|
94
|
+
propsAllowList?: readonly string[];
|
|
95
|
+
/** A component with this prop set (`<View lucidaNoCapture />`) and its subtree are never captured. */
|
|
96
|
+
noCaptureProp?: string;
|
|
97
|
+
/** Prop read as the element's label, after `accessibilityLabel`. Default `testID`. */
|
|
98
|
+
customLabelProp?: string;
|
|
99
|
+
rageTap?: RageTapConfig;
|
|
100
|
+
}
|
|
101
|
+
/** One element in the touched component's ancestor chain (nearest first). */
|
|
102
|
+
interface TapElement {
|
|
103
|
+
name: string;
|
|
104
|
+
label?: string | undefined;
|
|
105
|
+
noCapture?: boolean | undefined;
|
|
106
|
+
isInput?: boolean | undefined;
|
|
107
|
+
props?: Record<string, string> | undefined;
|
|
108
|
+
}
|
|
109
|
+
interface Autocapture {
|
|
110
|
+
tap(chain: readonly TapElement[], x: number, y: number): void;
|
|
111
|
+
change(chain: readonly TapElement[]): void;
|
|
112
|
+
readonly config: AutocaptureConfig;
|
|
113
|
+
}
|
|
114
|
+
declare function createAutocapture(core: Core, config: AutocaptureConfig, now?: () => number): Autocapture;
|
|
115
|
+
/** The slice of a React fiber the chain builder reads. */
|
|
116
|
+
interface FiberLike {
|
|
117
|
+
type?: unknown;
|
|
118
|
+
memoizedProps?: Record<string, unknown> | null;
|
|
119
|
+
return?: FiberLike | null;
|
|
120
|
+
}
|
|
121
|
+
/** Touch event shape from a `<View onTouchEnd>`; `_targetInst` is React's own record of the touched fiber. */
|
|
122
|
+
interface TouchEventLike {
|
|
123
|
+
nativeEvent?: {
|
|
124
|
+
pageX?: number;
|
|
125
|
+
pageY?: number;
|
|
126
|
+
};
|
|
127
|
+
_targetInst?: FiberLike | null;
|
|
128
|
+
}
|
|
129
|
+
/** Put the result on your root view: `<View onTouchEnd={touchHandler(client.autocapture)} />`. */
|
|
130
|
+
declare function touchHandler(ac: Autocapture): (e: TouchEventLike) => void;
|
|
131
|
+
/**
|
|
132
|
+
* Builds the ancestor chain from a touched fiber (`event._targetInst`). Only host components and composites that carry
|
|
133
|
+
* a label (`accessibilityLabel` or `customLabelProp`) or the no-capture prop are kept, so the chain stays readable.
|
|
134
|
+
* Text-field values are never read: inputs contribute their name only.
|
|
135
|
+
*/
|
|
136
|
+
declare function chainFromFiber(start: FiberLike | null | undefined, cfg: AutocaptureConfig): TapElement[];
|
|
137
|
+
|
|
138
|
+
interface GuideButton {
|
|
139
|
+
label: string;
|
|
140
|
+
/** next | back | dismiss | link | event (start_tour behaves like next on mobile). */
|
|
141
|
+
action: string;
|
|
142
|
+
url?: string;
|
|
143
|
+
event?: string;
|
|
144
|
+
}
|
|
145
|
+
interface GuideStep {
|
|
146
|
+
id: string;
|
|
147
|
+
title?: string | null;
|
|
148
|
+
body?: string | null;
|
|
149
|
+
buttons?: GuideButton[] | null;
|
|
150
|
+
/** Anchor view: the `testID` of a `<GuideAnchor>` (tooltip, hotspot and tour steps; web selectors are ignored). */
|
|
151
|
+
native_id?: string | null;
|
|
152
|
+
placement?: string | null;
|
|
153
|
+
/** How long to wait for the anchor (ms, default 3000, max 15000) before showing the step unanchored. */
|
|
154
|
+
wait_ms?: number | null;
|
|
155
|
+
}
|
|
156
|
+
/** Where an anchored step points. `hotspot`: a pulsing dot on the view opens the step. */
|
|
157
|
+
interface GuideAnchorSpec {
|
|
158
|
+
nativeId: string;
|
|
159
|
+
placement?: string;
|
|
160
|
+
waitMs: number;
|
|
161
|
+
hotspot: boolean;
|
|
162
|
+
}
|
|
163
|
+
/** A guide as `GET /v1/guides` serves it. */
|
|
164
|
+
interface MobileGuide {
|
|
165
|
+
id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
kind: string;
|
|
168
|
+
steps: GuideStep[];
|
|
169
|
+
conditions: Record<string, unknown>;
|
|
170
|
+
/** A draft served to a QA user: no targeting, shown once per launch, no events. */
|
|
171
|
+
preview?: boolean;
|
|
172
|
+
/** Resolved appearance (the gateway merges the project theme in): tokens, dark.tokens, theme_mode, font_family. */
|
|
173
|
+
appearance?: Record<string, unknown> | null;
|
|
174
|
+
/** A workflow queued this guide for this person (gateway): it goes first and skips sampling. */
|
|
175
|
+
queued?: boolean;
|
|
176
|
+
}
|
|
177
|
+
/** What `<LucidaGuides />` draws. */
|
|
178
|
+
interface GuideView {
|
|
179
|
+
guide: MobileGuide;
|
|
180
|
+
step: number;
|
|
181
|
+
buttons: GuideButton[];
|
|
182
|
+
/** Set when this step points at a view. */
|
|
183
|
+
anchor?: GuideAnchorSpec;
|
|
184
|
+
}
|
|
185
|
+
interface GuidesService {
|
|
186
|
+
/** Fetches the live guides (needs analytics consent), then re-evaluates the current screen. */
|
|
187
|
+
reload(): Promise<void>;
|
|
188
|
+
/** The app moved to `name`: show the first eligible guide if none is on screen. */
|
|
189
|
+
show(name: string): void;
|
|
190
|
+
current(): GuideView | null;
|
|
191
|
+
subscribe(cb: (v: GuideView | null) => void): () => void;
|
|
192
|
+
/** The tapped button, or null when the person closed the step (a dismissal). */
|
|
193
|
+
act(button: GuideButton | null): void;
|
|
194
|
+
/** How https links open (the view wires Linking.openURL). */
|
|
195
|
+
setOpener(fn: (url: string) => void): void;
|
|
196
|
+
/** The view could not find the current step's anchor in time (emits `$guide_target_missing`; it then shows unanchored). */
|
|
197
|
+
targetMissing(): void;
|
|
198
|
+
}
|
|
199
|
+
interface GuidesDeps {
|
|
200
|
+
core: Core;
|
|
201
|
+
fetch: typeof fetch;
|
|
202
|
+
apiHost: string;
|
|
203
|
+
apiKey: string;
|
|
204
|
+
flag(key: string): boolean;
|
|
205
|
+
random?: () => number;
|
|
206
|
+
}
|
|
207
|
+
declare function matches(screen: string, pattern: string, how: string): boolean;
|
|
208
|
+
declare function guideButtons(g: MobileGuide, i: number): GuideButton[];
|
|
209
|
+
/** Project-wide exclusions matched against the screen name: `*` globs, the same patterns the web matches against URLs. */
|
|
210
|
+
declare function screenExcluded(screen: string, patterns: readonly string[]): boolean;
|
|
211
|
+
/** The anchor of step `i`, when its guide kind anchors and the step names a view. */
|
|
212
|
+
declare function guideAnchor(g: MobileGuide, i: number): GuideAnchorSpec | undefined;
|
|
213
|
+
declare function createGuides(d: GuidesDeps): GuidesService;
|
|
214
|
+
|
|
215
|
+
/** A `beforeSend` hook: return the (possibly amended) event, or null to drop it. */
|
|
216
|
+
type BeforeSend = (event: WireEvent) => WireEvent | null;
|
|
217
|
+
type PersonProfiles = "always" | "identified_only" | "never";
|
|
218
|
+
/**
|
|
219
|
+
* Runs hooks in order; each sees the previous hook's output and null stops the chain (the event is dropped).
|
|
220
|
+
* A hook that throws is logged and skipped: the event continues unchanged (as it was before that hook).
|
|
221
|
+
*/
|
|
222
|
+
declare function runHooks(hooks: readonly BeforeSend[], event: WireEvent, log: (...a: unknown[]) => void): WireEvent | null;
|
|
223
|
+
/** Keeps roughly `rate` (0..1) of events and stamps survivors with `$sample_rate`. Compose it with your own filter hook. */
|
|
224
|
+
declare function sampleHook(rate: number, random?: () => number): BeforeSend;
|
|
225
|
+
/** `personProfiles` state: `identified_only` flips to processing on the first identify/alias/group/person-property write. */
|
|
226
|
+
declare class PersonMode {
|
|
227
|
+
readonly mode: PersonProfiles;
|
|
228
|
+
private readonly storage;
|
|
229
|
+
private identified;
|
|
230
|
+
constructor(mode: PersonProfiles, storage: Storage);
|
|
231
|
+
get blocksPersonWrites(): boolean;
|
|
232
|
+
markIdentified(): void;
|
|
233
|
+
/** True when an event of this name should carry `$process_person_profile: false`. */
|
|
234
|
+
personless(event: string): boolean;
|
|
235
|
+
reset(): void;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* P-L12 controlled exposure. The RSA-OAEP-SHA256 encryption runs natively (`encryptSensitive` on the LucidaReplay module,
|
|
240
|
+
* a blocking synchronous method: iOS SecKey, Android Cipher with MGF1-SHA256), so only an audited reveal on the server can
|
|
241
|
+
* read the value. Identical values are sent once per session (an in-memory SHA-256 set, never persisted or sent).
|
|
242
|
+
*/
|
|
243
|
+
interface NativeSensitiveModule {
|
|
244
|
+
/** Standard base64 of the ciphertext of the UTF-8 of `plain`, or "" on failure. */
|
|
245
|
+
encryptSensitive(pkcs1: string, n: string, e: string, plain: string): string;
|
|
246
|
+
}
|
|
247
|
+
interface ExposureKey {
|
|
248
|
+
kid: string;
|
|
249
|
+
/** base64url (no padding) big-endian modulus */
|
|
250
|
+
n: string;
|
|
251
|
+
/** base64url (no padding) exponent */
|
|
252
|
+
e: string;
|
|
253
|
+
/** standard base64 of the PKCS#1 RSAPublicKey DER */
|
|
254
|
+
pkcs1: string;
|
|
255
|
+
}
|
|
256
|
+
declare const SENSITIVE_MAX_PLAIN_BYTES = 190;
|
|
257
|
+
/** [value] cut so its UTF-8 is at most [max] bytes, on a code point boundary; `truncated` when cut. */
|
|
258
|
+
declare function truncateUtf8(value: string, max?: number): {
|
|
259
|
+
text: string;
|
|
260
|
+
truncated: boolean;
|
|
261
|
+
};
|
|
262
|
+
/** Native encryption, failing closed: null when the module is missing, throws or returns "". */
|
|
263
|
+
declare function encryptSensitive(module: Partial<NativeSensitiveModule> | undefined, key: ExposureKey, plain: string): string | null;
|
|
264
|
+
interface SensitiveCapture {
|
|
265
|
+
/** `$sensitive_capture` properties, or null when this value was already sent this session or encryption failed. */
|
|
266
|
+
properties(field: string, value: string, screen: string, key: ExposureKey, sessionId: string): Record<string, unknown> | null;
|
|
267
|
+
}
|
|
268
|
+
declare function createSensitiveCapture(module: Partial<NativeSensitiveModule> | undefined): SensitiveCapture;
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Native contract (implemented by ios/LucidaReplay.swift and android/.../LucidaReplayModule.kt, which call the
|
|
272
|
+
* Lucida iOS / Android SDK wireframe encoders): one capture of the current native view hierarchy.
|
|
273
|
+
* Resolves to JSON text `{"width": number, "height": number, "events": [meta (type 4), fullSnapshot (type 2)]}`,
|
|
274
|
+
* or "" when nothing can be captured (no key window / activity). Native code must mask: input values and
|
|
275
|
+
* secure fields never leave the device, other text is starred when `maskText` is true. Views wrapped in
|
|
276
|
+
* `<LucidaMask>` / `<LucidaUnmask>` carry the native markers the encoders already honour. See README "Session replay".
|
|
277
|
+
*/
|
|
278
|
+
interface NativeReplayModule extends Partial<NativeSensitiveModule> {
|
|
279
|
+
captureWireframe(options: {
|
|
280
|
+
/** P-M17: mask every text, input and image whatever the unmask markers say (present only when true). */
|
|
281
|
+
maskScreen?: boolean;
|
|
282
|
+
/** P-M46: regular expressions starred in shown text; screenshots paint matching views (present only when non-empty). */
|
|
283
|
+
maskTextPatterns?: string[];
|
|
284
|
+
screen: string;
|
|
285
|
+
maskText: boolean;
|
|
286
|
+
maskAllTextInputs: boolean;
|
|
287
|
+
maskAllImages: boolean;
|
|
288
|
+
maskSandboxedViews: boolean;
|
|
289
|
+
/** Present only in screenshot mode: the native side draws the window, paints the mask plan and only then encodes JPEG. */
|
|
290
|
+
mode?: "screenshot";
|
|
291
|
+
/** JPEG quality 0..1 (screenshot mode). */
|
|
292
|
+
screenshotQuality?: number;
|
|
293
|
+
}): Promise<string>;
|
|
294
|
+
}
|
|
295
|
+
interface ReplayConfig {
|
|
296
|
+
/** Off by default; also requires the `replay` consent category. */
|
|
297
|
+
enabled?: boolean;
|
|
298
|
+
/** The native module (`NativeModules.LucidaReplay`), injected so tests and non-native hosts need no React Native. */
|
|
299
|
+
module: NativeReplayModule;
|
|
300
|
+
/** Seconds-level cadence; a snapshot is only sent when the structure changed. Default 5000. */
|
|
301
|
+
snapshotIntervalMs?: number;
|
|
302
|
+
/** Starred text unless false. Default true. */
|
|
303
|
+
maskText?: boolean;
|
|
304
|
+
/** Text-field values are starred unless false (secure fields are never readable). Default true. */
|
|
305
|
+
maskAllTextInputs?: boolean;
|
|
306
|
+
/** Images carry a mask marker unless false. Default true. */
|
|
307
|
+
maskAllImages?: boolean;
|
|
308
|
+
/** WebViews / maps / platform views are recorded as an empty frame unless false. Default true. */
|
|
309
|
+
maskSandboxedViews?: boolean;
|
|
310
|
+
/** "screenshot" sends masked JPEG frames (masks painted on device before encoding) instead of wireframes. Default "wireframe". */
|
|
311
|
+
mode?: "wireframe" | "screenshot";
|
|
312
|
+
/** JPEG quality 0..1 for screenshot mode. Default 0.4. */
|
|
313
|
+
screenshotQuality?: number;
|
|
314
|
+
/** 0..1, decided once per session. Default 1. */
|
|
315
|
+
sampleRate?: number;
|
|
316
|
+
/** Minimum ms between captures on top of the interval. Default 0. */
|
|
317
|
+
throttleMs?: number;
|
|
318
|
+
/** Hold chunks until the session has been recorded this long; a session that ends sooner (or `stop()`) is never sent. Default 0 (off). */
|
|
319
|
+
minDurationMs?: number;
|
|
320
|
+
/**
|
|
321
|
+
* Disk budget for chunks that wait (min duration, offline, upload backoff); over it the oldest whole session is dropped
|
|
322
|
+
* and `onSpoolDrop` fires. Default 10 MiB. Needs `asyncStorage` to survive a restart.
|
|
323
|
+
*/
|
|
324
|
+
spoolBudgetBytes?: number;
|
|
325
|
+
/** A whole session was dropped because the spool exceeded its budget (also logged in debug mode). */
|
|
326
|
+
onSpoolDrop?: (info: {
|
|
327
|
+
sessionId: string;
|
|
328
|
+
chunks: number;
|
|
329
|
+
bytes: number;
|
|
330
|
+
}) => void;
|
|
331
|
+
/** Fetch `GET /v1/config` and honour the project kill switch, sample rate and minimum duration (tightening only). Default true. */
|
|
332
|
+
useRemoteConfig?: boolean;
|
|
333
|
+
/** Start recording only while this flag is on. */
|
|
334
|
+
linkedFlag?: string;
|
|
335
|
+
/** Only these screens are recorded (empty = all). The deny list wins and pauses recording on a denied screen. */
|
|
336
|
+
screenAllowList?: string[];
|
|
337
|
+
screenDenyList?: string[];
|
|
338
|
+
/** Record taps fed to `client.replay.tap(x, y)` (positions only, never what was typed). Default true. */
|
|
339
|
+
captureTouches?: boolean;
|
|
340
|
+
/** P-M17 screen-name patterns (`*` wildcard, case-insensitive, whole name): mask everything there. Union with the project's list. */
|
|
341
|
+
maskScreens?: string[];
|
|
342
|
+
/** P-M46 regular expressions starred in any text replay would show. Union with the project's list; invalid ones are ignored. */
|
|
343
|
+
maskTextPatterns?: string[];
|
|
344
|
+
/** P-M46 `$dead_swipe` for swipes fed by `<LucidaTouchCapture>` that changed nothing. Default true. */
|
|
345
|
+
captureDeadSwipes?: boolean;
|
|
346
|
+
/** How long after a swipe the screen must still look the same (tests). Default 700. */
|
|
347
|
+
deadSwipeSettleMs?: number;
|
|
348
|
+
/** Random source for sampling (tests). */
|
|
349
|
+
random?: () => number;
|
|
350
|
+
/** Largest chunk sent (the gateway accepts 10 MB decompressed). Default 5_000_000. */
|
|
351
|
+
maxEventBytes?: number;
|
|
352
|
+
/** Timer injection for tests. */
|
|
353
|
+
setInterval?: (fn: () => void, ms: number) => unknown;
|
|
354
|
+
clearInterval?: (h: unknown) => void;
|
|
355
|
+
}
|
|
356
|
+
interface ReplayService {
|
|
357
|
+
start(): void;
|
|
358
|
+
stop(): void;
|
|
359
|
+
/** Manual pause: ticks capture nothing until resume() (the timer keeps running). */
|
|
360
|
+
pause(): void;
|
|
361
|
+
resume(): void;
|
|
362
|
+
/** Timer running, not paused, both consent categories held, sampled in, flag on and screen allowed. */
|
|
363
|
+
isRecording(): boolean;
|
|
364
|
+
/** Screen the next capture is attributed to (also emits a navigation marker on change). */
|
|
365
|
+
setScreen(name: string): void;
|
|
366
|
+
/** A tap at screen point (x, y); ignored without consent, before the first snapshot, while paused or on a gated screen. */
|
|
367
|
+
tap(x: number, y: number): void;
|
|
368
|
+
/** Erase every spilled chunk (opt-out, reset). */
|
|
369
|
+
clearSpool(): void;
|
|
370
|
+
/** One capture pass (the interval calls this); exposed for tests. */
|
|
371
|
+
tick(): Promise<void>;
|
|
372
|
+
/** P-M46: a swipe passed the touch slop; its start structure is captured now. */
|
|
373
|
+
swipeBegan(): void;
|
|
374
|
+
/** P-M46: the swipe ended (end - start, points); resolves true when a `$dead_swipe` was captured. */
|
|
375
|
+
swipeEnded(dx: number, dy: number): Promise<boolean>;
|
|
376
|
+
/** P-L12: see `NativeClient.captureSensitive`; true when a `$sensitive_capture` was captured. */
|
|
377
|
+
captureSensitive(field: string, value: string): boolean;
|
|
378
|
+
}
|
|
379
|
+
interface ReplayDeps {
|
|
380
|
+
core: Core;
|
|
381
|
+
/** Where waiting chunks are spilled (the client's storage, encrypted with the queue cipher when one is set). */
|
|
382
|
+
storage: Storage;
|
|
383
|
+
fetch: typeof fetch;
|
|
384
|
+
apiHost: string;
|
|
385
|
+
apiKey: string;
|
|
386
|
+
config: ReplayConfig;
|
|
387
|
+
now?: () => number;
|
|
388
|
+
log?: (...a: unknown[]) => void;
|
|
389
|
+
/** Reads `config.linkedFlag`. */
|
|
390
|
+
isFlagOn?: (key: string) => boolean;
|
|
391
|
+
}
|
|
392
|
+
type Remote = {
|
|
393
|
+
session_recording?: boolean;
|
|
394
|
+
replay_sample_rate?: number;
|
|
395
|
+
replay_min_duration_ms?: number;
|
|
396
|
+
replay_mask_screens?: unknown;
|
|
397
|
+
replay_mask_text_patterns?: unknown;
|
|
398
|
+
replay_exposure_fields?: unknown;
|
|
399
|
+
replay_exposure_key?: unknown;
|
|
400
|
+
};
|
|
401
|
+
/** The P-L12 key when all four parts are strings, else null (the feature is off). */
|
|
402
|
+
declare function exposureKeyOf(v: unknown): ExposureKey | null;
|
|
403
|
+
/** Deny wins; an empty allow list allows every screen. */
|
|
404
|
+
declare const screenAllowed: (screen: string, allow?: readonly string[], deny?: readonly string[]) => boolean;
|
|
405
|
+
/** Remote config can only tighten the local settings. */
|
|
406
|
+
declare const effectiveSampleRate: (local: number, remote: Remote | null) => number;
|
|
407
|
+
declare const effectiveMinDurationMs: (local: number, remote: Remote | null) => number;
|
|
408
|
+
/**
|
|
409
|
+
* Mobile wireframe replay for React Native. The native module encodes the view hierarchy; this side owns what
|
|
410
|
+
* the native SDKs own for themselves: consent gate (analytics + replay), project kill switch / sample rate /
|
|
411
|
+
* minimum duration, linked flag, screen allow/deny list, session/window ids, chunk `seq` that resets when the
|
|
412
|
+
* session rotates, dedupe by structure, and delivery to `POST /v1/replay`.
|
|
413
|
+
*/
|
|
414
|
+
declare function createReplay(d: ReplayDeps): ReplayService;
|
|
415
|
+
|
|
416
|
+
/** Wireframe replay control; no-ops unless `replay.enabled`. Consent (analytics + replay) is always required. */
|
|
417
|
+
interface ReplayControl {
|
|
418
|
+
start(): void;
|
|
419
|
+
stop(): void;
|
|
420
|
+
pause(): void;
|
|
421
|
+
resume(): void;
|
|
422
|
+
/** Emits `$replay_tag` { name, session_id }. */
|
|
423
|
+
tag(name: string): void;
|
|
424
|
+
isRecording(): boolean;
|
|
425
|
+
/** A tap at screen point (x, y) for the replay touch overlay; see `<LucidaTouchCapture>`. Ignored unless recording with `replay.captureTouches`. */
|
|
426
|
+
tap(x: number, y: number): void;
|
|
427
|
+
/** P-M46: a swipe passed the touch slop (`<LucidaTouchCapture>` calls this). */
|
|
428
|
+
swipeBegan(): void;
|
|
429
|
+
/** P-M46: the swipe ended, `dx`/`dy` = end - start in points; a 48 pt swipe that changed nothing becomes `$dead_swipe`. */
|
|
430
|
+
swipeEnded(dx: number, dy: number): void;
|
|
431
|
+
}
|
|
432
|
+
/** Identity and flag values known before the first network response (server-rendered, or from a previous launch). */
|
|
433
|
+
interface Bootstrap {
|
|
434
|
+
distinctId?: string;
|
|
435
|
+
/** Treat `distinctId` as an identified user (no anonymous merge on the first identify). */
|
|
436
|
+
isIdentified?: boolean;
|
|
437
|
+
flags?: Record<string, FlagValue>;
|
|
438
|
+
payloads?: Record<string, unknown>;
|
|
439
|
+
}
|
|
440
|
+
interface FlagOptions {
|
|
441
|
+
bootstrap?: Bootstrap | undefined;
|
|
442
|
+
/** Emit `$feature_flag_called` / `$exposure` when a flag is read. Default true. */
|
|
443
|
+
sendFeatureFlagEvent?: boolean;
|
|
444
|
+
/** false: no flag request at launch or on identity changes until `reloadFlags()`. Default true. */
|
|
445
|
+
preload?: boolean;
|
|
446
|
+
/** Environment tags sent with the flag request; the server returns only flags whose tags match. */
|
|
447
|
+
evaluationContexts?: string[];
|
|
448
|
+
/**
|
|
449
|
+
* Skip the automatic flag request at launch while the last response for this device and id is younger than this many ms.
|
|
450
|
+
* `reloadFlags()`, identity changes and property changes always fetch. Default off.
|
|
451
|
+
*/
|
|
452
|
+
cacheTtlMs?: number;
|
|
453
|
+
}
|
|
454
|
+
interface CaptureOptions {
|
|
455
|
+
/** Person properties set with this event (`$set`). */
|
|
456
|
+
set?: Props;
|
|
457
|
+
/** Person properties set once with this event (`$set_once`). */
|
|
458
|
+
setOnce?: Props;
|
|
459
|
+
}
|
|
460
|
+
interface NativeClient {
|
|
461
|
+
readonly distinctId: string;
|
|
462
|
+
/** Anonymous id of this device; survives `reset()` with `reuseAnonymousId`. */
|
|
463
|
+
getAnonymousId(): string;
|
|
464
|
+
isOptedOut(): boolean;
|
|
465
|
+
/** Runtime debug toggle. */
|
|
466
|
+
debug(on: boolean): void;
|
|
467
|
+
capture(event: string, props?: Props, opts?: CaptureOptions): void;
|
|
468
|
+
/** Updates person properties without an event of your own (sends a `$set` event). */
|
|
469
|
+
setPersonProperties(set?: Props, setOnce?: Props): void;
|
|
470
|
+
/** `$deep_link_opened` with the url, its query parameters and the referrer. */
|
|
471
|
+
openURL(url: string, referrer?: string): void;
|
|
472
|
+
/** `fetch` that adds `X-Lucida-Distinct-Id` / `X-Lucida-Session-Id` for hosts in `tracingHeaders`. */
|
|
473
|
+
readonly fetch: typeof fetch;
|
|
474
|
+
readonly autocapture: Autocapture;
|
|
475
|
+
/** Screen view: `$screen` with `$screen_name`. */
|
|
476
|
+
screen(name: string, props?: Props): void;
|
|
477
|
+
identify(id: string, set?: Props, once?: Props): void;
|
|
478
|
+
alias(alias: string, original?: string): void;
|
|
479
|
+
reset(): void;
|
|
480
|
+
register(p: Props): void;
|
|
481
|
+
registerOnce(p: Props): void;
|
|
482
|
+
unregister(key: string): void;
|
|
483
|
+
group(type: string, key: string, props?: Props): void;
|
|
484
|
+
optIn(): void;
|
|
485
|
+
optOut(): void;
|
|
486
|
+
getFlag(key: string): FlagValue | undefined;
|
|
487
|
+
getFlagPayload(key: string): unknown;
|
|
488
|
+
/** Value, variant, payload and reason (evaluator/server reason, or `override`, `bootstrap`, `cached`, `pushed`, `not_found`) in one read. */
|
|
489
|
+
getFlagResult(key: string, opts?: {
|
|
490
|
+
sendExposure?: boolean;
|
|
491
|
+
}): FlagResult;
|
|
492
|
+
isFeatureEnabled(key: string): boolean;
|
|
493
|
+
onFlags(cb: (f: Record<string, FlagValue>) => void): () => void;
|
|
494
|
+
/** In-session interventions (N-ANA-2) fired for this person; polls GET /v1/interventions while a listener is set. Returns unsubscribe. */
|
|
495
|
+
onIntervention(cb: (i: Intervention) => void): () => void;
|
|
496
|
+
reloadFlags(): Promise<void>;
|
|
497
|
+
/** Injects server-evaluated flags (own backend); use with `disableRemoteFeatureFlags`. `merge` keeps existing values. */
|
|
498
|
+
updateFlags(flags: Record<string, FlagValue>, payloads?: Record<string, unknown>, opts?: {
|
|
499
|
+
merge?: boolean;
|
|
500
|
+
}): void;
|
|
501
|
+
/** Every known flag value, without exposure events. */
|
|
502
|
+
getAllFlags(): Record<string, FlagValue>;
|
|
503
|
+
/** Force values on this device; `false` clears. */
|
|
504
|
+
overrideFlags(map: Record<string, FlagValue> | false): void;
|
|
505
|
+
/** Person / group properties used only to evaluate flags (no `$set` event). `reload` (default true) re-evaluates now. */
|
|
506
|
+
setPersonPropertiesForFlags(props: Props, reload?: boolean): Promise<void>;
|
|
507
|
+
resetPersonPropertiesForFlags(reload?: boolean): Promise<void>;
|
|
508
|
+
setGroupPropertiesForFlags(type: string, props: Props, reload?: boolean): Promise<void>;
|
|
509
|
+
resetGroupPropertiesForFlags(reload?: boolean): Promise<void>;
|
|
510
|
+
readonly replay: ReplayControl;
|
|
511
|
+
/**
|
|
512
|
+
* Controlled exposure (P-L12): sends `value` RSA-OAEP-SHA256 encrypted (natively) with the project key as
|
|
513
|
+
* `$sensitive_capture`, only when `field` (your testID) is listed in the project's exposure fields, the key is present,
|
|
514
|
+
* analytics + replay consent is held and replay is enabled. Only an audited reveal on the server can read it; identical
|
|
515
|
+
* values are sent once per session. Never pass a secure (password) field. Returns whether an event was captured.
|
|
516
|
+
*/
|
|
517
|
+
captureSensitive(field: string, value: string): boolean;
|
|
518
|
+
/** In-app guides; inert unless `guides.enabled` and a `<LucidaGuides />` is mounted. */
|
|
519
|
+
readonly guides: GuidesService;
|
|
520
|
+
flush(): Promise<void>;
|
|
521
|
+
shutdown(): Promise<void>;
|
|
522
|
+
}
|
|
523
|
+
interface NativeConfig {
|
|
524
|
+
apiHost?: string;
|
|
525
|
+
/** Inject @react-native-async-storage/async-storage. Omitted: in-memory only. */
|
|
526
|
+
asyncStorage?: AsyncStorageLike;
|
|
527
|
+
/** Inject react-native's AppState for $app_opened / $app_backgrounded and flush on background. */
|
|
528
|
+
appState?: AppStateLike;
|
|
529
|
+
fetch?: typeof fetch;
|
|
530
|
+
transport?: Transport;
|
|
531
|
+
appVersion?: string;
|
|
532
|
+
appBuild?: string;
|
|
533
|
+
/** Send install attribution on the first-open `$app_installed` (default true); needs `installInfo`. */
|
|
534
|
+
installAttribution?: boolean;
|
|
535
|
+
/** Native source: `() => NativeModules.LucidaReplay.installInfo()`. Omitted: `$app_installed` is sent immediately without attribution. */
|
|
536
|
+
installInfo?: () => Promise<InstallInfo | null>;
|
|
537
|
+
platform?: string;
|
|
538
|
+
osVersion?: string;
|
|
539
|
+
consent?: {
|
|
540
|
+
mode?: "opt_in" | "opt_out";
|
|
541
|
+
queueBeforeConsent?: boolean;
|
|
542
|
+
storageKey?: string;
|
|
543
|
+
};
|
|
544
|
+
flushAt?: number;
|
|
545
|
+
flushIntervalMs?: number;
|
|
546
|
+
maxQueueSize?: number;
|
|
547
|
+
/** Encrypt the persisted offline queue at rest (Keychain / Keystore backed). */
|
|
548
|
+
queueCipher?: QueueCipher;
|
|
549
|
+
/** Largest batch per request; bigger queues are sent in chunks, also for a custom `transport`. Default: no split. */
|
|
550
|
+
maxBatchSize?: number;
|
|
551
|
+
maxRetries?: number;
|
|
552
|
+
/** Base of the exponential retry delay. Default 1000. */
|
|
553
|
+
retryDelayMs?: number;
|
|
554
|
+
/** Abort (or, for a custom `transport`, stop waiting for) a request after this long (0 = never). Default 0. */
|
|
555
|
+
requestTimeoutMs?: number;
|
|
556
|
+
/** `wifi`: flush only while `network.isWifi()`; events wait in the queue otherwise. */
|
|
557
|
+
dataMode?: "any" | "wifi";
|
|
558
|
+
network?: {
|
|
559
|
+
isWifi(): boolean;
|
|
560
|
+
};
|
|
561
|
+
sessionIdleMs?: number;
|
|
562
|
+
sessionMaxMs?: number;
|
|
563
|
+
/** Ask the server not to geolocate events (`$geoip_disable`). */
|
|
564
|
+
disableGeoip?: boolean;
|
|
565
|
+
/** Ordered hooks run on every event before it is queued; return null to drop. A throwing hook is logged and skipped. */
|
|
566
|
+
beforeSend?: BeforeSend[];
|
|
567
|
+
personProfiles?: PersonProfiles;
|
|
568
|
+
/** Keep the anonymous id across `reset()`. */
|
|
569
|
+
reuseAnonymousId?: boolean;
|
|
570
|
+
flagOptions?: FlagOptions;
|
|
571
|
+
/** Never call POST /v1/flags: use with `flagOptions.bootstrap`, `localFlags` and `updateFlags`. */
|
|
572
|
+
disableRemoteFeatureFlags?: boolean;
|
|
573
|
+
/** Attach `$feature/<key>` and `$active_feature_flags` to events (default true). */
|
|
574
|
+
sendFeatureFlags?: boolean;
|
|
575
|
+
/** Send $app_version, $os and $lib as person properties with flag requests (default true). */
|
|
576
|
+
sendDefaultFlagProps?: boolean;
|
|
577
|
+
autocapture?: AutocaptureConfig;
|
|
578
|
+
/** Own-backend hosts that receive `X-Lucida-*` headers through `client.fetch`. */
|
|
579
|
+
tracingHeaders?: string[];
|
|
580
|
+
disabled?: boolean;
|
|
581
|
+
debug?: boolean;
|
|
582
|
+
/** Definitions for on-device evaluation; flags needing the server (cohorts) still use POST /v1/flags. */
|
|
583
|
+
localFlags?: FlagDefinition[];
|
|
584
|
+
localFlagsContext?: Partial<EvalContext>;
|
|
585
|
+
/** Consume /v1/flags/stream with the public key (change notices only) and refetch on notice; falls back to polling (`pollMs`, default 60s) when the runtime cannot stream fetch bodies. Default off. */
|
|
586
|
+
flagStream?: boolean | {
|
|
587
|
+
pollMs?: number;
|
|
588
|
+
};
|
|
589
|
+
/** Wireframe session replay through the native module (off unless `enabled`). */
|
|
590
|
+
replay?: ReplayConfig;
|
|
591
|
+
/** In-app guides (LC-ENG-15). Off by default: no `/v1/guides` request until enabled. */
|
|
592
|
+
guides?: {
|
|
593
|
+
enabled?: boolean;
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
declare const LIB = "lucida-react-native";
|
|
598
|
+
declare const LIB_VERSION = "0.1.1";
|
|
599
|
+
/**
|
|
600
|
+
* Hydrates storage, then builds the client. Nothing is persisted before consent in opt_in mode;
|
|
601
|
+
* `$app_opened` fires on creation and on every foreground, `$app_backgrounded` flushes the queue.
|
|
602
|
+
*/
|
|
603
|
+
declare function createClient(apiKey: string, config?: NativeConfig): Promise<NativeClient>;
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Native contract (ios/LucidaReplay.swift -> KeychainQueueCipher, android/.../LucidaReplayModule.kt -> KeystoreQueueCipher):
|
|
607
|
+
* two blocking synchronous methods (`RCT_EXTERN__BLOCKING_SYNCHRONOUS_METHOD` / `isBlockingSynchronousMethod = true`), because
|
|
608
|
+
* the core's Storage port is synchronous. AES-256-GCM with a non-exportable key held by the platform (iOS Keychain, Android
|
|
609
|
+
* Keystore), generated on first use. Ciphertext is base64 of nonce + ciphertext + tag, the same format as the native SDKs.
|
|
610
|
+
* Blocking synchronous methods need JSI, so they are unavailable while debugging through the remote JS debugger.
|
|
611
|
+
*/
|
|
612
|
+
interface NativeCipherModule {
|
|
613
|
+
encryptQueue(plain: string): string;
|
|
614
|
+
/** null or "" when the value cannot be decrypted (key lost or rotated, corrupt data). */
|
|
615
|
+
decryptQueue(cipher: string): string | null | undefined;
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* `queueCipher` backed by the platform key store: `createClient({ queueCipher: nativeQueueCipher(NativeModules.LucidaReplay), ... })`.
|
|
619
|
+
* The module is injected so tests and non-native hosts need no React Native. Fails closed: a native error on encrypt stores an
|
|
620
|
+
* empty value (read back as an empty queue), never the plaintext; a native error on decrypt reads as an empty queue.
|
|
621
|
+
*/
|
|
622
|
+
declare function nativeQueueCipher(module: NativeCipherModule | null | undefined): QueueCipher;
|
|
623
|
+
|
|
624
|
+
/** The slice of a react-navigation container ref used for screen tracking (optional peer). */
|
|
625
|
+
interface NavigationRefLike {
|
|
626
|
+
getCurrentRoute(): {
|
|
627
|
+
name: string;
|
|
628
|
+
params?: Record<string, unknown>;
|
|
629
|
+
} | undefined;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Screen tracking for react-navigation. Wire both handlers on `<NavigationContainer>`:
|
|
633
|
+
* `onReady` records the first screen, `onStateChange` records changes (deduped by route name).
|
|
634
|
+
* Route params are not sent (they often hold PII); pass `includeParams` to opt in.
|
|
635
|
+
*/
|
|
636
|
+
declare function screenTracker(client: Pick<NativeClient, "screen">, ref: {
|
|
637
|
+
current: NavigationRefLike | null;
|
|
638
|
+
}, opts?: {
|
|
639
|
+
includeParams?: boolean;
|
|
640
|
+
}): {
|
|
641
|
+
onReady(): void;
|
|
642
|
+
onStateChange(): void;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* `X-Lucida-Distinct-Id` / `X-Lucida-Session-Id` for requests to your own backend hosts so server events join this
|
|
647
|
+
* device's session. Never for the Lucida api host, and only while analytics consent is granted.
|
|
648
|
+
* Servers must treat these headers as client-controlled, never as authentication.
|
|
649
|
+
* The same requests carry W3C `baggage: lucida.session_id=<id>` (merged into the app's own baggage) so Watchtower links its
|
|
650
|
+
* spans, network timings and logs to this session: the Watchtower seam, Lucida itself captures no network or log data.
|
|
651
|
+
*/
|
|
652
|
+
declare function tracingHeaders(hosts: readonly string[], apiHost: string, consent: () => boolean, ids: () => {
|
|
653
|
+
distinctId: string;
|
|
654
|
+
sessionId: string;
|
|
655
|
+
}): (url: string, baggage?: string | null) => Record<string, string>;
|
|
656
|
+
/** `lucida.session_id=<id>` appended to `existing` W3C baggage, replacing a stale Lucida entry. */
|
|
657
|
+
declare function withBaggage(existing: string | null | undefined, sessionId: string): string;
|
|
658
|
+
/** A fetch that adds the tracing headers when the target host matches; everything else passes through untouched. */
|
|
659
|
+
declare function tracedFetch(fetchImpl: typeof fetch, headersFor: (url: string, baggage?: string | null) => Record<string, string>): typeof fetch;
|
|
660
|
+
|
|
661
|
+
interface SpoolEntry {
|
|
662
|
+
id: number;
|
|
663
|
+
sid: string;
|
|
664
|
+
seq: number;
|
|
665
|
+
/** Waiting for the minimum session duration (dropped if the session ends first); false = waiting for upload. */
|
|
666
|
+
held: boolean;
|
|
667
|
+
/** Body size in bytes. */
|
|
668
|
+
n: number;
|
|
669
|
+
/** Number of parts the body is split into (`lc_replay.<id>.<i>`). */
|
|
670
|
+
p: number;
|
|
671
|
+
/** Body length in UTF-16 units: a torn or truncated body no longer adds up and is discarded. */
|
|
672
|
+
len: number;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Durable, ordered spill for replay chunks that cannot be sent yet (min duration, offline, upload backoff).
|
|
676
|
+
* `lc_replay` is the index (JSON list of entries), `lc_replay.<id>.<i>` holds part i of one chunk body (no api key), split
|
|
677
|
+
* into parts under 1 MB. Write order: `lc_replay.pending` names the entry in flight, then its parts, then the index, then
|
|
678
|
+
* pending is cleared; a crash in between leaves parts the index never lists, which load deletes. Pass a storage
|
|
679
|
+
* wrapped with `encryptedStorage(..., [SPOOL_KEY], [`${SPOOL_KEY}.`])` to encrypt at rest. Over `budget` bytes the oldest
|
|
680
|
+
* whole session is dropped (never single chunks, so a kept session has no hole) and `onDrop` reports it.
|
|
681
|
+
*/
|
|
682
|
+
declare function createReplaySpool(storage: Storage, budget?: number, onDrop?: (sid: string, chunks: number, bytes: number) => void): {
|
|
683
|
+
entries: () => readonly SpoolEntry[];
|
|
684
|
+
totalBytes: () => number;
|
|
685
|
+
add(sid: string, seq: number, held: boolean, body: string): void;
|
|
686
|
+
/** The whole body, or null when a part is missing or the length does not add up (torn write): the caller discards it. */
|
|
687
|
+
read(e: SpoolEntry): string | null;
|
|
688
|
+
remove(e: SpoolEntry): void;
|
|
689
|
+
/** Held chunks of every session except `keep`; returned so the caller can give their seq numbers back. */
|
|
690
|
+
removeHeld(keep?: string): SpoolEntry[];
|
|
691
|
+
/** The session reached its minimum duration: its held chunks become plain pending uploads. */
|
|
692
|
+
release(sid: string): void;
|
|
693
|
+
nextSeq: (sid: string) => number;
|
|
694
|
+
clear(): void;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* P-M17 masked screens and P-M46 text patterns. The native encoders apply both (they receive `maskScreen` and
|
|
699
|
+
* `maskTextPatterns` with every capture, screenshots included); [maskSnapshotText] re-applies the patterns to the
|
|
700
|
+
* returned wireframe in JS as well. Remote lists add to the local ones, so they can only tighten.
|
|
701
|
+
*/
|
|
702
|
+
/** Case-insensitive match of the whole screen name; `*` matches any run of characters. Empty list = false. */
|
|
703
|
+
declare function screenMasked(screen: string, patterns: readonly string[]): boolean;
|
|
704
|
+
/** Patterns that fail to compile are dropped, never fatal. No built-in patterns: the server masks emails/cards/phones again. */
|
|
705
|
+
declare function compilePatterns(patterns: readonly string[]): RegExp[];
|
|
706
|
+
/** Every character inside a match of any pattern becomes `*` (whitespace kept), like the native `mask()` helpers. */
|
|
707
|
+
declare function maskPatterns(text: string, patterns: readonly RegExp[]): string;
|
|
708
|
+
/** A copy of a full-snapshot node tree with the patterns applied to text nodes and input `value` attributes. */
|
|
709
|
+
declare function maskSnapshotText(node: unknown, patterns: readonly RegExp[]): unknown;
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* P-M46 `$dead_swipe`: a swipe of at least 48 points after which nothing on screen scrolled, paged or changed (the
|
|
713
|
+
* wireframe fingerprint ~700 ms after the gesture equals the one at its start). Carries geometry only, never text.
|
|
714
|
+
*/
|
|
715
|
+
declare const DEAD_SWIPE_MIN_DISTANCE = 48;
|
|
716
|
+
type SwipeDirection = "left" | "right" | "up" | "down";
|
|
717
|
+
/** Dominant axis of end - start; screen y grows downwards. */
|
|
718
|
+
declare const swipeDirection: (dx: number, dy: number) => SwipeDirection;
|
|
719
|
+
interface DeadSwipeInput {
|
|
720
|
+
screen: string;
|
|
721
|
+
startScreen: string;
|
|
722
|
+
startFingerprint: string;
|
|
723
|
+
endFingerprint: string;
|
|
724
|
+
dx: number;
|
|
725
|
+
dy: number;
|
|
726
|
+
nowMs: number;
|
|
727
|
+
lastAt?: number | undefined;
|
|
728
|
+
}
|
|
729
|
+
/** The `$dead_swipe` properties when the swipe was dead and the 2 s cooldown has passed, else null. */
|
|
730
|
+
declare function deadSwipeDecision(i: DeadSwipeInput): {
|
|
731
|
+
$screen_name: string;
|
|
732
|
+
$swipe_direction: SwipeDirection;
|
|
733
|
+
$swipe_distance: number;
|
|
734
|
+
} | null;
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Draws the current in-app guide step (plain text only). Mount once, as the LAST child of the app root (anchored
|
|
738
|
+
* steps draw over the app from here); needs `guides.enabled`. Anchored steps (tooltip, hotspot, tour with native_id)
|
|
739
|
+
* point at a `<GuideAnchor testID>`; a missing anchor shows the step as a modal.
|
|
740
|
+
*/
|
|
741
|
+
declare function LucidaGuides(): react.JSX.Element | null;
|
|
742
|
+
|
|
743
|
+
interface GuideTokens {
|
|
744
|
+
background?: string;
|
|
745
|
+
text?: string;
|
|
746
|
+
accent?: string;
|
|
747
|
+
radius?: number;
|
|
748
|
+
font?: string;
|
|
749
|
+
}
|
|
750
|
+
type Obj = Record<string, unknown>;
|
|
751
|
+
declare function guideTokens(a: Obj | null | undefined, systemDark: boolean): GuideTokens;
|
|
752
|
+
interface Rect {
|
|
753
|
+
x: number;
|
|
754
|
+
y: number;
|
|
755
|
+
width: number;
|
|
756
|
+
height: number;
|
|
757
|
+
}
|
|
758
|
+
/** Bubble origin: below the target when it fits (or placement is not "top"), else above; centred and clamped. */
|
|
759
|
+
declare function bubbleOrigin(w: number, h: number, r: Rect, bounds: Rect, placement?: string, margin?: number, gap?: number): {
|
|
760
|
+
x: number;
|
|
761
|
+
y: number;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/** What a registered anchor must do: React Native host views implement measureInWindow. */
|
|
765
|
+
interface Measurable {
|
|
766
|
+
measureInWindow(cb: (x: number, y: number, width: number, height: number) => void): void;
|
|
767
|
+
}
|
|
768
|
+
/** Register a view under `id` (the latest mount wins). Returns the unregister. */
|
|
769
|
+
declare function registerAnchor(id: string, m: Measurable): () => void;
|
|
770
|
+
/** The anchor's rect in window coordinates; null when nothing is registered or it has no size (not laid out). */
|
|
771
|
+
declare function measureAnchor(id: string): Promise<Rect | null>;
|
|
772
|
+
/** Wrap a view a guide may anchor to. `testID` is also set on the view (tests / automation keep working). */
|
|
773
|
+
declare function GuideAnchor({ testID, children, style }: {
|
|
774
|
+
testID: string;
|
|
775
|
+
children?: ReactNode;
|
|
776
|
+
style?: unknown;
|
|
777
|
+
}): react.JSX.Element;
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Markers the native encoders already honour (iOS accessibilityIdentifier, Android view tag), applied through `testID`
|
|
781
|
+
* (which also stops React Native from flattening the view away). They replace any `testID` on the wrapper, so put your
|
|
782
|
+
* own `testID` on a child. Unmask never overrides mask or secure fields, at any depth.
|
|
783
|
+
*/
|
|
784
|
+
declare const REPLAY_MARKERS: {
|
|
785
|
+
readonly ios: {
|
|
786
|
+
readonly mask: "lucida-sensitive";
|
|
787
|
+
readonly unmask: "lucida-unmask";
|
|
788
|
+
};
|
|
789
|
+
readonly android: {
|
|
790
|
+
readonly mask: "lc-mask";
|
|
791
|
+
readonly unmask: "lc-unmask";
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
declare const replayMarker: (kind: "mask" | "unmask", os?: string) => string;
|
|
795
|
+
interface MarkProps {
|
|
796
|
+
children?: ReactNode;
|
|
797
|
+
style?: unknown;
|
|
798
|
+
}
|
|
799
|
+
/** Never show this subtree in replay: text is dropped and images/inputs are masked, whatever the global switches say. */
|
|
800
|
+
declare function LucidaMask({ children, style }: MarkProps): react.JSX.Element;
|
|
801
|
+
/** Show this subtree's text, input values (never secure ones) and images in replay even with the global masks on. */
|
|
802
|
+
declare function LucidaUnmask({ children, style }: MarkProps): react.JSX.Element;
|
|
803
|
+
/**
|
|
804
|
+
* Wrap the app root to feed taps to `client.replay.tap` (the replay touch overlay) and swipes to the `$dead_swipe` check.
|
|
805
|
+
* It only observes touch events bubbling up from children; it never becomes the responder, so no touch is cancelled or delayed.
|
|
806
|
+
*/
|
|
807
|
+
declare function LucidaTouchCapture({ children, style }: MarkProps): react.JSX.Element;
|
|
808
|
+
|
|
809
|
+
type Flags = Record<string, FlagValue>;
|
|
810
|
+
interface LucidaProviderProps {
|
|
811
|
+
client?: NativeClient;
|
|
812
|
+
apiKey?: string;
|
|
813
|
+
options?: NativeConfig;
|
|
814
|
+
children?: ReactNode;
|
|
815
|
+
}
|
|
816
|
+
declare function LucidaProvider({ client: given, apiKey, options, children }: LucidaProviderProps): react.JSX.Element;
|
|
817
|
+
|
|
818
|
+
declare function useLucida(): NativeClient | null;
|
|
819
|
+
/** Stable capture function; a no-op until the client is ready. */
|
|
820
|
+
declare function useCapture(): NativeClient["capture"];
|
|
821
|
+
/** Recording control; inert until the client is ready. */
|
|
822
|
+
declare function useReplay(): ReplayControl;
|
|
823
|
+
/** Records a `$screen` event when `name` changes (for apps not using react-navigation). */
|
|
824
|
+
declare function useScreen(name: string, props?: Record<string, unknown>): void;
|
|
825
|
+
/** Flag value from provider state; the exposure is recorded after mount, never during render. */
|
|
826
|
+
declare function useFeatureFlag(key: string): FlagValue | undefined;
|
|
827
|
+
declare function useFeatureFlagVariant(key: string): string | undefined;
|
|
828
|
+
declare function useFeatureFlagPayload(key: string): unknown;
|
|
829
|
+
declare function useActiveFeatureFlags(): string[];
|
|
830
|
+
interface FeatureGateProps {
|
|
831
|
+
flag: string;
|
|
832
|
+
variant?: string;
|
|
833
|
+
fallback?: ReactNode;
|
|
834
|
+
children?: ReactNode;
|
|
835
|
+
}
|
|
836
|
+
declare function FeatureGate({ flag, variant, fallback, children }: FeatureGateProps): react.JSX.Element;
|
|
837
|
+
|
|
838
|
+
export { type AppStateLike, type AppStateStatus, type AsyncStorageLike, type Autocapture, type AutocaptureConfig, type Bootstrap, type CaptureOptions, DEAD_SWIPE_MIN_DISTANCE, type DeadSwipeInput, type ExposureKey, FeatureGate, type FeatureGateProps, type FiberLike, type FlagOptions, type Flags, GuideAnchor, type GuideAnchorSpec, type GuideButton, type GuideStep, type GuideTokens, type GuideView, type GuidesDeps, type GuidesService, type InstallInfo, LIB, LIB_VERSION, LucidaGuides, LucidaMask, LucidaProvider, type LucidaProviderProps, LucidaTouchCapture, LucidaUnmask, type Measurable, type MobileGuide, type NativeCipherModule, type NativeClient, type NativeConfig, type NativeInstallModule, type NativeReplayModule, type NativeSensitiveModule, type NavigationRefLike, PERSISTED_KEYS, PersonMode, type QueueCipher, REPLAY_MARKERS, type RageTapConfig, type ReplayConfig, type ReplayControl, type ReplayService, SENSITIVE_MAX_PLAIN_BYTES, type SensitiveCapture, type SpoolEntry, type SwipeDirection, type TapElement, type TouchEventLike, appLifecycle, appStoreInstallProps, asyncStorageAdapter, bubbleOrigin, chainFromFiber, compilePatterns, createAutocapture, createClient, createGuides, createReplay, createReplaySpool, createSensitiveCapture, deadSwipeDecision, effectiveMinDurationMs, effectiveSampleRate, encryptSensitive, encryptedStorage, exposureKeyOf, fetchTransport, guideAnchor, guideButtons, guideTokens, installReferrerProps, maskPatterns, maskSnapshotText, matches as matchGuideScreen, measureAnchor, memoryStorage, nativeQueueCipher, registerAnchor, replayMarker, resolveInstallProps, runHooks, sampleHook, screenAllowed, screenExcluded, screenMasked, screenTracker, splitBatches, swipeDirection, touchHandler, tracedFetch, tracingHeaders, truncateUtf8, useActiveFeatureFlags, useCapture, useFeatureFlag, useFeatureFlagPayload, useFeatureFlagVariant, useLucida, useReplay, useScreen, withBaggage, withTimeout };
|