@cxpinsight/react-native 0.2.0 → 0.4.0

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +159 -0
  2. package/README.md +25 -0
  3. package/lib/CXPinsight.d.ts +24 -1
  4. package/lib/CXPinsight.d.ts.map +1 -1
  5. package/lib/CXPinsight.js +137 -8
  6. package/lib/CXPinsight.js.map +1 -1
  7. package/lib/CXPinsightProvider.d.ts.map +1 -1
  8. package/lib/CXPinsightProvider.js +5 -1
  9. package/lib/CXPinsightProvider.js.map +1 -1
  10. package/lib/core/autoTapCapture.d.ts +15 -0
  11. package/lib/core/autoTapCapture.d.ts.map +1 -0
  12. package/lib/core/autoTapCapture.js +242 -0
  13. package/lib/core/autoTapCapture.js.map +1 -0
  14. package/lib/core/consent.d.ts +21 -0
  15. package/lib/core/consent.d.ts.map +1 -0
  16. package/lib/core/consent.js +131 -0
  17. package/lib/core/consent.js.map +1 -0
  18. package/lib/core/eventQueue.d.ts.map +1 -1
  19. package/lib/core/eventQueue.js +7 -0
  20. package/lib/core/eventQueue.js.map +1 -1
  21. package/lib/core/mobileRecorder.d.ts +6 -0
  22. package/lib/core/mobileRecorder.d.ts.map +1 -1
  23. package/lib/core/mobileRecorder.js +54 -2
  24. package/lib/core/mobileRecorder.js.map +1 -1
  25. package/lib/core/networkCapture.d.ts +10 -1
  26. package/lib/core/networkCapture.d.ts.map +1 -1
  27. package/lib/core/networkCapture.js +53 -5
  28. package/lib/core/networkCapture.js.map +1 -1
  29. package/lib/core/triggerEngine.js +3 -3
  30. package/lib/core/triggerEngine.js.map +1 -1
  31. package/lib/instrumented/Pressable.js +4 -0
  32. package/lib/instrumented/Pressable.js.map +1 -1
  33. package/lib/instrumented/ScrollView.js +4 -0
  34. package/lib/instrumented/ScrollView.js.map +1 -1
  35. package/lib/instrumented/TextInput.js +4 -0
  36. package/lib/instrumented/TextInput.js.map +1 -1
  37. package/lib/instrumented/TouchableOpacity.js +4 -0
  38. package/lib/instrumented/TouchableOpacity.js.map +1 -1
  39. package/lib/setup/index.d.ts +0 -1
  40. package/lib/setup/index.d.ts.map +1 -1
  41. package/lib/setup/index.js +4 -6
  42. package/lib/setup/index.js.map +1 -1
  43. package/lib/survey/SurveyModal.d.ts.map +1 -1
  44. package/lib/survey/SurveyModal.js +7 -1
  45. package/lib/survey/SurveyModal.js.map +1 -1
  46. package/lib/survey/expressionEval.d.ts +13 -9
  47. package/lib/survey/expressionEval.d.ts.map +1 -1
  48. package/lib/survey/expressionEval.js +16 -284
  49. package/lib/survey/expressionEval.js.map +1 -1
  50. package/lib/survey/interpolate.d.ts.map +1 -1
  51. package/lib/survey/interpolate.js +9 -27
  52. package/lib/survey/interpolate.js.map +1 -1
  53. package/lib/survey/questions/TextQuestion.d.ts +9 -1
  54. package/lib/survey/questions/TextQuestion.d.ts.map +1 -1
  55. package/lib/survey/questions/TextQuestion.js +15 -3
  56. package/lib/survey/questions/TextQuestion.js.map +1 -1
  57. package/lib/survey/validate.js +10 -0
  58. package/lib/survey/validate.js.map +1 -1
  59. package/lib/types.d.ts +3 -0
  60. package/lib/types.d.ts.map +1 -1
  61. package/lib/types.js +3 -0
  62. package/lib/types.js.map +1 -1
  63. package/package.json +6 -2
  64. package/lib/setup/TouchPatcher.d.ts +0 -27
  65. package/lib/setup/TouchPatcher.d.ts.map +0 -1
  66. package/lib/setup/TouchPatcher.js +0 -182
  67. package/lib/setup/TouchPatcher.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,159 @@
