react-native-nitro-amplitude 0.1.0 → 0.5.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 (176) hide show
  1. package/README.md +547 -55
  2. package/cpp/bindings/HybridAmplitudeWorker.cpp +27 -7
  3. package/lib/commonjs/analytics/config.js +31 -10
  4. package/lib/commonjs/analytics/config.js.map +1 -1
  5. package/lib/commonjs/analytics/index.js +8 -2
  6. package/lib/commonjs/analytics/index.js.map +1 -1
  7. package/lib/commonjs/analytics/network-guarded-fetch-transport.js +16 -0
  8. package/lib/commonjs/analytics/network-guarded-fetch-transport.js.map +1 -0
  9. package/lib/commonjs/analytics/nitro-transport.js +2 -0
  10. package/lib/commonjs/analytics/nitro-transport.js.map +1 -1
  11. package/lib/commonjs/analytics/plugins/context.js +7 -1
  12. package/lib/commonjs/analytics/plugins/context.js.map +1 -1
  13. package/lib/commonjs/analytics/react-native-client.js +155 -9
  14. package/lib/commonjs/analytics/react-native-client.js.map +1 -1
  15. package/lib/commonjs/diagnostics.js +92 -0
  16. package/lib/commonjs/diagnostics.js.map +1 -0
  17. package/lib/commonjs/errors.js +48 -0
  18. package/lib/commonjs/errors.js.map +1 -0
  19. package/lib/commonjs/experiment/experimentClient.js +84 -2
  20. package/lib/commonjs/experiment/experimentClient.js.map +1 -1
  21. package/lib/commonjs/experiment/index.js +12 -0
  22. package/lib/commonjs/experiment/index.js.map +1 -1
  23. package/lib/commonjs/experiment/stubClient.js +25 -0
  24. package/lib/commonjs/experiment/stubClient.js.map +1 -1
  25. package/lib/commonjs/experiment/transport/http.js +8 -2
  26. package/lib/commonjs/experiment/transport/http.js.map +1 -1
  27. package/lib/commonjs/experiment/typed-variants.js +52 -0
  28. package/lib/commonjs/experiment/typed-variants.js.map +1 -0
  29. package/lib/commonjs/experiment/types/config.js +32 -4
  30. package/lib/commonjs/experiment/types/config.js.map +1 -1
  31. package/lib/commonjs/index.js +105 -3
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/commonjs/index.web.js +387 -13
  34. package/lib/commonjs/index.web.js.map +1 -1
  35. package/lib/commonjs/native/context.web.js +26 -0
  36. package/lib/commonjs/native/context.web.js.map +1 -0
  37. package/lib/commonjs/native/http.js +23 -8
  38. package/lib/commonjs/native/http.js.map +1 -1
  39. package/lib/commonjs/native/http.web.js +17 -0
  40. package/lib/commonjs/native/http.web.js.map +1 -0
  41. package/lib/commonjs/native/hybrid.web.js +23 -0
  42. package/lib/commonjs/native/hybrid.web.js.map +1 -0
  43. package/lib/commonjs/native/storage.js +27 -15
  44. package/lib/commonjs/native/storage.js.map +1 -1
  45. package/lib/commonjs/native/storage.web.js +135 -0
  46. package/lib/commonjs/native/storage.web.js.map +1 -0
  47. package/lib/commonjs/network.js +154 -0
  48. package/lib/commonjs/network.js.map +1 -0
  49. package/lib/commonjs/presets.js +118 -0
  50. package/lib/commonjs/presets.js.map +1 -0
  51. package/lib/commonjs/testing.js +166 -0
  52. package/lib/commonjs/testing.js.map +1 -0
  53. package/lib/module/analytics/config.js +32 -11
  54. package/lib/module/analytics/config.js.map +1 -1
  55. package/lib/module/analytics/index.js +4 -1
  56. package/lib/module/analytics/index.js.map +1 -1
  57. package/lib/module/analytics/network-guarded-fetch-transport.js +11 -0
  58. package/lib/module/analytics/network-guarded-fetch-transport.js.map +1 -0
  59. package/lib/module/analytics/nitro-transport.js +2 -0
  60. package/lib/module/analytics/nitro-transport.js.map +1 -1
  61. package/lib/module/analytics/plugins/context.js +7 -1
  62. package/lib/module/analytics/plugins/context.js.map +1 -1
  63. package/lib/module/analytics/react-native-client.js +154 -9
  64. package/lib/module/analytics/react-native-client.js.map +1 -1
  65. package/lib/module/diagnostics.js +85 -0
  66. package/lib/module/diagnostics.js.map +1 -0
  67. package/lib/module/errors.js +41 -0
  68. package/lib/module/errors.js.map +1 -0
  69. package/lib/module/experiment/experimentClient.js +84 -2
  70. package/lib/module/experiment/experimentClient.js.map +1 -1
  71. package/lib/module/experiment/index.js +1 -0
  72. package/lib/module/experiment/index.js.map +1 -1
  73. package/lib/module/experiment/stubClient.js +25 -0
  74. package/lib/module/experiment/stubClient.js.map +1 -1
  75. package/lib/module/experiment/transport/http.js +8 -2
  76. package/lib/module/experiment/transport/http.js.map +1 -1
  77. package/lib/module/experiment/typed-variants.js +43 -0
  78. package/lib/module/experiment/typed-variants.js.map +1 -0
  79. package/lib/module/experiment/types/config.js +31 -4
  80. package/lib/module/experiment/types/config.js.map +1 -1
  81. package/lib/module/index.js +15 -3
  82. package/lib/module/index.js.map +1 -1
  83. package/lib/module/index.web.js +60 -11
  84. package/lib/module/index.web.js.map +1 -1
  85. package/lib/module/native/context.web.js +18 -0
  86. package/lib/module/native/context.web.js.map +1 -0
  87. package/lib/module/native/http.js +23 -8
  88. package/lib/module/native/http.js.map +1 -1
  89. package/lib/module/native/http.web.js +12 -0
  90. package/lib/module/native/http.web.js.map +1 -0
  91. package/lib/module/native/hybrid.web.js +16 -0
  92. package/lib/module/native/hybrid.web.js.map +1 -0
  93. package/lib/module/native/storage.js +27 -15
  94. package/lib/module/native/storage.js.map +1 -1
  95. package/lib/module/native/storage.web.js +128 -0
  96. package/lib/module/native/storage.web.js.map +1 -0
  97. package/lib/module/network.js +138 -0
  98. package/lib/module/network.js.map +1 -0
  99. package/lib/module/presets.js +110 -0
  100. package/lib/module/presets.js.map +1 -0
  101. package/lib/module/testing.js +160 -0
  102. package/lib/module/testing.js.map +1 -0
  103. package/lib/typescript/analytics/config.d.ts +19 -6
  104. package/lib/typescript/analytics/config.d.ts.map +1 -1
  105. package/lib/typescript/analytics/index.d.ts +1 -1
  106. package/lib/typescript/analytics/index.d.ts.map +1 -1
  107. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts +6 -0
  108. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts.map +1 -0
  109. package/lib/typescript/analytics/nitro-transport.d.ts.map +1 -1
  110. package/lib/typescript/analytics/plugins/context.d.ts +2 -0
  111. package/lib/typescript/analytics/plugins/context.d.ts.map +1 -1
  112. package/lib/typescript/analytics/react-native-client.d.ts +46 -6
  113. package/lib/typescript/analytics/react-native-client.d.ts.map +1 -1
  114. package/lib/typescript/diagnostics.d.ts +21 -0
  115. package/lib/typescript/diagnostics.d.ts.map +1 -0
  116. package/lib/typescript/errors.d.ts +9 -0
  117. package/lib/typescript/errors.d.ts.map +1 -0
  118. package/lib/typescript/experiment/experimentClient.d.ts +9 -1
  119. package/lib/typescript/experiment/experimentClient.d.ts.map +1 -1
  120. package/lib/typescript/experiment/index.d.ts +1 -0
  121. package/lib/typescript/experiment/index.d.ts.map +1 -1
  122. package/lib/typescript/experiment/stubClient.d.ts +6 -1
  123. package/lib/typescript/experiment/stubClient.d.ts.map +1 -1
  124. package/lib/typescript/experiment/transport/http.d.ts.map +1 -1
  125. package/lib/typescript/experiment/typed-variants.d.ts +9 -0
  126. package/lib/typescript/experiment/typed-variants.d.ts.map +1 -0
  127. package/lib/typescript/experiment/types/client.d.ts +21 -0
  128. package/lib/typescript/experiment/types/client.d.ts.map +1 -1
  129. package/lib/typescript/experiment/types/config.d.ts.map +1 -1
  130. package/lib/typescript/index.d.ts +12 -3
  131. package/lib/typescript/index.d.ts.map +1 -1
  132. package/lib/typescript/index.web.d.ts +37 -8
  133. package/lib/typescript/index.web.d.ts.map +1 -1
  134. package/lib/typescript/native/context.web.d.ts +8 -0
  135. package/lib/typescript/native/context.web.d.ts.map +1 -0
  136. package/lib/typescript/native/http.d.ts.map +1 -1
  137. package/lib/typescript/native/http.web.d.ts +6 -0
  138. package/lib/typescript/native/http.web.d.ts.map +1 -0
  139. package/lib/typescript/native/hybrid.web.d.ts +8 -0
  140. package/lib/typescript/native/hybrid.web.d.ts.map +1 -0
  141. package/lib/typescript/native/storage.d.ts.map +1 -1
  142. package/lib/typescript/native/storage.web.d.ts +30 -0
  143. package/lib/typescript/native/storage.web.d.ts.map +1 -0
  144. package/lib/typescript/network.d.ts +50 -0
  145. package/lib/typescript/network.d.ts.map +1 -0
  146. package/lib/typescript/presets.d.ts +50 -0
  147. package/lib/typescript/presets.d.ts.map +1 -0
  148. package/lib/typescript/testing.d.ts +11 -0
  149. package/lib/typescript/testing.d.ts.map +1 -0
  150. package/package.json +4 -2
  151. package/src/analytics/config.ts +33 -8
  152. package/src/analytics/index.ts +3 -0
  153. package/src/analytics/network-guarded-fetch-transport.ts +10 -0
  154. package/src/analytics/nitro-transport.ts +2 -0
  155. package/src/analytics/plugins/context.ts +10 -1
  156. package/src/analytics/react-native-client.ts +238 -9
  157. package/src/diagnostics.ts +119 -0
  158. package/src/errors.ts +60 -0
  159. package/src/experiment/experimentClient.ts +116 -3
  160. package/src/experiment/index.ts +1 -0
  161. package/src/experiment/stubClient.ts +42 -1
  162. package/src/experiment/transport/http.ts +10 -2
  163. package/src/experiment/typed-variants.ts +68 -0
  164. package/src/experiment/types/client.ts +29 -0
  165. package/src/experiment/types/config.ts +29 -5
  166. package/src/index.ts +28 -2
  167. package/src/index.web.ts +89 -14
  168. package/src/native/context.web.ts +38 -0
  169. package/src/native/http.ts +38 -8
  170. package/src/native/http.web.ts +24 -0
  171. package/src/native/hybrid.web.ts +21 -0
  172. package/src/native/storage.ts +27 -25
  173. package/src/native/storage.web.ts +152 -0
  174. package/src/network.ts +258 -0
  175. package/src/presets.ts +208 -0
  176. package/src/testing.ts +177 -0
