react-native-nitro-amplitude 0.2.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 (155) hide show
  1. package/README.md +387 -8
  2. package/cpp/bindings/HybridAmplitudeWorker.cpp +5 -1
  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 +142 -1
  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/typed-variants.js +52 -0
  26. package/lib/commonjs/experiment/typed-variants.js.map +1 -0
  27. package/lib/commonjs/experiment/types/config.js +32 -4
  28. package/lib/commonjs/experiment/types/config.js.map +1 -1
  29. package/lib/commonjs/index.js +105 -3
  30. package/lib/commonjs/index.js.map +1 -1
  31. package/lib/commonjs/index.web.js +110 -12
  32. package/lib/commonjs/index.web.js.map +1 -1
  33. package/lib/commonjs/native/http.js +23 -8
  34. package/lib/commonjs/native/http.js.map +1 -1
  35. package/lib/commonjs/native/http.web.js +2 -0
  36. package/lib/commonjs/native/http.web.js.map +1 -1
  37. package/lib/commonjs/native/hybrid.web.js +23 -0
  38. package/lib/commonjs/native/hybrid.web.js.map +1 -0
  39. package/lib/commonjs/native/storage.js +27 -15
  40. package/lib/commonjs/native/storage.js.map +1 -1
  41. package/lib/commonjs/network.js +154 -0
  42. package/lib/commonjs/network.js.map +1 -0
  43. package/lib/commonjs/presets.js +118 -0
  44. package/lib/commonjs/presets.js.map +1 -0
  45. package/lib/commonjs/testing.js +166 -0
  46. package/lib/commonjs/testing.js.map +1 -0
  47. package/lib/module/analytics/config.js +32 -11
  48. package/lib/module/analytics/config.js.map +1 -1
  49. package/lib/module/analytics/index.js +4 -1
  50. package/lib/module/analytics/index.js.map +1 -1
  51. package/lib/module/analytics/network-guarded-fetch-transport.js +11 -0
  52. package/lib/module/analytics/network-guarded-fetch-transport.js.map +1 -0
  53. package/lib/module/analytics/nitro-transport.js +2 -0
  54. package/lib/module/analytics/nitro-transport.js.map +1 -1
  55. package/lib/module/analytics/plugins/context.js +7 -1
  56. package/lib/module/analytics/plugins/context.js.map +1 -1
  57. package/lib/module/analytics/react-native-client.js +141 -1
  58. package/lib/module/analytics/react-native-client.js.map +1 -1
  59. package/lib/module/diagnostics.js +85 -0
  60. package/lib/module/diagnostics.js.map +1 -0
  61. package/lib/module/errors.js +41 -0
  62. package/lib/module/errors.js.map +1 -0
  63. package/lib/module/experiment/experimentClient.js +84 -2
  64. package/lib/module/experiment/experimentClient.js.map +1 -1
  65. package/lib/module/experiment/index.js +1 -0
  66. package/lib/module/experiment/index.js.map +1 -1
  67. package/lib/module/experiment/stubClient.js +25 -0
  68. package/lib/module/experiment/stubClient.js.map +1 -1
  69. package/lib/module/experiment/typed-variants.js +43 -0
  70. package/lib/module/experiment/typed-variants.js.map +1 -0
  71. package/lib/module/experiment/types/config.js +31 -4
  72. package/lib/module/experiment/types/config.js.map +1 -1
  73. package/lib/module/index.js +15 -3
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/index.web.js +19 -6
  76. package/lib/module/index.web.js.map +1 -1
  77. package/lib/module/native/http.js +23 -8
  78. package/lib/module/native/http.js.map +1 -1
  79. package/lib/module/native/http.web.js +2 -0
  80. package/lib/module/native/http.web.js.map +1 -1
  81. package/lib/module/native/hybrid.web.js +16 -0
  82. package/lib/module/native/hybrid.web.js.map +1 -0
  83. package/lib/module/native/storage.js +27 -15
  84. package/lib/module/native/storage.js.map +1 -1
  85. package/lib/module/network.js +138 -0
  86. package/lib/module/network.js.map +1 -0
  87. package/lib/module/presets.js +110 -0
  88. package/lib/module/presets.js.map +1 -0
  89. package/lib/module/testing.js +160 -0
  90. package/lib/module/testing.js.map +1 -0
  91. package/lib/typescript/analytics/config.d.ts +19 -6
  92. package/lib/typescript/analytics/config.d.ts.map +1 -1
  93. package/lib/typescript/analytics/index.d.ts +1 -1
  94. package/lib/typescript/analytics/index.d.ts.map +1 -1
  95. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts +6 -0
  96. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts.map +1 -0
  97. package/lib/typescript/analytics/nitro-transport.d.ts.map +1 -1
  98. package/lib/typescript/analytics/plugins/context.d.ts +2 -0
  99. package/lib/typescript/analytics/plugins/context.d.ts.map +1 -1
  100. package/lib/typescript/analytics/react-native-client.d.ts +43 -0
  101. package/lib/typescript/analytics/react-native-client.d.ts.map +1 -1
  102. package/lib/typescript/diagnostics.d.ts +21 -0
  103. package/lib/typescript/diagnostics.d.ts.map +1 -0
  104. package/lib/typescript/errors.d.ts +9 -0
  105. package/lib/typescript/errors.d.ts.map +1 -0
  106. package/lib/typescript/experiment/experimentClient.d.ts +9 -1
  107. package/lib/typescript/experiment/experimentClient.d.ts.map +1 -1
  108. package/lib/typescript/experiment/index.d.ts +1 -0
  109. package/lib/typescript/experiment/index.d.ts.map +1 -1
  110. package/lib/typescript/experiment/stubClient.d.ts +6 -1
  111. package/lib/typescript/experiment/stubClient.d.ts.map +1 -1
  112. package/lib/typescript/experiment/typed-variants.d.ts +9 -0
  113. package/lib/typescript/experiment/typed-variants.d.ts.map +1 -0
  114. package/lib/typescript/experiment/types/client.d.ts +21 -0
  115. package/lib/typescript/experiment/types/client.d.ts.map +1 -1
  116. package/lib/typescript/experiment/types/config.d.ts.map +1 -1
  117. package/lib/typescript/index.d.ts +12 -3
  118. package/lib/typescript/index.d.ts.map +1 -1
  119. package/lib/typescript/index.web.d.ts +16 -6
  120. package/lib/typescript/index.web.d.ts.map +1 -1
  121. package/lib/typescript/native/http.d.ts.map +1 -1
  122. package/lib/typescript/native/http.web.d.ts.map +1 -1
  123. package/lib/typescript/native/hybrid.web.d.ts +8 -0
  124. package/lib/typescript/native/hybrid.web.d.ts.map +1 -0
  125. package/lib/typescript/native/storage.d.ts.map +1 -1
  126. package/lib/typescript/network.d.ts +50 -0
  127. package/lib/typescript/network.d.ts.map +1 -0
  128. package/lib/typescript/presets.d.ts +50 -0
  129. package/lib/typescript/presets.d.ts.map +1 -0
  130. package/lib/typescript/testing.d.ts +11 -0
  131. package/lib/typescript/testing.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/src/analytics/config.ts +33 -8
  134. package/src/analytics/index.ts +3 -0
  135. package/src/analytics/network-guarded-fetch-transport.ts +10 -0
  136. package/src/analytics/nitro-transport.ts +2 -0
  137. package/src/analytics/plugins/context.ts +10 -1
  138. package/src/analytics/react-native-client.ts +217 -0
  139. package/src/diagnostics.ts +119 -0
  140. package/src/errors.ts +60 -0
  141. package/src/experiment/experimentClient.ts +116 -3
  142. package/src/experiment/index.ts +1 -0
  143. package/src/experiment/stubClient.ts +42 -1
  144. package/src/experiment/typed-variants.ts +68 -0
  145. package/src/experiment/types/client.ts +29 -0
  146. package/src/experiment/types/config.ts +29 -5
  147. package/src/index.ts +28 -2
  148. package/src/index.web.ts +33 -5
  149. package/src/native/http.ts +38 -8
  150. package/src/native/http.web.ts +2 -0
  151. package/src/native/hybrid.web.ts +21 -0
  152. package/src/native/storage.ts +27 -25
  153. package/src/network.ts +258 -0
  154. package/src/presets.ts +208 -0
  155. package/src/testing.ts +177 -0