1
+ # Changelog
2
+
3
+ ## 0.4.0
4
+
5
+ ### ⚠️ New required dependency
6
+
7
+ This release depends on **`@cxpinsight/survey-spec`**, which npm installs
8
+ automatically. It holds the survey behaviour that must be identical whether a
9
+ respondent answers on a link, a website or in your app — expression evaluation
10
+ and variable resolution — so the three renderers cannot drift apart again.
11
+
12
+ ### Fixed
13
+ - **`{{user.name}}` rendered the entire namespace object.** Text interpolation
14
+ fell back to the namespace when a dotted key missed, so
15
+ `"Hi {{user.name}}"` rendered `Hi {"plan":"pro","name":"Sam"}` — every field of
16
+ that namespace, on screen, to the respondent. Conditions had always resolved
17
+ the path correctly, so the same reference worked in a `showWhen` and not in
18
+ question text.
19
+ - **`minLength` was ignored** unless expressed through the `rules[]` array, so a
20
+ length floor set in the builder was enforced on links and silently dropped
21
+ here.
22
+ - **The builder's "Answer format" picker had no effect.** A text question with
23
+ `inputType: 'email'` got the default keyboard, no autofill and no validation.
24
+ Now maps to `keyboardType`, `textContentType`, `autoComplete` and
25
+ `secureTextEntry`.
26
+ - Variable namespaces (`{{user.plan}}`, `{{session.*}}`) resolve consistently,
27
+ and an unparseable condition is ignored rather than hiding its question.
28
+
29
+ ### Added
30
+ - **Automatic tap capture.** The babel plugin rewrites four component types in
31
+ your own source; anything inside a UI library was invisible, because it skips
32
+ `node_modules` by design. Taps are now captured wherever a touch component is
33
+ rendered from. `autoCaptureTaps: false` opts out.
34
+
35
+ **Expect event volume to rise** if you relied on the plugin alone — taps inside
36
+ UI-library components were previously not recorded at all.
37
+ - **`sdk_diagnostic` events.** The SDK patches React Native internals for scroll,
38
+ tap and network capture. Those used to fail silently on an RN upgrade; a
39
+ failure is now reported as an event, so it appears in your dashboard rather
40
+ than only in a device log.
41
+
42
+ ### Removed
43
+ - `TouchPatcher` — never called, and it patched `React.createElement`, which the
44
+ automatic JSX runtime bypasses. Not part of the public API.
45
+
46
+ ## Unreleased
47
+
48
+ ### Added
49
+ - **Automatic tap capture.** The babel plugin rewrites four component types in
50
+ your own source, which leaves three gaps: `TouchableHighlight`,
51
+ `TouchableWithoutFeedback` and `Button` are not rewritten; anything inside
52
+ `node_modules` is skipped by design, so every tappable in a UI library was
53
+ invisible; and an app that skips the babel step got nothing. Taps are now
54
+ captured by patching the JSX factory, so a touch component is instrumented
55
+ wherever it is rendered from. `autoCaptureTaps: false` opts out.
56
+
57
+ Components the babel plugin already rewrote carry a marker and are skipped, so
58
+ a tap is never recorded twice.
59
+
60
+ **Expect event volume to rise** on apps that relied on the plugin alone —
61
+ taps inside UI-library components were previously not recorded at all.
62
+
63
+ ### Fixed
64
+ - `showWhen` conditions now behave identically on web, links and mobile. An
65
+ expression that cannot be parsed is ignored rather than treated as false: a
66
+ hidden question is invisible in the response data, so a typo used to cause
67
+ silent data loss. Variable namespaces (`{{user.plan}}`, `{{session.*}}`) also
68
+ resolve consistently across all three renderers.
69
+
70
+ ## 0.3.0
71
+
72
+ ### Added
73
+ - **Consent API** — `optIn()`, `optOut()`, `consent(granted)` and `getConsent()`,
74
+ with a `requireConsent` init flag. Same names and semantics as the web SDK, so
75
+ an app on both channels wires up one concept. With `requireConsent` on, events
76
+ are *held* in the queue while a decision is pending rather than dropped, so a
77
+ visitor who accepts a banner a few seconds in keeps the opening of their
78
+ session. Declining clears the buffer and stops recording and network capture.
79
+ The decision persists across launches.
80
+ - `CHANGELOG.md`, shipped with the package.
81
+
82
+ ### Changed
83
+ - **Network capture scrubs identifiers out of request paths.** Stripping the
84
+ query string was never enough: REST paths carry identity directly, so
85
+ `/customers/user@example.com/orders` merely moved the personal data one
86
+ delimiter left. Email, numeric-id, UUID, ObjectId and opaque-token segments now
87
+ collapse to `:id`.
88
+
89
+ This changes the shape of captured `api_call` data: routes that previously
90
+ appeared as thousands of distinct literal paths now aggregate as one. That is
91
+ the intent — per-user paths were never useful analytics — but it is visible in
92
+ the dashboard, so it is a deliberate break rather than a silent one.
93
+ - Setup mode no longer writes to `console` outside debug builds.
94
+
95
+ ### Fixed
96
+ - **Events tracked before `init()` resolved were dropped.** `init()` awaits a
97
+ network fetch and several AsyncStorage reads, so on a cold start there is a
98
+ real window in which an app's own startup tracking vanished with only a console
99
+ warning. Those calls are now held and replayed once initialization completes,
100
+ carrying their **original timestamp** so a replayed event lands where it
101
+ actually happened rather than bunching at the end of init. Bounded at 50.
102
+
103
+ ### Changed
104
+ - **Replay screenshots are bandwidth-bounded.** The one-per-second capture
105
+ throttle is a rate limit, not a bandwidth limit, and screenshots are base64
106
+ JPEGs inside a JSON chunk — a busy screen could push multi-megabyte uploads
107
+ that the end user pays for on a metered connection. A single frame over ~400 KB
108
+ is now dropped rather than shrunk (quality belongs to the host app's capture
109
+ provider), and a ~8 MB per-recording budget applies, after which the timeline
110
+ continues *without* screenshots rather than the recording being abandoned.
111
+ Readable via `shotStats()`.
112
+ - **The builder's "Answer format" picker now works.** A text question with
113
+ `inputType: 'email'` (or `tel`, `url`, `number`, `password`) was rendered with
114
+ the default keyboard, no autofill and no validation — the picker only worked on
115
+ the React renderer. Mobile now maps it to `keyboardType`, `textContentType`,
116
+ `autoComplete` and `secureTextEntry`, and forces single-line for a typed field.
117
+
118
+ ### Unchanged
119
+ - `requireConsent` defaults to `false`. An app that does not set it behaves
120
+ exactly as before, and a test asserts that.
121
+
122
+ ## 0.2.0
123
+
124
+ First published release.
125
+
126
+ ### Added
127
+ - Compiled CommonJS + type declarations. Previously `main` and `types` pointed at
128
+ `src/index.ts`, so the package shipped raw TypeScript: consumers' Metro had to
129
+ transform it out of `node_modules`, their Jest needed a
130
+ `transformIgnorePatterns` entry, and the package's own type errors landed in
131
+ their build output.
132
+ - README, LICENSE, and a stable deep-import path at
133
+ `@cxpinsight/react-native/instrumented/<Component>` for the Babel plugin and
134
+ Metro helper.
135
+
136
+ ### Fixed
137
+ - **`app_foregrounded` never fired.** `lifecycle` imported a
138
+ `refreshNetworkState` that was never exported, so every foreground threw an
139
+ uncaught `TypeError` inside the `AppState` handler, before the event was
140
+ emitted.
141
+ - **No dashboard setting reached the app.** The server SDK config was read one
142
+ level too shallow (`res.data` rather than `res.data.data`), so the recording
143
+ toggle, sample rate, session timeout and flush cadence all resolved to
144
+ `undefined`.
145
+ - **Offline flushing spun.** A failed batch was re-queued and immediately
146
+ retried while the queue was over threshold — a tight loop against a dead
147
+ network. Now full-jitter exponential backoff, 1s base to a 5 min ceiling.
148
+ - **The event backlog was unbounded.** `MAX_BATCH_SIZE` capped the request, not
149
+ the queue. Capped at 1000, oldest-first, with drops counted.
150
+ - Failed replay chunks vanished silently — `sdkFetch` resolves with
151
+ `{ success: false }` rather than throwing, so the surrounding `catch` was
152
+ unreachable and the result was never checked.
153
+ - A phantom `scanViewHierarchy` re-export (the real name is
154
+ `scanNativeViewHierarchy`).
155
+
156
+ ### Changed
157
+ - Event-queue and session writes are coalesced instead of one AsyncStorage write
158
+ per event and per interaction. Backgrounding persists unconditionally, which is
159
+ how a session normally ends.
package/README.md CHANGED
@@ -28,6 +28,8 @@ module.exports = function (api) {
28
28
 
29
29
  The plugin rewrites `TouchableOpacity`, `Pressable`, `TextInput` and `ScrollView` imports in **your** source only — never inside `node_modules`, and never inside the SDK itself.
30
30
 
31
+ Taps on everything else — `TouchableHighlight`, `Button`, and any touchable inside a UI library such as React Native Paper or gluestack — are captured automatically by patching the JSX factory, so those need no imports changed either. Set `autoCaptureTaps: false` to rely on the plugin alone. A tap is never counted twice: components the plugin rewrote are marked and skipped.
32
+
31
33
  ### 2. Initialise at startup
32
34
 
33
35
  ```js
@@ -79,6 +81,24 @@ CXPinsight.identify({ id: user.id, email: user.email, name: user.name });
79
81
 
80
82
  Events recorded before `identify()` are re-attributed to the user automatically, so one visitor does not show up as two.
81
83
 
84
+ ### 5. Consent, if you show a banner
85
+
86
+ ```js
87
+ CXPinsight.init({ /* ... */ requireConsent: true });
88
+
89
+ // in your banner's handlers
90
+ CXPinsight.optIn(); // or CXPinsight.consent(true)
91
+ CXPinsight.optOut(); // or CXPinsight.consent(false)
92
+ ```
93
+
94
+ With `requireConsent` on, nothing leaves the device until the visitor accepts.
95
+ Events are *held* rather than dropped in the meantime, so someone who accepts a
96
+ few seconds in keeps the opening of their session. Declining clears what was
97
+ buffered and stops session recording and network capture. The decision persists,
98
+ so the banner is not shown again on the next launch.
99
+
100
+ Omit `requireConsent` and behaviour is unchanged — capture starts at `init()`.
101
+
82
102
  ## API
83
103
 
84
104
  | Method | Purpose |
@@ -91,6 +111,8 @@ Events recorded before `identify()` are re-attributed to the user automatically,
91
111
  | `showSurvey(surveyId)` | Present a survey immediately. |
92
112
  | `startRecording()` / `stopRecording()` | Session replay, for hosts that gate on their own consent UI. |
93
113
  | `enterSetupMode()` / `exitSetupMode()` | Visual element mapping — tap an element to make it trackable. |
114
+ | `optIn()` / `optOut()` / `consent(bool)` | Grant or revoke consent from your banner. |
115
+ | `getConsent()` | `'granted'`, `'denied'`, or `null` when undecided. |
94
116
  | `flush()` | Force-send queued events. |
95
117
  | `reset()` | Clear the stored identity, session and frequency caps. |
96
118
 
@@ -105,6 +127,8 @@ Events recorded before `identify()` are re-attributed to the user automatically,
105
127
  | `recordMaskInputs` | `true` | Never persist typed text. |
106
128
  | `captureScreen` | — | Screenshot provider; omit for a timeline-only replay. |
107
129
  | `captureNetwork` | `true` | Sanitised `api_call` events — method, host, path, status. Never bodies or headers. |
130
+ | `requireConsent` | `false` | Hold all capture until `optIn()`. |
131
+ | `autoCaptureTaps` | `true` | Capture taps on touch components the babel plugin cannot reach, including inside UI libraries. |
108
132
  | `sessionTimeout` | 30 min | Inactivity before a new session starts. |
109
133
  | `flushInterval` / `flushThreshold` | 30 s / 10 | Event batching. |
110
134
  | `theme` | OS | `'light'` / `'dark'` if your app has its own toggle. |
@@ -119,6 +143,7 @@ Values omitted here are filled from the application's dashboard settings at star
119
143
  - Network capture records method, host, path, status and duration only. Query strings are stripped; request and response bodies and headers are never read.
120
144
  - The SDK's own backend calls are excluded from network capture.
121
145
  - Events are queued locally and persist across launches, so nothing is lost offline.
146
+ - With `requireConsent`, nothing is transmitted until the visitor accepts, and declining discards what was buffered.
122
147
 
123
148
  ## Requirements
124
149
 
@@ -1,4 +1,5 @@
1
1
  import type { CXPinsightConfig, CXPinsightCallbacks, UserIdentity, FunnelStepOptions, ExperimentOptions, ConversionOptions } from './types';
2
+ import { type ConsentState } from './core/consent';
2
3
  type SurveyPresenter = (surveyId: string) => void;
3
4
  type SetupPresenter = (show: boolean) => void;
4
5
  declare class CXPinsightSDK {
@@ -20,6 +21,9 @@ declare class CXPinsightSDK {
20
21
  private resumeEngagementClock;
21
22
  private getEngagementMs;
22
23
  private static errorHandlerInstalled;
24
+ private pending;
25
+ private static readonly MAX_PENDING;
26
+ private pendingDropped;
23
27
  /** Initialize the SDK. Call once at app startup. */
24
28
  init(config: CXPinsightConfig, callbacks?: CXPinsightCallbacks): Promise<void>;
25
29
  /**
@@ -32,6 +36,8 @@ declare class CXPinsightSDK {
32
36
  private applyServerSdkConfig;
33
37
  /** Roll the recording + engagement clock onto a freshly-started session. */
34
38
  private onSessionRollover;
39
+ /** Emit everything buffered before init() resolved, oldest first. */
40
+ private flushPending;
35
41
  /** Wire native signals → typed analytics events (shared taxonomy). */
36
42
  private startAutoEvents;
37
43
  /** Track a custom event */
@@ -41,7 +47,9 @@ declare class CXPinsightSDK {
41
47
  * gateway, or localhost on a physical device). Never throws or blocks init.
42
48
  */
43
49
  private verifyConnectivity;
44
- track(eventName: string, data?: Record<string, unknown>, eventType?: string, teeToRecording?: boolean): void;
50
+ track(eventName: string, data?: Record<string, unknown>, eventType?: string, teeToRecording?: boolean,
51
+ /** Internal: preserves the original time of a call buffered before init(). */
52
+ eventTimestamp?: string): void;
45
53
  /** Track a screen view (triggers remote page_view rules) */
46
54
  trackScreen(screenName: string, data?: Record<string, unknown>): void;
47
55
  /** Identify the current user */
@@ -73,6 +81,21 @@ declare class CXPinsightSDK {
73
81
  /** Stop mobile session recording and flush the final chunk. */
74
82
  stopRecording(): Promise<void>;
75
83
  /** Reset all SDK state (call on logout) */
84
+ /**
85
+ * Consent — the host drives these from its own banner. Same names and
86
+ * semantics as the web SDK's CXPinsight('optIn' | 'optOut' | 'consent').
87
+ *
88
+ * With requireConsent on, events accumulate in the queue while a decision is
89
+ * pending and are sent once granted, so accepting the banner a few seconds in
90
+ * keeps the opening of the session. Denial clears the buffer and stops capture.
91
+ */
92
+ optIn(): void;
93
+ optOut(): void;
94
+ consent(granted: boolean): void;
95
+ getConsent(): ConsentState;
96
+ isConsentRequired(): boolean;
97
+ /** Start or tear down capture when consent flips. */
98
+ private onConsentChange;
76
99
  reset(): Promise<void>;
77
100
  /** Manual flush for testing */
78
101
  flush(): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"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"}
package/lib/CXPinsight.js CHANGED
@@ -49,6 +49,8 @@ const interactionObserver_1 = require("./instrumented/interactionObserver");
49
49
  const lifecycle_1 = require("./core/lifecycle");
50
50
  const networkCapture_1 = require("./core/networkCapture");
51
51
  const scrollCapture_1 = require("./core/scrollCapture");
52
+ const autoTapCapture_1 = require("./core/autoTapCapture");
53
+ const consent_1 = require("./core/consent");
52
54
  // Canonical eventType per event name — keeps mobile events aligned with the shared
53
55
  // engagement taxonomy (docs/engagement-vocabulary-v1.md) instead of everything being
54
56
  // 'user_action'. Unknown names fall back to 'user_action'.
@@ -85,6 +87,18 @@ class CXPinsightSDK {
85
87
  this.engagementStartTs = 0;
86
88
  this.engagementAccumMs = 0;
87
89
  this.engagementActive = false;
90
+ // Calls made before init() resolves. init() awaits a network fetch and several
91
+ // AsyncStorage reads, so there is a real window — hundreds of milliseconds on a
92
+ // cold start — in which an app's own startup tracking is simply lost. These
93
+ // used to be dropped with a console warning; the web SDK has always buffered
94
+ // its pre-load calls through the CXPinsight.q command queue, and this is the
95
+ // mobile equivalent.
96
+ //
97
+ // The ORIGINAL timestamp is kept, so a replayed event lands where it actually
98
+ // happened rather than bunching at the end of init. Bounded, because an app
99
+ // that never calls init() must not grow this forever.
100
+ this.pending = [];
101
+ this.pendingDropped = 0;
88
102
  }
89
103
  startEngagementClock() { this.engagementStartTs = Date.now(); this.engagementAccumMs = 0; this.engagementActive = true; }
90
104
  pauseEngagementClock() { if (this.engagementActive) {
@@ -134,6 +148,9 @@ class CXPinsightSDK {
134
148
  const savedIdentity = await storage.get(types_1.STORAGE_KEYS.USER_IDENTITY);
135
149
  if (savedIdentity)
136
150
  this.userIdentity = savedIdentity;
151
+ // Restore any prior consent decision before a single capture path starts.
152
+ await (0, consent_1.initConsent)(config.requireConsent);
153
+ (0, consent_1.setConsentHandler)((granted) => { void this.onConsentChange(granted); });
137
154
  // Start session
138
155
  await (0, session_1.initSession)(config.sessionTimeout);
139
156
  // Start event queue
@@ -151,7 +168,7 @@ class CXPinsightSDK {
151
168
  // Start mobile session (timeline) recording when opted in via config. Hosts
152
169
  // that gate on their own consent UI should omit `record` and call
153
170
  // CXPinsight.startRecording() after consent instead.
154
- if (config.record) {
171
+ if (config.record && (0, consent_1.mayCapture)()) {
155
172
  recorder.startRecording({
156
173
  sessionId: (0, session_1.getSessionId)(),
157
174
  userId: this.userId,
@@ -196,6 +213,10 @@ class CXPinsightSDK {
196
213
  is_first_visit: isFirstVisit,
197
214
  }, 'navigation', false);
198
215
  }
216
+ // Replay anything the app tracked while init() was still resolving. After
217
+ // session_start, so the ordering a consumer sees matches what happened: the
218
+ // session begins, then the events that occurred during startup.
219
+ this.flushPending();
199
220
  // When an inactive session rolls over to a new one, start a fresh recording so a
200
221
  // session maps to one recording (prevents an ever-growing single recording).
201
222
  (0, session_1.setRolloverHandler)((newSessionId, prevSessionId) => {
@@ -281,6 +302,22 @@ class CXPinsightSDK {
281
302
  this.startEngagementClock(); // reset engagement time for the new session
282
303
  this.track('session_start', { sessionId: newSessionId }, 'navigation', false);
283
304
  }
305
+ /** Emit everything buffered before init() resolved, oldest first. */
306
+ flushPending() {
307
+ if (!this.pending.length && !this.pendingDropped)
308
+ return;
309
+ const held = this.pending;
310
+ this.pending = [];
311
+ this.log(`Replaying ${held.length} event(s) tracked before init completed`);
312
+ if (this.pendingDropped) {
313
+ console.warn(`[CXPinsight] ${this.pendingDropped} event(s) tracked before init() were dropped — ` +
314
+ `more than ${CXPinsightSDK.MAX_PENDING} arrived before initialization finished. ` +
315
+ `Call init() earlier in startup.`);
316
+ this.pendingDropped = 0;
317
+ }
318
+ for (const e of held)
319
+ this.track(e.name, e.data, e.type, e.tee, e.ts);
320
+ }
284
321
  /** Wire native signals → typed analytics events (shared taxonomy). */
285
322
  startAutoEvents() {
286
323
  // Scroll depth from the app's ORGANIC <ScrollView>s (and FlatList /
@@ -289,8 +326,32 @@ class CXPinsightSDK {
289
326
  // app-code restructuring the install snippet shouldn't demand. Without it
290
327
  // `percent_scrolled` was always 0, so scroll-depth definitions never fired.
291
328
  // Returns false if RN internals have moved; capture is simply off then.
292
- if (!(0, scrollCapture_1.installScrollCapture)()) {
293
- this.log('Scroll capture unavailable on this React Native version');
329
+ // Three patches reach into React Native internals: the ScrollView module
330
+ // getter, the JSX factory, and global fetch/XHR. Each is guarded and each
331
+ // degrades to a no-op if RN moves — which is the failure mode that goes
332
+ // unnoticed for weeks, because a customer losing scroll depth sees no error,
333
+ // just a chart that stopped moving.
334
+ //
335
+ // Report them as an EVENT, not only a console warning. A device log is not
336
+ // somewhere anyone looks; the dashboard is. `teeToRecording` is false — this
337
+ // is diagnostics about the SDK, not respondent behaviour.
338
+ const capture = {};
339
+ capture.scroll = (0, scrollCapture_1.installScrollCapture)();
340
+ if (!capture.scroll) {
341
+ console.warn('[CXPinsight] Scroll capture could not install on this React Native version. ' +
342
+ 'Scroll-depth events and any definition that depends on percent_scrolled will not fire.');
343
+ }
344
+ // Taps on every touch component, wherever it is rendered from — including
345
+ // inside UI libraries, which the babel plugin cannot reach because it skips
346
+ // node_modules by design. Components the plugin already rewrote carry a
347
+ // marker and are skipped here, so a tap is never counted twice.
348
+ if (this.config?.autoCaptureTaps !== false) {
349
+ capture.tap = (0, autoTapCapture_1.installAutoTapCapture)(this.config?.debug);
350
+ if (!capture.tap) {
351
+ console.warn('[CXPinsight] Automatic tap capture could not install — no JSX factory was patchable. ' +
352
+ 'Taps are still captured for components the babel plugin rewrites; anything inside a ' +
353
+ 'UI library will not be. This usually means an unexpected React build.');
354
+ }
294
355
  }
295
356
  // Interactions → click / engagement / form events. tee=false: these are already
296
357
  // in the recording as interaction frames, so don't duplicate them there.
@@ -352,6 +413,19 @@ class CXPinsightSDK {
352
413
  // value_change / blur → captured in the recording only, no analytics event
353
414
  }
354
415
  });
416
+ // One event carrying which internals patches took. Absent fields mean the
417
+ // capture was not requested; false means it was requested and FAILED, which
418
+ // is the case worth alerting on.
419
+ const failed = Object.keys(capture).filter((k) => capture[k] === false);
420
+ if (failed.length) {
421
+ this.track('sdk_diagnostic', {
422
+ issue: 'capture_patch_failed',
423
+ failed,
424
+ capture,
425
+ reactNativeVersion: (0, deviceInfo_1.getDeviceInfo)().osVersion,
426
+ sdk: 'react-native',
427
+ }, 'error', false);
428
+ }
355
429
  // App lifecycle → navigation events (app_opened / foregrounded / backgrounded).
356
430
  // Also pause/resume the engagement clock so time-on-app excludes backgrounded time.
357
431
  (0, lifecycle_1.startLifecycleTracking)((evt) => {
@@ -363,9 +437,9 @@ class CXPinsightSDK {
363
437
  });
364
438
  // Network capture → sanitized api_call events (method/host/path/status/duration).
365
439
  // On by default; set captureNetwork:false to disable.
366
- if (this.config?.captureNetwork !== false) {
440
+ if (this.config?.captureNetwork !== false && (0, consent_1.mayCapture)()) {
367
441
  this.log('Network capture enabled');
368
- (0, networkCapture_1.startNetworkCapture)(this.config.apiBase, (call) => {
442
+ capture.network = (0, networkCapture_1.startNetworkCapture)(this.config.apiBase, (call) => {
369
443
  this.log('api_call captured:', call.method, call.path, call.status);
370
444
  // Static eventName `api_call` (was `"${METHOD} ${PATH}"` — dynamic per URL,
371
445
  // which exploded the observed-events catalog into thousands of unique
@@ -435,14 +509,22 @@ class CXPinsightSDK {
435
509
  console.warn(`[CXPinsight] Could not reach apiBase "${base}" (${msg}). It must be your CXPinsight gateway (usually :3000), reachable from the device — on a physical device use your machine's LAN IP, not localhost.`);
436
510
  }