package/README.md CHANGED
@@ -1,33 +1,53 @@
1
1
  # react-native-nitro-amplitude
2
2
 
3
- [![npm](https://img.shields.io/npm/v/react-native-nitro-amplitude)](https://www.npmjs.com/package/react-native-nitro-amplitude)
4
- [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
3
+ [![version](https://img.shields.io/badge/version-0.5.0-blue)](https://www.npmjs.com/package/react-native-nitro-amplitude)
4
+ [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/license/mit)
5
5
  [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
6
+ [![Expo](https://img.shields.io/badge/expo-SDK%2056-000020)](https://expo.dev/)
6
7
  [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.7-black)](https://nitro.margelo.com/)
7
8
 
8
- High-performance Amplitude Analytics and Experiment SDK for React Native, powered by Nitro Modules and C++.
9
-
10
- This package combines the public APIs of `amplitude-rn-analytics` and `amplitude-rn-experiment` with a native Nitro layer for:
11
-
12
- - synchronous JSI key-value storage (analytics events + experiment variants)
13
- - synchronous device/application context collection
14
- - background HTTP transport for uploads and experiment fetches
9
+ Amplitude Analytics and Experiment for React Native in one Nitro package.
10
+
11
+ The public API is aligned with `amplitude-rn-analytics` and
12
+ `amplitude-rn-experiment`, while iOS and Android use Nitro Modules for native
13
+ context, storage, and background HTTP transport. Web builds use the package's
14
+ `index.web.ts` entrypoint with browser fetch and storage fallbacks.
15
+
16
+ ## Features
17
+
18
+ - Analytics client methods including `init`, `track`, `identify`, `flush`,
19
+ `reset`, `shutdown`, and `createInstance`.
20
+ - Experiment client methods including `Experiment.initialize`,
21
+ `Experiment.initializeWithAmplitudeAnalytics`, `start`, `fetch`, `variant`,
22
+ and `exposure`.
23
+ - Compatibility subpaths for incremental migration:
24
+ `react-native-nitro-amplitude/analytics` and
25
+ `react-native-nitro-amplitude/experiment`.
26
+ - Native `AmplitudeContext`, `AmplitudeStorage`, and `AmplitudeWorker`
27
+ HybridObjects on iOS and Android.
28
+ - Web-compatible Analytics and Experiment entrypoints without native Nitro
29
+ requirements.
30
+ - Expo config plugin and deterministic example app smoke tests.
15
31
 
16
32
  ## Install
17
33
 
18
34
  ```sh
19
- bun add react-native-nitro-amplitude react-native-nitro-modules@>=0.35.7
35
+ bun add react-native-nitro-amplitude "react-native-nitro-modules@>=0.35.7"
20
36
  ```
21
37
 
22
- Peer dependencies: `react`, `react-native`, and `react-native-nitro-modules >=0.35.7`.
38
+ Peer dependencies:
39
+
40
+ - `react >=18.2.0`
41
+ - `react-native >=0.75.0`
42
+ - `react-native-nitro-modules >=0.35.7`
23
43
 
24
- Expo SDK 56:
44
+ For Expo SDK 56:
25
45
 
26
46
  ```sh
27
47
  bunx expo install react-native-nitro-amplitude react-native-nitro-modules
28
48
  ```
29
49
 
30
- Add the config plugin and prebuild:
50
+ Add the config plugin for native iOS and Android builds:
31
51
 
32
52
  ```json
33
53
  {
@@ -37,43 +57,367 @@ Add the config plugin and prebuild:
37
57
  }
38
58
  ```
39
59
 
40
- ## Analytics quick start
60
+ Then prebuild or rebuild your native app:
41
61
 
42
- ```ts
43
- import { init, track, identify, flush } from "react-native-nitro-amplitude";
62
+ ```sh
63
+ bunx expo prebuild
64
+ ```
65
+
66
+ Web-only apps do not need the config plugin.
44
67
 
45
- await init("YOUR_API_KEY", "user-id", {
68
+ ## Analytics
69
+
70
+ ```ts
71
+ import {
72
+ Identify,
73
+ flush,
74
+ identify,
75
+ init,
76
+ track,
77
+ } from "react-native-nitro-amplitude";
78
+
79
+ await init("AMPLITUDE_API_KEY", "user-id", {
46
80
  instanceName: "$default_instance",
47
81
  trackingSessionEvents: true,
48
82
  }).promise;
49
83
 
50
84
  track("button_clicked", { screen: "home" });
85
+
86
+ const update = new Identify();
87
+ update.set("plan", "pro");
88
+ identify(update);
89
+
51
90
  await flush().promise;
52
91
  ```
53
92
 
54
- ## Experiment quick start
93
+ Use named instances when one app needs separate Analytics clients:
94
+
95
+ ```ts
96
+ import { createInstance } from "react-native-nitro-amplitude";
97
+
98
+ const analytics = createInstance();
99
+
100
+ await analytics.init("AMPLITUDE_API_KEY", "user-id", {
101
+ instanceName: "checkout",
102
+ }).promise;
103
+
104
+ analytics.track("checkout_started");
105
+ ```
106
+
107
+ ## Experiment
55
108
 
56
109
  ```ts
57
110
  import { Experiment, init } from "react-native-nitro-amplitude";
58
111
 
59
- await init("YOUR_API_KEY").promise;
112
+ const instanceName = "main";
113
+
114
+ await init("AMPLITUDE_ANALYTICS_API_KEY", "demo-user", {
115
+ instanceName,
116
+ }).promise;
117
+
118
+ const experiment = Experiment.initializeWithAmplitudeAnalytics(
119
+ "AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY",
120
+ {
121
+ instanceName,
122
+ automaticExposureTracking: true,
123
+ },
124
+ );
125
+
126
+ await experiment.fetch();
127
+
128
+ const variant = experiment.variant("demo-flag");
129
+
130
+ if (variant.value === "on") {
131
+ showEnabledExperience();
132
+ }
133
+ ```
134
+
135
+ Use `Experiment.initialize` when the Experiment client should not read identity
136
+ from an Analytics instance:
137
+
138
+ ```ts
139
+ import { Experiment } from "react-native-nitro-amplitude";
140
+
141
+ const experiment = Experiment.initialize("AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY");
142
+
143
+ await experiment.fetch({
144
+ user_id: "demo-user",
145
+ device_id: "device-id",
146
+ });
147
+ ```
148
+
149
+ ## Durable storage preset
150
+
151
+ The package works without app configuration, but production apps usually want
152
+ Analytics queue storage, Analytics session storage, and Experiment variant
153
+ storage to survive restarts together:
154
+
155
+ ```ts
156
+ import {
157
+ Experiment,
158
+ createDurableAmplitudeStoragePreset,
159
+ init,
160
+ } from "react-native-nitro-amplitude";
161
+
162
+ const storage = createDurableAmplitudeStoragePreset({ namespace: "main" });
163
+
164
+ await init("AMPLITUDE_API_KEY", "user-id", {
165
+ instanceName: "main",
166
+ ...storage.analytics,
167
+ }).promise;
168
+
169
+ const experiment = Experiment.initializeWithAmplitudeAnalytics(
170
+ "AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY",
171
+ {
172
+ instanceName: "main",
173
+ ...storage.experiment,
174
+ },
175
+ );
176
+ ```
177
+
178
+ | State | Default | Durable preset |
179
+ | ------------------------------ | ----------------------------------------- | --------------------------------------- |
180
+ | Analytics event queue | Nitro-backed event storage | Nitro-backed event storage |
181
+ | Analytics session/cookie state | React Native local storage fallback | Nitro-backed session storage |
182
+ | User ID and device ID | Stored in session/cookie state after init | Stored in Nitro-backed session storage |
183
+ | Experiment variants | Nitro memory storage by default | Nitro-backed persistent variant storage |
184
+ | Exposure dedupe state | In memory per Experiment client session | In memory per Experiment client session |
185
+
186
+ `createPersistentAmplitudeConfig()` is an alias for the same preset. The
187
+ namespace isolates multiple app environments and instances.
188
+
189
+ ## Combined client
190
+
191
+ Use `createAmplitudeClient` when Analytics and Experiment should share instance
192
+ name, identity, device ID, durable storage, and dry-run options:
193
+
194
+ ```ts
195
+ import { createAmplitudeClient } from "react-native-nitro-amplitude";
196
+
197
+ const amplitude = createAmplitudeClient({
198
+ analyticsApiKey: "AMPLITUDE_API_KEY",
199
+ experimentDeploymentKey: "AMPLITUDE_EXPERIMENT_DEPLOYMENT_KEY",
200
+ instanceName: "main",
201
+ userId: "user-id",
202
+ durableStorage: true,
203
+ });
204
+
205
+ await amplitude.init();
206
+ await amplitude.experiment.fetch();
207
+ const variant = amplitude.experiment.variant("checkout-copy");
208
+ ```
209
+
210
+ When `Experiment.initializeWithAmplitudeAnalytics` or `createAmplitudeClient`
211
+ uses the same `instanceName` as Analytics, the Experiment user provider reads
212
+ the Analytics user ID and device ID through Amplitude's connector. Multiple
213
+ Analytics instances are isolated by `instanceName`. TypeScript treats
214
+ `amplitude.experiment` as required when `experimentDeploymentKey` is present and
215
+ optional when the combined client is Analytics-only.
216
+
217
+ ## Diagnostics and health checks
218
+
219
+ ```ts
220
+ import {
221
+ getDiagnostics,
222
+ getLastNativeError,
223
+ healthCheck,
224
+ } from "react-native-nitro-amplitude";
225
+
226
+ const diagnostics = getDiagnostics();
227
+ const health = await healthCheck();
228
+ const lastNativeError = getLastNativeError();
229
+ ```
230
+
231
+ Diagnostics expose initialized state, queue size, last flush time, last flush
232
+ duration, last flush error, user ID, device ID, session ID, active instance
233
+ names, network-enabled state, native HybridObject availability, and storage
234
+ writability. On native, `healthCheck()` verifies that native bindings can be
235
+ reached and storage can round-trip a value. On web, diagnostics intentionally
236
+ report `nativeAvailable: false` while browser fetch and storage fallbacks remain
237
+ usable.
238
+
239
+ Structured errors use stable codes: `not_initialized`, `network_error`,
240
+ `storage_error`, `invalid_api_key`, `invalid_deployment_key`,
241
+ `experiment_fetch_failed`, `native_unavailable`, `serialization_error`,
242
+ `event_too_large`, `timeout`, and `unknown`.
243
+
244
+ ## Network controls and dry-run transport
245
+
246
+ ```ts
247
+ import {
248
+ clearDryRunTransportRecords,
249
+ dryRunHttpClient,
250
+ dryRunTransport,
251
+ getDryRunTransportRecords,
252
+ setNetworkEnabled,
253
+ } from "react-native-nitro-amplitude";
254
+
255
+ setNetworkEnabled(false);
256
+ clearDryRunTransportRecords();
257
+
258
+ await init("AMPLITUDE_API_KEY", "benchmark-user", {
259
+ transportProvider: dryRunTransport,
260
+ }).promise;
261
+
262
+ const experiment = Experiment.initialize("DEPLOYMENT_KEY", {
263
+ httpClient: dryRunHttpClient,
264
+ });
265
+
266
+ const captured = getDryRunTransportRecords();
267
+ ```
268
+
269
+ Use dry-run transport for QA screens, test runs, and benchmark-safe validation.
270
+ For side-by-side benchmarks, do not point both SDKs at production projects.
271
+ Either use dry-run transport or isolated Amplitude dev/benchmark projects.
272
+
273
+ ## Request timing and performance debugging
274
+
275
+ Use the timing wrappers to split slow paths into HTTP time and app/package code
276
+ time without changing native code:
60
277
 
61
- const experiment = Experiment.initializeWithAmplitudeAnalytics("YOUR_API_KEY");
62
- await experiment.start();
278
+ ```ts
279
+ import {
280
+ createNetworkTimingBuffer,
281
+ createTimedAnalyticsTransport,
282
+ createTimedHttpClient,
283
+ init,
284
+ nitroHttpClient,
285
+ nitroTransport,
286
+ Experiment,
287
+ } from "react-native-nitro-amplitude";
288
+
289
+ const timings = createNetworkTimingBuffer(20);
290
+
291
+ await init("AMPLITUDE_API_KEY", "user-id", {
292
+ transportProvider: createTimedAnalyticsTransport(
293
+ nitroTransport,
294
+ timings.record,
295
+ ),
296
+ }).promise;
297
+
298
+ const experiment = Experiment.initialize("DEPLOYMENT_KEY", {
299
+ httpClient: createTimedHttpClient(nitroHttpClient, timings.record),
300
+ });
63
301
 
64
- const variant = experiment.variant("my-flag");
302
+ console.log(timings.getTimings());
65
303
  ```
66
304
 
305
+ For Analytics, `track()` measures queue/code execution and `flushWithResult()`
306
+ is the request-backed path. For Experiment, `fetchWithMetadata()` is the
307
+ request-backed path and `variantWithMetadata()` measures cached variant
308
+ resolution plus exposure enqueue behavior. The example app shows both the last
309
+ request and a bounded request history so slow runs can be compared directly.
310
+
311
+ ## Flush and fetch metadata
312
+
313
+ `flushWithResult()` returns sent, failed, dropped, retried, and reason fields
314
+ around the underlying Analytics flush result.
315
+
316
+ `experiment.fetchWithMetadata()` returns fetched flag keys, cache hit status,
317
+ duration, source, and failure reason. `variantWithMetadata()` returns the
318
+ variant, source, fallback status, stale status, and fallback/failure reason.
319
+ Repeated simultaneous Experiment fetches are deduplicated by sharing the
320
+ in-flight request.
321
+
322
+ ```ts
323
+ const flush = await flushWithResult();
324
+ const fetch = await experiment.fetchWithMetadata(undefined, {
325
+ flagKeys: ["checkout-copy"],
326
+ });
327
+ const variant = experiment.variantWithMetadata("checkout-copy", "control");
328
+ ```
329
+
330
+ `clearVariants()` clears cached variants without resetting Analytics identity.
331
+ `hasCachedVariant(flagKey)` checks whether a flag currently has a cached
332
+ variant.
333
+
334
+ ## Experiment behavior
335
+
336
+ `fetch()` contacts Amplitude Experiment and stores returned variants. `variant()`
337
+ reads from the configured source and, when `automaticExposureTracking` is true,
338
+ sends an exposure through the configured exposure provider. `exposure()` sends
339
+ an exposure explicitly for a previously resolved key. `fetch()` does not send
340
+ exposures by itself.
341
+
342
+ If no exposure provider is configured, exposure calls are no-ops. The provider
343
+ is configured automatically when using
344
+ `Experiment.initializeWithAmplitudeAnalytics`.
345
+
346
+ Typed helpers are available for common flag shapes:
347
+
348
+ ```ts
349
+ import {
350
+ variantBoolean,
351
+ variantJson,
352
+ variantNumber,
353
+ variantPayload,
354
+ variantString,
355
+ } from "react-native-nitro-amplitude";
356
+
357
+ const enabled = variantBoolean(experiment, "new-flow", false);
358
+ const label = variantString(experiment, "button-label", "Continue");
359
+ const limit = variantNumber(experiment, "max-items", 10);
360
+ const config = variantJson(experiment, "layout-json", { columns: 1 });
361
+ const payload = variantPayload(experiment, "payload-flag", { color: "blue" });
362
+ ```
363
+
364
+ ## Privacy, lifecycle, and identity recipes
365
+
366
+ Use `setOptOut(true)` to stop tracking, `reset()` to clear Analytics user ID and
367
+ rotate the device ID, and the durable storage preset `clear()` helper to clear
368
+ package-managed persisted state for a namespace.
369
+
370
+ Recommended sign-out sequence:
371
+
372
+ 1. `await flushWithResult()`
373
+ 2. `experiment.clearVariants()`
374
+ 3. `reset()`
375
+ 4. `setUserId(undefined)`
376
+ 5. Clear app-owned user properties and app-owned experiment state
377
+
378
+ Recommended user switch sequence:
379
+
380
+ 1. `await flushWithResult()` for the previous user
381
+ 2. `experiment.clearVariants()`
382
+ 3. `setUserId(nextUserId)`
383
+ 4. `await experiment.fetch({ user_id: nextUserId })`
384
+ 5. Render the first screen that depends on Experiment assignments
385
+
386
+ Consent-gated apps should wait to call `init()` until consent is granted. To
387
+ avoid pre-consent network traffic in test or privacy states, call
388
+ `setNetworkEnabled(false)` before initialization and use dry-run transport.
389
+
390
+ Client keys and deployment keys are not server secrets, but they should be
391
+ environment-specific and excluded from logs. Diagnostics intentionally expose
392
+ state shape and status rather than event property values.
393
+
394
+ ## Native rebuild and Expo plugin
395
+
396
+ Installing the package changes native code. Metro reload is not enough. Rebuild
397
+ or prebuild the native app after installation, Nitro version changes, config
398
+ plugin changes, or React Native upgrades.
399
+
400
+ The Expo config plugin injects Android application context setup required by the
401
+ native Analytics adapter and lets Expo autolinking include the Nitro module.
402
+ After `bunx expo prebuild`, verify:
403
+
404
+ - Android `MainApplication.onCreate` calls `AndroidAmplitudeAdapter.setContext`.
405
+ - iOS Pods include `react-native-nitro-amplitude`.
406
+ - The app launches without a `NitroModules.createHybridObject` failure.
407
+ - `healthCheck()` reports `nativeAvailable: true`.
408
+
409
+ For Expo Doctor, this package may not appear in React Native Directory checks.
410
+ Exclude it from directory validation if your app enforces that check.
411
+
67
412
  ## Compatibility imports
68
413
 
69
- The root entry exports both Analytics and Experiment APIs for new code:
414
+ The root entry exports Analytics and Experiment APIs for new code:
70
415
 
71
416
  ```ts
72
- import { init, track, Experiment } from "react-native-nitro-amplitude";
417
+ import { Experiment, init, track } from "react-native-nitro-amplitude";
73
418
  ```
74
419
 
75
- For migrations that need a clearer boundary between the two original packages,
76
- the package also exposes compatibility subpaths:
420
+ For migrations from the original packages, use the compatibility subpaths:
77
421
 
78
422
  ```ts
79
423
  import { init, track } from "react-native-nitro-amplitude/analytics";
@@ -84,56 +428,204 @@ import { Experiment } from "react-native-nitro-amplitude/experiment";
84
428
  `amplitude-rn-analytics`. `react-native-nitro-amplitude/experiment` mirrors the
85
429
  public entrypoint shape of `amplitude-rn-experiment`.
86
430
 
87
- ## Native HybridObjects
431
+ Import app-facing APIs from the root package unless you are preserving old
432
+ import paths during a migration. The `/analytics` and `/experiment` subpaths are
433
+ stable compatibility paths. Do not import from `src`, `lib`, `native`, or other
434
+ private package internals.
88
435
 
89
- | Object | Purpose |
90
- | ------------------ | ----------------------------------------------- |
91
- | `AmplitudeContext` | Device context + legacy SDK migration hooks |
92
- | `AmplitudeStorage` | Memory + disk KV for analytics/experiment state |
93
- | `AmplitudeWorker` | Background HTTP queue |
436
+ ## TypeScript
437
+
438
+ The package ships generated declaration files and re-exports the public
439
+ Analytics and Experiment types used by the implementation:
440
+
441
+ ```ts
442
+ import type {
443
+ AmplitudeNetworkTimingBuffer,
444
+ ExperimentConfig,
445
+ ExperimentFetchResult,
446
+ ExperimentVariantResult,
447
+ Logger,
448
+ Variant,
449
+ } from "react-native-nitro-amplitude";
450
+ import { createNetworkTimingBuffer } from "react-native-nitro-amplitude";
451
+ import type { ReactNativeOptions } from "react-native-nitro-amplitude/analytics";
452
+
453
+ const analyticsOptions = {
454
+ instanceName: "main",
455
+ trackingSessionEvents: true,
456
+ } satisfies ReactNativeOptions;
94
457
 
95
- ## Upstream compatibility
458
+ const experimentConfig = {
459
+ instanceName: "main",
460
+ automaticExposureTracking: true,
461
+ serverZone: "US",
462
+ } satisfies ExperimentConfig;
96
463
 
97
- This package is a Nitro-first replacement, not a forked merge of the two source
98
- packages. The JavaScript API surface is intentionally aligned with:
464
+ const lastTimings: AmplitudeNetworkTimingBuffer = createNetworkTimingBuffer(20);
99
465
 
100
- - `amplitude-rn-analytics` 1.6.0 for analytics client methods, storage provider
101
- contracts, exported `Revenue`, `Identify`, and `Types`.
102
- - `amplitude-rn-experiment` 1.8.13 for `Experiment`, `ExperimentClient`,
103
- config/user/variant/exposure/storage types, loggers, and memory storage.
466
+ function handleFetchResult(result: ExperimentFetchResult): void {
467
+ const failureReason = result.fetched ? undefined : result.failureReason;
468
+ void failureReason;
469
+ }
470
+
471
+ function handleVariantResult(result: ExperimentVariantResult): Variant {
472
+ return result.variant;
473
+ }
474
+ ```
104
475
 
105
- The default implementations differ where Nitro provides the native layer:
476
+ Prefer `satisfies` for configuration objects so TypeScript keeps literal values
477
+ while still checking option names and value types. The root package also
478
+ exports structured result and diagnostics types such as
479
+ `AmplitudeDiagnostics`, `AmplitudeErrorCode`, `AmplitudeNetworkTiming`,
480
+ `AmplitudeNetworkTimingBuffer`, `ExperimentFetchResult`, and
481
+ `ExperimentVariantResult` so app code can avoid stringly typed wrappers.
106
482
 
107
- - analytics storage uses Nitro-backed memory/disk storage by default;
108
- - experiment storage uses Nitro-backed storage by default;
109
- - analytics upload and experiment fetches use the native `AmplitudeWorker`;
110
- - native context is read through the `AmplitudeContext` HybridObject.
483
+ Test helpers are typed and can be imported from the root package:
484
+
485
+ ```ts
486
+ import {
487
+ createFakeExperimentStorage,
488
+ createMockAmplitudeClient,
489
+ createMockExperimentClient,
490
+ } from "react-native-nitro-amplitude";
491
+ ```
492
+
493
+ The package's CI validates strict public types, documented root exports,
494
+ compatibility subpaths, and representative README examples through type tests
495
+ and import smoke tests.
496
+
497
+ ## Web
498
+
499
+ The root entrypoint and both compatibility subpaths are available on web:
500
+
501
+ ```ts
502
+ import { Experiment, init, track } from "react-native-nitro-amplitude";
503
+ ```
504
+
505
+ Web builds use browser `fetch`, `localStorage`, `sessionStorage`, and in-memory
506
+ fallbacks instead of Nitro HybridObjects. Native-only types such as
507
+ `AmplitudeWorker` remain available for TypeScript compatibility, but no Nitro
508
+ native module is loaded on web.
509
+
510
+ ## Native HybridObjects
511
+
512
+ | Object | Purpose |
513
+ | ------------------ | ----------------------------------------------- |
514
+ | `AmplitudeContext` | Device context and legacy SDK migration hooks |
515
+ | `AmplitudeStorage` | Memory and disk KV for analytics/experiment |
516
+ | `AmplitudeWorker` | Background HTTP queue for upload/fetch requests |
517
+
518
+ If Nitro bindings are unavailable, rebuild the native app. Typical causes are a
519
+ missing prebuild, Pods not installed, Gradle cache using an old native project,
520
+ Hermes/New Architecture mismatch, or a Nitro Modules peer version mismatch.
111
521
 
112
522
  ## Platform support
113
523
 
114
- | Platform | Analytics | Experiment | Native Nitro |
115
- | -------- | ----------------------------------------- | ----------- | ------------ |
116
- | iOS | Yes | Yes | Yes |
117
- | Android | Yes | Yes | Yes |
118
- | Web | Unsupported entry (`index.web.ts` throws) | Unsupported | No |
524
+ | Platform | Analytics | Experiment | Native Nitro |
525
+ | -------- | --------- | ---------- | ------------ |
526
+ | iOS | Yes | Yes | Yes |
527
+ | Android | Yes | Yes | Yes |
528
+ | Web | Yes | Yes | No |
529
+
530
+ The package targets Hermes and the React Native New Architecture through Nitro
531
+ Modules. Tested release lanes use React Native 0.85, Expo SDK 56, Nitro Modules
532
+ 0.35.7, Xcode 26, iOS 18+, Android Gradle Plugin from the Expo SDK 56 template,
533
+ and Android minSdk from the example template. Older RN versions are allowed by
534
+ peer range but should be validated in the consuming app.
535
+
536
+ Android ships consumer R8 rules with the package. iOS and Android release
537
+ builds should be part of app-level release validation when Analytics or
538
+ Experiment behavior is release-critical.
539
+
540
+ ## Troubleshooting
541
+
542
+ - `Nitro module not found`: rebuild native projects, reinstall Pods, clean
543
+ Gradle/CMake outputs, and confirm `react-native-nitro-modules` peer version.
544
+ - No events in dashboard: check API key, server zone, opt-out state, network
545
+ state, flush result, and whether dry-run transport is configured.
546
+ - Experiment fetch returns no variant: check deployment key, flag key, user ID,
547
+ device ID, server zone, and `fetchWithMetadata()` failure reason.
548
+ - Missing deployment key: validate configuration before app startup and avoid
549
+ falling back to production keys in development.
550
+ - Hermes issues: confirm the app is rebuilt after installing Nitro Modules and
551
+ that the native module is present in the generated app.
552
+ - Pods or Gradle failures: run clean prebuild, reinstall Pods, and inspect the
553
+ package-owned `app.plugin.js`, podspec, Gradle, CMake, and generated Nitro
554
+ files first.
555
+
556
+ ## Production checklist
557
+
558
+ - Use separate Analytics API keys and Experiment deployment keys for dev,
559
+ staging, benchmark, and production.
560
+ - Choose US or EU server zone consistently across Analytics and Experiment.
561
+ - Configure durable storage if sessions, queued events, or variants must
562
+ survive restarts.
563
+ - Define consent, opt-out, sign-out, user-switch, and local data clearing
564
+ behavior before release.
565
+ - Verify Android debug, Android release, iOS debug, and iOS release builds.
566
+ - Confirm events, identify calls, Experiment fetches, variants, exposures, and
567
+ flushes in Amplitude dashboards without logging keys or sensitive properties.
568
+ - Capture timing output for `track`, `flushWithResult`, `fetchWithMetadata`,
569
+ `variantWithMetadata`, and exposure in the example app.
570
+ - Confirm slow paths are either HTTP latency or local code time before changing
571
+ package internals.
572
+ - Capture `getDiagnostics()`, `healthCheck()`, package version, React Native
573
+ version, platform, and redacted app config when reporting support issues.
574
+
575
+ ## Known limitations
576
+
577
+ - Legacy Amplitude SDK SQLite migration hooks are stubbed.
578
+ - Exposure dedupe state is in memory for the current Experiment client session.
579
+ - Background uploads are not guaranteed after process suspension; flush on
580
+ foreground/background transitions from app code when required.
581
+ - Event plan validation, reserved-property enforcement, event-size enforcement,
582
+ custom proxy/base URL examples, and local fake Experiment server support are
583
+ extension points rather than mandatory runtime behavior in this release.
584
+ - Tree shaking depends on the consuming bundler. Analytics-only apps should use
585
+ root or `/analytics` imports consistently and validate bundle output in their
586
+ own Metro setup.
587
+
588
+ ## Benchmarks
589
+
590
+ Use dry-run transport or isolated benchmark projects so local measurements do
591
+ not pollute production Amplitude data. The repo benchmark gate is:
592
+
593
+ ```sh
594
+ bun run benchmark
595
+ ```
596
+
597
+ Native latency numbers depend on the device, build type, network, server zone,
598
+ and whether dry-run transport is used. Record cold start, repeated `track()`,
599
+ HTTP duration and total duration for `flushWithResult()`, HTTP duration and SDK
600
+ duration for `fetchWithMetadata()`, variant read latency, and exposure enqueue
601
+ latency separately for Android and iOS. Treat `createTimedAnalyticsTransport`
602
+ and `createTimedHttpClient` output as the first debug signal before profiling
603
+ native code.
119
604
 
120
605
  ## Development
121
606
 
122
- From the monorepo root:
607
+ From the repository root:
123
608
 
124
609
  ```sh
125
610
  bun install
126
- bun run codegen
127
- bun run build
128
611
  bun run check
129
- bun run example:generate-icons # after editing apps/example/scripts/generate-icons.sh
612
+ bun run example:check
613
+ bun run audit:package
614
+ bun run publish-package:dry-run
615
+ ```
616
+
617
+ Native example verification:
618
+
619
+ ```sh
130
620
  bun run example:prebuild:clean
131
621
  bun run example:android
132
622
  bun run example:ios
623
+ bun run example:smoke
133
624
  ```
134
625
 
135
- See [AGENTS.md](./AGENTS.md) for package invariants and release bar.
626
+ Package invariants and release requirements live in `AGENTS.md` in this
627
+ repository.
136
628
 
137
629
  ## License
138
630
 
139
- MIT
631
+ [MIT](https://opensource.org/license/mit)