@@ -0,0 +1,11 @@
1
+ import type { AmplitudeReactNativeClient } from "./analytics/react-native-client";
2
+ import type { Client } from "./experiment/types/client";
3
+ import type { Storage } from "./experiment/types/storage";
4
+ import type { Variants } from "./experiment/types/variant";
5
+ export type FakeExperimentStorage = Storage & {
6
+ values: Map<string, string>;
7
+ };
8
+ export declare function createFakeExperimentStorage(initialValues?: Record<string, string>): FakeExperimentStorage;
9
+ export declare function createMockAmplitudeClient(): AmplitudeReactNativeClient;
10
+ export declare function createMockExperimentClient(initialVariants?: Variants): Client;
11
+ //# sourceMappingURL=testing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../../src/testing.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC;AAClF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAE1D,OAAO,KAAK,EAAW,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AA8BpE,MAAM,MAAM,qBAAqB,GAAG,OAAO,GAAG;IAC5C,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC7B,CAAC;AAEF,wBAAgB,2BAA2B,CACzC,aAAa,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GACzC,qBAAqB,CAevB;AAED,wBAAgB,yBAAyB,IAAI,0BAA0B,CA8DtE;AAED,wBAAgB,0BAA0B,CACxC,eAAe,GAAE,QAAa,GAC7B,MAAM,CAoDR"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-amplitude",
3
- "version": "0.2.0",
3
+ "version": "0.5.0",
4
4
  "description": "Amplitude Analytics and Experiment SDK for React Native powered by Nitro Modules and C++ on native platforms, with web-compatible fetch and storage fallbacks.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -11,13 +11,33 @@ import {
