@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.
- package/CHANGELOG.md +159 -0
- package/README.md +25 -0
- package/lib/CXPinsight.d.ts +24 -1
- package/lib/CXPinsight.d.ts.map +1 -1
- package/lib/CXPinsight.js +137 -8
- package/lib/CXPinsight.js.map +1 -1
- package/lib/CXPinsightProvider.d.ts.map +1 -1
- package/lib/CXPinsightProvider.js +5 -1
- package/lib/CXPinsightProvider.js.map +1 -1
- package/lib/core/autoTapCapture.d.ts +15 -0
- package/lib/core/autoTapCapture.d.ts.map +1 -0
- package/lib/core/autoTapCapture.js +242 -0
- package/lib/core/autoTapCapture.js.map +1 -0
- package/lib/core/consent.d.ts +21 -0
- package/lib/core/consent.d.ts.map +1 -0
- package/lib/core/consent.js +131 -0
- package/lib/core/consent.js.map +1 -0
- package/lib/core/eventQueue.d.ts.map +1 -1
- package/lib/core/eventQueue.js +7 -0
- package/lib/core/eventQueue.js.map +1 -1
- package/lib/core/mobileRecorder.d.ts +6 -0
- package/lib/core/mobileRecorder.d.ts.map +1 -1
- package/lib/core/mobileRecorder.js +54 -2
- package/lib/core/mobileRecorder.js.map +1 -1
- package/lib/core/networkCapture.d.ts +10 -1
- package/lib/core/networkCapture.d.ts.map +1 -1
- package/lib/core/networkCapture.js +53 -5
- package/lib/core/networkCapture.js.map +1 -1
- package/lib/core/triggerEngine.js +3 -3
- package/lib/core/triggerEngine.js.map +1 -1
- package/lib/instrumented/Pressable.js +4 -0
- package/lib/instrumented/Pressable.js.map +1 -1
- package/lib/instrumented/ScrollView.js +4 -0
- package/lib/instrumented/ScrollView.js.map +1 -1
- package/lib/instrumented/TextInput.js +4 -0
- package/lib/instrumented/TextInput.js.map +1 -1
- package/lib/instrumented/TouchableOpacity.js +4 -0
- package/lib/instrumented/TouchableOpacity.js.map +1 -1
- package/lib/setup/index.d.ts +0 -1
- package/lib/setup/index.d.ts.map +1 -1
- package/lib/setup/index.js +4 -6
- package/lib/setup/index.js.map +1 -1
- package/lib/survey/SurveyModal.d.ts.map +1 -1
- package/lib/survey/SurveyModal.js +7 -1
- package/lib/survey/SurveyModal.js.map +1 -1
- package/lib/survey/expressionEval.d.ts +13 -9
- package/lib/survey/expressionEval.d.ts.map +1 -1
- package/lib/survey/expressionEval.js +16 -284
- package/lib/survey/expressionEval.js.map +1 -1
- package/lib/survey/interpolate.d.ts.map +1 -1
- package/lib/survey/interpolate.js +9 -27
- package/lib/survey/interpolate.js.map +1 -1
- package/lib/survey/questions/TextQuestion.d.ts +9 -1
- package/lib/survey/questions/TextQuestion.d.ts.map +1 -1
- package/lib/survey/questions/TextQuestion.js +15 -3
- package/lib/survey/questions/TextQuestion.js.map +1 -1
- package/lib/survey/validate.js +10 -0
- package/lib/survey/validate.js.map +1 -1
- package/lib/types.d.ts +3 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +3 -0
- package/lib/types.js.map +1 -1
- package/package.json +6 -2
- package/lib/setup/TouchPatcher.d.ts +0 -27
- package/lib/setup/TouchPatcher.d.ts.map +0 -1
- package/lib/setup/TouchPatcher.js +0 -182
- 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
|
|
package/lib/CXPinsight.d.ts
CHANGED
|
@@ -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
|
|
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>;
|
package/lib/CXPinsight.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAGZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"CXPinsight.d.ts","sourceRoot":"","sources":["../src/CXPinsight.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EAGZ,iBAAiB,EACjB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,SAAS,CAAC;AAiBjB,OAAO,EAAsH,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAgBvK,KAAK,eAAe,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;AAClD,KAAK,cAAc,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;AAE9C,cAAM,aAAa;IACjB,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,SAAS,CAA2B;IAC5C,OAAO,CAAC,MAAM,CAAc;IAK5B,OAAO,CAAC,YAAY,CAA6B;IACjD,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,cAAc,CAA+B;IAErD,OAAO,CAAC,cAAc,CAA6B;IAEnD,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAuB;IAG7C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,gBAAgB,CAAS;IAEjC,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,qBAAqB;IAC7B,OAAO,CAAC,eAAe;IAEvB,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAS;IAY7C,OAAO,CAAC,OAAO,CAAwG;IACvH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAM;IACzC,OAAO,CAAC,cAAc,CAAK;IAE3B,oDAAoD;IAC9C,IAAI,CAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8HnF;IAED;;;;;;OAMG;YACW,oBAAoB;IAiDlC,4EAA4E;YAC9D,iBAAiB;IAqB/B,qEAAqE;IACrE,OAAO,CAAC,YAAY;IAgBpB,sEAAsE;IACtE,OAAO,CAAC,eAAe;IA8JvB,2BAA2B;IAC3B;;;;OAIG;YACW,kBAAkB;IA+BhC,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,SAAS,CAAC,EAAE,MAAM,EAClB,cAAc,UAAO;IACrB,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI,CAyEN;IAED,4DAA4D;IAC5D,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAmBpE;IAED,gCAAgC;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBhD;IAED,0BAA0B;IAC1B,eAAe,CACb,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAqCN;IAED,qCAAqC;IACrC,eAAe,CACb,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,IAAI,CAmCN;IAED,+BAA+B;IAC/B,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAkDpE;IAID,uDAAuD;IACvD,cAAc,IAAI,IAAI,CAUrB;IAED,oCAAoC;IAC9B,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAKnC;IAED,+CAA+C;IAC/C,aAAa,IAAI,OAAO,CAEvB;IAED,2CAA2C;IAC3C,aAAa,+DAEZ;IAED,8DAA8D;IACxD,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CAEnC;IAED,qCAAqC;IACrC,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAMjC;IAED;;;;OAIG;IACG,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC,CAapC;IAED,+DAA+D;IACzD,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAEnC;IAED,2CAA2C;IAC3C;;;;;;;OAOG;IACH,KAAK,IAAI,IAAI,CAAoB;IACjC,MAAM,IAAI,IAAI,CAAqB;IACnC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAgD;IAC/E,UAAU,IAAI,YAAY,CAAyB;IACnD,iBAAiB,IAAI,OAAO,CAA8B;IAE1D,qDAAqD;YACvC,eAAe;IA+BvB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CA4B3B;IAED,+BAA+B;IACzB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3B;IAED,sEAAsE;IACtE,wBAAwB,CAAC,SAAS,EAAE,eAAe,GAAG,IAAI,CAEzD;IAED,gDAAgD;IAChD,0BAA0B,IAAI,IAAI,CAEjC;IAED,qEAAqE;IACrE,uBAAuB,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAEvD;IAED,+CAA+C;IAC/C,yBAAyB,IAAI,IAAI,CAEhC;IAED,8BAA8B;IAC9B,aAAa,IAAI,mBAAmB,CAEnC;IAED,2BAA2B;IAC3B,UAAU,IAAI,MAAM,CAEnB;IAED;;;;wEAIoE;IACpE,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEjD;IAED,2BAA2B;IAC3B,UAAU,IAAI,gBAAgB,GAAG,IAAI,CAEpC;IAED;;mFAE+E;IAC/E,uBAAuB,IAAI;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;KAAE,GAAG,IAAI,CAOlG;IAED,qCAAqC;IACrC,cAAc,IAAI,OAAO,CAExB;IAED,OAAO,CAAC,GAAG;CAKZ;AAED,6BAA6B;AAC7B,eAAO,MAAM,UAAU,eAAsB,CAAC"}
|
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
|
-
|
|
293
|
-
|
|
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
|
-
|
|
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
|