437
511
  }
438
- track(eventName, data, eventType, teeToRecording = true) {
512
+ track(eventName, data, eventType, teeToRecording = true,
513
+ /** Internal: preserves the original time of a call buffered before init(). */
514
+ eventTimestamp) {
439
515
  if (!this.initialized || !this.config) {
440
- console.warn('[CXPinsight] track() called but SDK not initialized call ignored. Event:', eventName);
516
+ // Held, not dropped. Drained by flushPending() once init() completes.
517
+ if (this.pending.length < CXPinsightSDK.MAX_PENDING) {
518
+ this.pending.push({ name: eventName, data, type: eventType, tee: teeToRecording, ts: new Date().toISOString() });
519
+ }
520
+ else {
521
+ this.pendingDropped += 1;
522
+ }
441
523
  return;
442
524
  }
443
525
  (0, session_1.touchSession)();
444
526
  const device = (0, deviceInfo_1.getDeviceInfo)();
445
- const now = new Date().toISOString();
527
+ const now = eventTimestamp ?? new Date().toISOString();
446
528
  // Explicit type wins; otherwise derive from the shared taxonomy by name.
447
529
  const resolvedType = eventType || EVENT_TYPE_BY_NAME[eventName] || 'user_action';
448
530
  // Identified user traits (from identify()) to stamp on EVERY event — including the
@@ -729,12 +811,57 @@ class CXPinsightSDK {
729
811
  await recorder.stopRecording();
730
812
  }
731
813
  /** Reset all SDK state (call on logout) */
814
+ /**
815
+ * Consent — the host drives these from its own banner. Same names and
816
+ * semantics as the web SDK's CXPinsight('optIn' | 'optOut' | 'consent').
817
+ *
818
+ * With requireConsent on, events accumulate in the queue while a decision is
819
+ * pending and are sent once granted, so accepting the banner a few seconds in
820
+ * keeps the opening of the session. Denial clears the buffer and stops capture.
821
+ */
822
+ optIn() { (0, consent_1.grantConsent)(); }
823
+ optOut() { (0, consent_1.revokeConsent)(); }
824
+ consent(granted) { granted ? (0, consent_1.grantConsent)() : (0, consent_1.revokeConsent)(); }
825
+ getConsent() { return (0, consent_1.getConsent)(); }
826
+ isConsentRequired() { return (0, consent_1.consentRequired)(); }
827
+ /** Start or tear down capture when consent flips. */
828
+ async onConsentChange(granted) {
829
+ this.log('Consent', granted ? 'granted' : 'revoked');
830
+ if (granted) {
831
+ // Send whatever accumulated while the banner was up.
832
+ await (0, eventQueue_1.flush)();
833
+ if (this.config?.record) {
834
+ recorder.startRecording({
835
+ sessionId: (0, session_1.getSessionId)(),
836
+ userId: this.userId,
837
+ applicationId: this.config.applicationId,
838
+ sampleRate: this.config.recordingSampleRate,
839
+ maskInputs: this.config.recordMaskInputs,
840
+ captureScreen: this.config.captureScreen,
841
+ }).catch((err) => this.log('Failed to start recording after consent:', err));
842
+ }
843
+ if (this.config && this.config.captureNetwork !== false) {
844
+ (0, networkCapture_1.startNetworkCapture)(this.config.apiBase, (call) => {
845
+ this.track('api_call', {
846
+ method: call.method, host: call.host, path: call.path,
847
+ status: call.status, durationMs: call.durationMs, success: call.success,
848
+ }, 'api_call', false);
849
+ });
850
+ }
851
+ return;
852
+ }
853
+ // Revoked: stop capturing and discard what was buffered rather than sending it.
854
+ (0, networkCapture_1.stopNetworkCapture)();
855
+ await recorder.stopRecording();
856
+ await (0, eventQueue_1.clearQueue)();
857
+ }
732
858
  async reset() {
733
859
  this.log('Resetting SDK state');
734
860
  if (this.autoEventUnsub) {
735
861
  this.autoEventUnsub();
736
862
  this.autoEventUnsub = null;
737
863
  }
864
+ (0, autoTapCapture_1.uninstallAutoTapCapture)();
738
865
  (0, session_1.setRolloverHandler)(null);
739
866
  (0, lifecycle_1.stopLifecycleTracking)();
740
867
  (0, networkCapture_1.stopNetworkCapture)();
@@ -747,6 +874,7 @@ class CXPinsightSDK {
747
874
  await (0, eventQueue_1.stopQueue)();
748
875
  await (0, session_1.destroySession)();
749
876
  await (0, frequencyCap_1.clearAllCaps)();
877
+ await (0, consent_1.clearConsent)();
750
878
  await (0, triggerEngine_1.clearTriggerState)();
751
879
  await (0, SetupController_1.clearSetupState)();
752
880
  await storage.multiRemove([
@@ -826,6 +954,7 @@ class CXPinsightSDK {
826
954
  }
827
955
  // The global JS error handler is installed once for the process lifetime.
828
956
  CXPinsightSDK.errorHandlerInstalled = false;
957
+ CXPinsightSDK.MAX_PENDING = 50;
829
958
  /** Singleton SDK instance */
830
959
  exports.CXPinsight = new CXPinsightSDK();
831
960
  //# sourceMappingURL=CXPinsight.js.map