11
11
  CookieStorage,
12
12
  getCookieName,
13
13
  getQueryParams,
14
- FetchTransport,
15
14
  } from "@amplitude/analytics-core";
15
+ import type { Transport } from "@amplitude/analytics-core";
16
16
 
17
17
  import { LocalStorage } from "./storage/local-storage";
18
18
  import RemnantDataMigration from "./migration/remnant-data-migration";
19
- import { NitroAnalyticsStorage } from "../native/storage";
20
- import { nitroTransport } from "./nitro-transport";
19
+ import { isNative } from "./utils/platform";
20
+ import { NetworkGuardedFetchTransport } from "./network-guarded-fetch-transport";
21
+
22
+ function createDefaultStorage() {
23
+ if (isNative()) {
24
+ const { NitroAnalyticsStorage } =
25
+ require("../native/storage") as typeof import("../native/storage");
26
+ return new NitroAnalyticsStorage<Event[]>("analytics-events");
27
+ }
28
+ const { NitroAnalyticsStorage } =
29
+ require("../native/storage.web") as typeof import("../native/storage");
30
+ return new NitroAnalyticsStorage<Event[]>("analytics-events");
31
+ }
32
+
33
+ function getDefaultTransport(): Transport {
34
+ if (isNative()) {
35
+ const { nitroTransport } =
36
+ require("./nitro-transport") as typeof import("./nitro-transport");
37
+ return nitroTransport;
38
+ }
39
+ return new NetworkGuardedFetchTransport();
40
+ }
21
41
 
22
42
  export const getDefaultConfig = () => {
23
43
  const cookieStorage = new MemoryStorage<UserSession>();
@@ -44,10 +64,10 @@ export const getDefaultConfig = () => {
44
64
  disableCookies: true,
45
65
  domain: "",
46
66
  sessionTimeout: 5 * 60 * 1000,
47
- storageProvider: new NitroAnalyticsStorage<Event[]>("analytics-events"),
67
+ storageProvider: createDefaultStorage(),
48
68
  trackingSessionEvents: false,
49
69
  trackingOptions,
50
- transportProvider: nitroTransport,
70
+ transportProvider: getDefaultTransport(),
51
71
  };
52
72
  };
53
73
 
@@ -79,9 +99,10 @@ export class ReactNativeConfig extends Config implements IReactNativeConfig {
79
99
  flushIntervalMillis: 1000,
80
100
  flushMaxRetries: 5,
81
101
  flushQueueSize: 30,
82
- transportProvider: defaultConfig.transportProvider,
83
102
  ...options,
84
103
  apiKey,
104
+ transportProvider:
105
+ options?.transportProvider ?? defaultConfig.transportProvider,
85
106
  });
86
107
 
87
108
  // NOTE: Define `cookieStorage` first to persist user session
@@ -233,7 +254,8 @@ export const useReactNativeConfig = async (
233
254
  let lastEventId = previousCookies?.lastEventId;
234
255
 
235
256
  const storageProvider =
236
- options?.storageProvider ?? (await createEventsStorage(options));
257
+ options?.storageProvider ??
258
+ (await createEventsStorage(options, defaultConfig));
237
259
 
238
260
  if (options?.migrateLegacyData !== false) {
239
261
  const legacySessionData = await new RemnantDataMigration(
@@ -263,7 +285,8 @@ export const useReactNativeConfig = async (
263
285
  ...defaultConfig.trackingOptions,
264
286
  ...options?.trackingOptions,
265
287
  },
266
- transportProvider: options?.transportProvider ?? new FetchTransport(),
288
+ transportProvider:
289
+ options?.transportProvider ?? defaultConfig.transportProvider,
267
290
  userId,
268
291
  });
269
292
 
@@ -319,6 +342,7 @@ const createFlexibleStorage = async <T>(
319
342
 
320
343
  export const createEventsStorage = async (
321
344
  overrides?: ReactNativeOptions,
345
+ baseConfig = getDefaultConfig(),
322
346
  ): Promise<Storage<Event[]> | undefined> => {
323
347
  const hasStorageProviderProperty =
324
348
  overrides &&
@@ -331,6 +355,7 @@ export const createEventsStorage = async (
331
355
  if (!hasStorageProviderProperty || overrides.storageProvider) {
332
356
  for (const storage of [
333
357
  overrides?.storageProvider,
358
+ hasStorageProviderProperty ? undefined : baseConfig.storageProvider,
334
359
  new LocalStorage<Event[]>(),
335
360
  ]) {
336
361
  if (storage && (await storage.isEnabled())) {
@@ -25,6 +25,9 @@ export const {
25
25
  shutdown,
26
26
  track,
27
27
  extendSession,
28
+ flushWithResult,
29
+ getDiagnostics,
30
+ healthCheck,
28
31
  } = client;
29
32
 
30
33
  export { Revenue, Identify } from "@amplitude/analytics-core";
@@ -0,0 +1,10 @@
1
+ import { FetchTransport } from "@amplitude/analytics-core";
2
+ import type { Payload, Response } from "@amplitude/analytics-core";
3
+ import { assertNetworkEnabled } from "../network";
4
+
5
+ export class NetworkGuardedFetchTransport extends FetchTransport {
6
+ async send(serverUrl: string, payload: Payload): Promise<Response | null> {
7
+ assertNetworkEnabled();
8
+ return await super.send(serverUrl, payload);
9
+ }
10
+ }
@@ -1,6 +1,7 @@
1
1
  import { BaseTransport } from "@amplitude/analytics-core";
2
2
  import type { Payload, Response, Transport } from "@amplitude/analytics-core";
3
3
  import { nitroHttpClient } from "../native/http";
4
+ import { assertNetworkEnabled } from "../network";
4
5
 
5
6
  export class NitroTransport extends BaseTransport implements Transport {
6
7
  private readonly customHeaders: Record<string, string>;
@@ -11,6 +12,7 @@ export class NitroTransport extends BaseTransport implements Transport {
11
12
  }
12
13
 
13
14
  async send(serverUrl: string, payload: Payload): Promise<Response | null> {
15
+ assertNetworkEnabled();
14
16
  const response = await nitroHttpClient.request(
15
17
  serverUrl,
16
18
  "POST",
@@ -60,6 +60,8 @@ export class Context implements BeforePlugin {
60
60
  config: ReactNativeConfig;
61
61
  uaResult: UAParser.IResult;
62
62
  library = `amplitude-nitro-ts/${VERSION}`;
63
+ private nativeContext: NativeContext | undefined;
64
+ private nativeContextLoaded = false;
63
65
 
64
66
  constructor() {
65
67
  let agent: string | undefined;
@@ -77,8 +79,15 @@ export class Context implements BeforePlugin {
77
79
  }
78
80
 
79
81
  private getNativeContext(): NativeContext | undefined {
82
+ if (this.nativeContextLoaded) {
83
+ return this.nativeContext;
84
+ }
85
+ this.nativeContextLoaded = true;
80
86
  try {
81
- return getNativeApplicationContext(this.config.trackingOptions);
87
+ this.nativeContext = getNativeApplicationContext(
88
+ this.config.trackingOptions,
89
+ );
90
+ return this.nativeContext;
82
91
  } catch (error) {
83
92
  this.config.loggerProvider?.error(
84
93
  `Failed to load native application context: ${String(error)}`,
@@ -27,6 +27,7 @@ import {
27
27
  SpecialEventType,
28
28
  AnalyticsClient,
29
29
  } from "@amplitude/analytics-core";
30
+ import { healthCheck } from "../diagnostics";
30
31
  import { CampaignTracker } from "./campaign/campaign-tracker";
31
32
  import { Context } from "./plugins/context";
32
33
  import { useReactNativeConfig, createCookieStorage } from "./config";
@@ -41,15 +42,61 @@ type ScheduledDestination = {
41
42
  flushId?: ReturnType<typeof setTimeout> | null;
42
43
  queue?: unknown[];
43
44
  resetSchedule?: () => void;
45
+ fulfillRequest?: (list: unknown[], code: number, message: string) => unknown;
46
+ };
47
+
48
+ type FlushOutcome = {
49
+ code: number;
50
+ count: number;
51
+ message: string;
44
52
  };
45
53
 
46
54
  export type AmplitudeReactNativeClient = ReactNativeClient & {
47
55
  shutdown: () => void;
56
+ flushWithResult: () => Promise<AmplitudeFlushResult>;
57
+ getDiagnostics: () => AmplitudeAnalyticsDiagnostics;
58
+ healthCheck: () => Promise<AmplitudeHealthCheckResult>;
59
+ };
60
+
61
+ export type AmplitudeFlushResult = {
62
+ ok: boolean;
63
+ sent: number;
64
+ failed: number;
65
+ dropped: number;
66
+ retried: number;
67
+ reason?: string;
68
+ result?: Result;
69
+ finishedAt: number;
70
+ };
71
+
72
+ export type AmplitudeAnalyticsDiagnostics = {
73
+ initialized: boolean;
74
+ instanceName?: string;
75
+ userId?: string;
76
+ deviceId?: string;
77
+ sessionId?: number;
78
+ queueSize: number;
79
+ lastFlushTime?: number;
80
+ lastFlushDurationMillis?: number;
81
+ lastFlushError?: string;
82
+ activeInstanceNames: string[];
83
+ };
84
+
85
+ export type AmplitudeHealthCheckResult = {
86
+ ok: boolean;
87
+ analyticsInitialized: boolean;
88
+ nativeAvailable: boolean;
89
+ storageWritable: boolean;
90
+ errors: string[];
48
91
  };
49
92
 
50
93
  let nextConnectorOwnerId = 0;
51
94
  const activeConnectorOwnerIds = new Map<string, number>();
52
95
 
96
+ export function getActiveAnalyticsInstanceNames(): string[] {
97
+ return Array.from(activeConnectorOwnerIds.keys()).sort();
98
+ }
99
+
53
100
  export class AmplitudeReactNative
54
101
  extends AmplitudeCore
55
102
  implements ReactNativeClient, AnalyticsClient
@@ -58,6 +105,9 @@ export class AmplitudeReactNative
58
105
  private appStateChangeHandler: NativeEventSubscription | undefined;
59
106
  private initPromise: Promise<void> | undefined;
60
107
  private readonly connectorOwnerId = ++nextConnectorOwnerId;
108
+ private lastFlushTime: number | undefined;
109
+ private lastFlushDurationMillis: number | undefined;
110
+ private lastFlushError: string | undefined;
61
111
  explicitSessionId: number | undefined;
62
112
 
63
113
  // @ts-ignore
@@ -279,6 +329,95 @@ export class AmplitudeReactNative
279
329
  return this.config?.sessionId;
280
330
  }
281
331
 
332
+ async flushWithResult(): Promise<AmplitudeFlushResult> {
333
+ const queueSize = this.getQueueSize();
334
+ const collector = this.collectFlushOutcomes();
335
+ const startedAt = Date.now();
336
+ try {
337
+ await this.flush().promise;
338
+ const outcomes = collector.finish();
339
+ this.lastFlushTime = Date.now();
340
+ this.lastFlushDurationMillis = this.lastFlushTime - startedAt;
341
+ const remainingQueueSize = this.getQueueSize();
342
+ const failedOutcomes = outcomes.filter(
343
+ (outcome) => !this.isSuccessStatusCode(outcome.code),
344
+ );
345
+ const dropped = failedOutcomes.reduce(
346
+ (total, outcome) => total + outcome.count,
347
+ 0,
348
+ );
349
+ if (remainingQueueSize > 0) {
350
+ this.lastFlushError = `Flush completed with ${remainingQueueSize} queued event(s) remaining`;
351
+ return {
352
+ ok: false,
353
+ sent: this.countSuccessfulOutcomes(outcomes),
354
+ failed: remainingQueueSize,
355
+ dropped,
356
+ retried: remainingQueueSize,
357
+ reason: this.lastFlushError,
358
+ finishedAt: this.lastFlushTime,
359
+ };
360
+ }
361
+ if (dropped > 0) {
362
+ this.lastFlushError =
363
+ failedOutcomes[0]?.message ??
364
+ `Flush dropped ${dropped} event(s) without retry`;
365
+ return {
366
+ ok: false,
367
+ sent: this.countSuccessfulOutcomes(outcomes),
368
+ failed: dropped,
369
+ dropped,
370
+ retried: 0,
371
+ reason: this.lastFlushError,
372
+ finishedAt: this.lastFlushTime,
373
+ };
374
+ }
375
+ this.lastFlushError = undefined;
376
+ return {
377
+ ok: true,
378
+ sent: this.countSuccessfulOutcomes(outcomes) || queueSize,
379
+ failed: 0,
380
+ dropped: 0,
381
+ retried: 0,
382
+ finishedAt: this.lastFlushTime,
383
+ };
384
+ } catch (error) {
385
+ collector.finish();
386
+ this.lastFlushTime = Date.now();
387
+ this.lastFlushDurationMillis = this.lastFlushTime - startedAt;
388
+ this.lastFlushError =
389
+ error instanceof Error ? error.message : String(error);
390
+ return {
391
+ ok: false,
392
+ sent: 0,
393
+ failed: queueSize,
394
+ dropped: 0,
395
+ retried: 0,
396
+ reason: this.lastFlushError,
397
+ finishedAt: this.lastFlushTime,
398
+ };
399
+ }
400
+ }
401
+
402
+ getDiagnostics(): AmplitudeAnalyticsDiagnostics {
403
+ return {
404
+ initialized: Boolean(this.config && this.isReady),
405
+ instanceName: this.config?.instanceName,
406
+ userId: this.getUserId(),
407
+ deviceId: this.getDeviceId(),
408
+ sessionId: this.getSessionId(),
409
+ queueSize: this.getQueueSize(),
410
+ lastFlushTime: this.lastFlushTime,
411
+ lastFlushDurationMillis: this.lastFlushDurationMillis,
412
+ lastFlushError: this.lastFlushError,
413
+ activeInstanceNames: getActiveAnalyticsInstanceNames(),
414
+ };
415
+ }
416
+
417
+ async healthCheck(): Promise<AmplitudeHealthCheckResult> {
418
+ return healthCheck(this);
419
+ }
420
+
282
421
  getIdentity() {
283
422
  return {
284
423
  userId: this.getUserId(),
@@ -309,6 +448,66 @@ export class AmplitudeReactNative
309
448
  this.config.lastEventTime = this.currentTimeMillis();
310
449
  }
311
450
 
451
+ private getQueueSize(): number {
452
+ let queueSize =
453
+ this.q.length + this.dispatchQ.length + this.timeline.queue.length;
454
+ this.timeline.plugins.forEach((plugin) => {
455
+ if (plugin.type !== "destination") {
456
+ return;
457
+ }
458
+ const destination = plugin as ScheduledDestination;
459
+ queueSize += destination.queue?.length ?? 0;
460
+ });
461
+ return queueSize;
462
+ }
463
+
464
+ private collectFlushOutcomes(): { finish: () => FlushOutcome[] } {
465
+ const outcomes: FlushOutcome[] = [];
466
+ const restorers: (() => void)[] = [];
467
+
468
+ this.timeline.plugins.forEach((plugin) => {
469
+ if (plugin.type !== "destination") {
470
+ return;
471
+ }
472
+
473
+ const destination = plugin as ScheduledDestination;
474
+ if (!destination.fulfillRequest) {
475
+ return;
476
+ }
477
+
478
+ const original = destination.fulfillRequest;
479
+ destination.fulfillRequest = (list, code, message) => {
480
+ outcomes.push({ code, count: list.length, message });
481
+ return original.call(destination, list, code, message);
482
+ };
483
+ restorers.push(() => {
484
+ destination.fulfillRequest = original;
485
+ });
486
+ });
487
+
488
+ return {
489
+ finish: () => {
490
+ while (restorers.length > 0) {
491
+ restorers.pop()?.();
492
+ }
493
+ return outcomes;
494
+ },
495
+ };
496
+ }
497
+
498
+ private countSuccessfulOutcomes(outcomes: FlushOutcome[]): number {
499
+ return outcomes.reduce((total, outcome) => {
500
+ if (!this.isSuccessStatusCode(outcome.code)) {
501
+ return total;
502
+ }
503
+ return total + outcome.count;
504
+ }, 0);
505
+ }
506
+
507
+ private isSuccessStatusCode(code: number): boolean {
508
+ return code >= 200 && code < 300;
509
+ }
510
+
312
511
  private setSessionIdInternal(sessionId: number, eventTime: number) {
313
512
  const previousSessionId = this.config.sessionId;
314
513
  if (previousSessionId === sessionId) {
@@ -519,6 +718,12 @@ export const createInstance = (): AmplitudeReactNativeClient => {
519
718
  getClientLogConfig(client),
520
719
  getClientStates(client, ["config.apiKey", "timeline.queue.length"]),
521
720
  ),
721
+ flushWithResult: debugWrapper(
722
+ client.flushWithResult.bind(client),
723
+ "flushWithResult",
724
+ getClientLogConfig(client),
725
+ getClientStates(client, ["config.apiKey", "timeline.queue.length"]),
726
+ ),
522
727
  getUserId: debugWrapper(
523
728
  client.getUserId.bind(client),
524
729
  "getUserId",
@@ -579,6 +784,18 @@ export const createInstance = (): AmplitudeReactNativeClient => {
579
784
  getClientLogConfig(client),
580
785
  getClientStates(client, ["config", "timeline.plugins"]),
581
786
  ),
787
+ getDiagnostics: debugWrapper(
788
+ client.getDiagnostics.bind(client),
789
+ "getDiagnostics",
790
+ getClientLogConfig(client),
791
+ getClientStates(client, ["config", "timeline.plugins"]),
792
+ ),
793
+ healthCheck: debugWrapper(
794
+ client.healthCheck.bind(client),
795
+ "healthCheck",
796
+ getClientLogConfig(client),
797
+ getClientStates(client, ["config", "timeline.plugins"]),
798
+ ),
582
799
  };
583
800
  };
584
801
 
@@ -0,0 +1,119 @@
1
+ import type {
2
+ AmplitudeAnalyticsDiagnostics,
3
+ AmplitudeHealthCheckResult,
4
+ AmplitudeReactNativeClient,
5
+ } from "./analytics/react-native-client";
6
+ import { isNative } from "./analytics/utils/platform";
7
+ import { getAmplitudeErrorCode } from "./errors";
8
+ import { getNetworkEnabled } from "./network";
9
+
10
+ type HybridModule = typeof import("./native/hybrid");
11
+
12
+ export type NativeStartupDiagnostics = {
13
+ nitroModulesAvailable: boolean;
14
+ contextAvailable: boolean;
15
+ storageAvailable: boolean;
16
+ workerAvailable: boolean;
17
+ nativeAvailable: boolean;
18
+ lastError?: {
19
+ code: string;
20
+ message: string;
21
+ };
22
+ };
23
+
24
+ export type AmplitudeDiagnostics = AmplitudeAnalyticsDiagnostics & {
25
+ native: NativeStartupDiagnostics;
26
+ networkEnabled: boolean;
27
+ };
28
+
29
+ let lastNativeError: NativeStartupDiagnostics["lastError"];
30
+
31
+ function getHybridModule(): HybridModule {
32
+ if (isNative()) {
33
+ return require("./native/hybrid") as HybridModule;
34
+ }
35
+ return require("./native/hybrid.web") as HybridModule;
36
+ }
37
+
38
+ export function getLastNativeError(): NativeStartupDiagnostics["lastError"] {
39
+ return lastNativeError;
40
+ }
41
+
42
+ export function getNativeStartupDiagnostics(): NativeStartupDiagnostics {
43
+ const result: NativeStartupDiagnostics = {
44
+ nitroModulesAvailable: true,
45
+ contextAvailable: false,
46
+ storageAvailable: false,
47
+ workerAvailable: false,
48
+ nativeAvailable: false,
49
+ lastError: lastNativeError,
50
+ };
51
+
52
+ try {
53
+ const { getAmplitudeContext, getAmplitudeStorage, getAmplitudeWorker } =
54
+ getHybridModule();
55
+ getAmplitudeContext();
56
+ result.contextAvailable = true;
57
+ getAmplitudeStorage();
58
+ result.storageAvailable = true;
59
+ getAmplitudeWorker();
60
+ result.workerAvailable = true;
61
+ result.nativeAvailable = true;
62
+ lastNativeError = undefined;
63
+ result.lastError = undefined;
64
+ } catch (error) {
65
+ lastNativeError = {
66
+ code: getAmplitudeErrorCode(error),
67
+ message: error instanceof Error ? error.message : String(error),
68
+ };
69
+ result.nitroModulesAvailable = false;
70
+ result.lastError = lastNativeError;
71
+ }
72
+
73
+ return result;
74
+ }
75
+
76
+ export function getAmplitudeDiagnostics(
77
+ analytics: AmplitudeReactNativeClient,
78
+ ): AmplitudeDiagnostics {
79
+ return {
80
+ ...analytics.getDiagnostics(),
81
+ native: getNativeStartupDiagnostics(),
82
+ networkEnabled: getNetworkEnabled(),
83
+ };
84
+ }
85
+
86
+ export async function healthCheck(
87
+ analytics?: AmplitudeReactNativeClient,
88
+ ): Promise<AmplitudeHealthCheckResult> {
89
+ const errors: string[] = [];
90
+ const native = getNativeStartupDiagnostics();
91
+ let storageWritable = false;
92
+
93
+ try {
94
+ const { getAmplitudeStorage } = getHybridModule();
95
+ const storage = getAmplitudeStorage();
96
+ const key = `health::${Date.now()}`;
97
+ storage.set(key, "ok", false);
98
+ storageWritable = storage.get(key, false) === "ok";
99
+ storage.remove(key, false);
100
+ } catch (error) {
101
+ errors.push(error instanceof Error ? error.message : String(error));
102
+ }
103
+
104
+ if (!native.nativeAvailable && native.lastError) {
105
+ errors.push(native.lastError.message);
106
+ }
107
+
108
+ const analyticsInitialized = analytics
109
+ ? analytics.getDiagnostics().initialized
110
+ : false;
111
+
112
+ return {
113
+ ok: native.nativeAvailable && storageWritable,
114
+ analyticsInitialized,
115
+ nativeAvailable: native.nativeAvailable,
116
+ storageWritable,
117
+ errors,
118
+ };
119
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,60 @@
1
+ export type AmplitudeErrorCode =
2
+ | "not_initialized"
3
+ | "network_error"
4
+ | "storage_error"
5
+ | "invalid_api_key"
6
+ | "invalid_deployment_key"
7
+ | "experiment_fetch_failed"
8
+ | "native_unavailable"
9
+ | "serialization_error"
10
+ | "event_too_large"
11
+ | "timeout"
12
+ | "unknown";
13
+
14
+ export class AmplitudeError extends Error {
15
+ readonly code: AmplitudeErrorCode;
16
+ readonly cause?: unknown;
17
+
18
+ constructor(code: AmplitudeErrorCode, message: string, cause?: unknown) {
19
+ super(message);
20
+ this.name = "AmplitudeError";
21
+ this.code = code;
22
+ this.cause = cause;
23
+ }
24
+ }
25
+
26
+ export function createAmplitudeError(
27
+ code: AmplitudeErrorCode,
28
+ message: string,
29
+ cause?: unknown,
30
+ ): AmplitudeError {
31
+ return new AmplitudeError(code, message, cause);
32
+ }
33
+
34
+ export function getAmplitudeErrorCode(error: unknown): AmplitudeErrorCode {
35
+ if (error instanceof AmplitudeError) {
36
+ return error.code;
37
+ }
38
+ if (error instanceof Error) {
39
+ const message = error.message.toLowerCase();
40
+ if (message.includes("deployment key")) {
41
+ return "invalid_deployment_key";
42
+ }
43
+ if (message.includes("api key")) {
44
+ return "invalid_api_key";
45
+ }
46
+ if (message.includes("timeout")) {
47
+ return "timeout";
48
+ }
49
+ if (message.includes("network") || message.includes("fetch")) {
50
+ return "network_error";
51
+ }
52
+ if (message.includes("storage")) {
53
+ return "storage_error";
54
+ }
55
+ if (message.includes("nitro") || message.includes("native")) {
56
+ return "native_unavailable";
57
+ }
58
+ }
59
+ return "unknown";
60
+ }