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.
- package/README.md +387 -8
- package/cpp/bindings/HybridAmplitudeWorker.cpp +5 -1
- package/lib/commonjs/analytics/config.js +31 -10
- package/lib/commonjs/analytics/config.js.map +1 -1
- package/lib/commonjs/analytics/index.js +8 -2
- package/lib/commonjs/analytics/index.js.map +1 -1
- package/lib/commonjs/analytics/network-guarded-fetch-transport.js +16 -0
- package/lib/commonjs/analytics/network-guarded-fetch-transport.js.map +1 -0
- package/lib/commonjs/analytics/nitro-transport.js +2 -0
- package/lib/commonjs/analytics/nitro-transport.js.map +1 -1
- package/lib/commonjs/analytics/plugins/context.js +7 -1
- package/lib/commonjs/analytics/plugins/context.js.map +1 -1
- package/lib/commonjs/analytics/react-native-client.js +142 -1
- package/lib/commonjs/analytics/react-native-client.js.map +1 -1
- package/lib/commonjs/diagnostics.js +92 -0
- package/lib/commonjs/diagnostics.js.map +1 -0
- package/lib/commonjs/errors.js +48 -0
- package/lib/commonjs/errors.js.map +1 -0
- package/lib/commonjs/experiment/experimentClient.js +84 -2
- package/lib/commonjs/experiment/experimentClient.js.map +1 -1
- package/lib/commonjs/experiment/index.js +12 -0
- package/lib/commonjs/experiment/index.js.map +1 -1
- package/lib/commonjs/experiment/stubClient.js +25 -0
- package/lib/commonjs/experiment/stubClient.js.map +1 -1
- package/lib/commonjs/experiment/typed-variants.js +52 -0
- package/lib/commonjs/experiment/typed-variants.js.map +1 -0
- package/lib/commonjs/experiment/types/config.js +32 -4
- package/lib/commonjs/experiment/types/config.js.map +1 -1
- package/lib/commonjs/index.js +105 -3
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +110 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/native/http.js +23 -8
- package/lib/commonjs/native/http.js.map +1 -1
- package/lib/commonjs/native/http.web.js +2 -0
- package/lib/commonjs/native/http.web.js.map +1 -1
- package/lib/commonjs/native/hybrid.web.js +23 -0
- package/lib/commonjs/native/hybrid.web.js.map +1 -0
- package/lib/commonjs/native/storage.js +27 -15
- package/lib/commonjs/native/storage.js.map +1 -1
- package/lib/commonjs/network.js +154 -0
- package/lib/commonjs/network.js.map +1 -0
- package/lib/commonjs/presets.js +118 -0
- package/lib/commonjs/presets.js.map +1 -0
- package/lib/commonjs/testing.js +166 -0
- package/lib/commonjs/testing.js.map +1 -0
- package/lib/module/analytics/config.js +32 -11
- package/lib/module/analytics/config.js.map +1 -1
- package/lib/module/analytics/index.js +4 -1
- package/lib/module/analytics/index.js.map +1 -1
- package/lib/module/analytics/network-guarded-fetch-transport.js +11 -0
- package/lib/module/analytics/network-guarded-fetch-transport.js.map +1 -0
- package/lib/module/analytics/nitro-transport.js +2 -0
- package/lib/module/analytics/nitro-transport.js.map +1 -1
- package/lib/module/analytics/plugins/context.js +7 -1
- package/lib/module/analytics/plugins/context.js.map +1 -1
- package/lib/module/analytics/react-native-client.js +141 -1
- package/lib/module/analytics/react-native-client.js.map +1 -1
- package/lib/module/diagnostics.js +85 -0
- package/lib/module/diagnostics.js.map +1 -0
- package/lib/module/errors.js +41 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/experiment/experimentClient.js +84 -2
- package/lib/module/experiment/experimentClient.js.map +1 -1
- package/lib/module/experiment/index.js +1 -0
- package/lib/module/experiment/index.js.map +1 -1
- package/lib/module/experiment/stubClient.js +25 -0
- package/lib/module/experiment/stubClient.js.map +1 -1
- package/lib/module/experiment/typed-variants.js +43 -0
- package/lib/module/experiment/typed-variants.js.map +1 -0
- package/lib/module/experiment/types/config.js +31 -4
- package/lib/module/experiment/types/config.js.map +1 -1
- package/lib/module/index.js +15 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +19 -6
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/native/http.js +23 -8
- package/lib/module/native/http.js.map +1 -1
- package/lib/module/native/http.web.js +2 -0
- package/lib/module/native/http.web.js.map +1 -1
- package/lib/module/native/hybrid.web.js +16 -0
- package/lib/module/native/hybrid.web.js.map +1 -0
- package/lib/module/native/storage.js +27 -15
- package/lib/module/native/storage.js.map +1 -1
- package/lib/module/network.js +138 -0
- package/lib/module/network.js.map +1 -0
- package/lib/module/presets.js +110 -0
- package/lib/module/presets.js.map +1 -0
- package/lib/module/testing.js +160 -0
- package/lib/module/testing.js.map +1 -0
- package/lib/typescript/analytics/config.d.ts +19 -6
- package/lib/typescript/analytics/config.d.ts.map +1 -1
- package/lib/typescript/analytics/index.d.ts +1 -1
- package/lib/typescript/analytics/index.d.ts.map +1 -1
- package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts +6 -0
- package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts.map +1 -0
- package/lib/typescript/analytics/nitro-transport.d.ts.map +1 -1
- package/lib/typescript/analytics/plugins/context.d.ts +2 -0
- package/lib/typescript/analytics/plugins/context.d.ts.map +1 -1
- package/lib/typescript/analytics/react-native-client.d.ts +43 -0
- package/lib/typescript/analytics/react-native-client.d.ts.map +1 -1
- package/lib/typescript/diagnostics.d.ts +21 -0
- package/lib/typescript/diagnostics.d.ts.map +1 -0
- package/lib/typescript/errors.d.ts +9 -0
- package/lib/typescript/errors.d.ts.map +1 -0
- package/lib/typescript/experiment/experimentClient.d.ts +9 -1
- package/lib/typescript/experiment/experimentClient.d.ts.map +1 -1
- package/lib/typescript/experiment/index.d.ts +1 -0
- package/lib/typescript/experiment/index.d.ts.map +1 -1
- package/lib/typescript/experiment/stubClient.d.ts +6 -1
- package/lib/typescript/experiment/stubClient.d.ts.map +1 -1
- package/lib/typescript/experiment/typed-variants.d.ts +9 -0
- package/lib/typescript/experiment/typed-variants.d.ts.map +1 -0
- package/lib/typescript/experiment/types/client.d.ts +21 -0
- package/lib/typescript/experiment/types/client.d.ts.map +1 -1
- package/lib/typescript/experiment/types/config.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +12 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +16 -6
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/native/http.d.ts.map +1 -1
- package/lib/typescript/native/http.web.d.ts.map +1 -1
- package/lib/typescript/native/hybrid.web.d.ts +8 -0
- package/lib/typescript/native/hybrid.web.d.ts.map +1 -0
- package/lib/typescript/native/storage.d.ts.map +1 -1
- package/lib/typescript/network.d.ts +50 -0
- package/lib/typescript/network.d.ts.map +1 -0
- package/lib/typescript/presets.d.ts +50 -0
- package/lib/typescript/presets.d.ts.map +1 -0
- package/lib/typescript/testing.d.ts +11 -0
- package/lib/typescript/testing.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/analytics/config.ts +33 -8
- package/src/analytics/index.ts +3 -0
- package/src/analytics/network-guarded-fetch-transport.ts +10 -0
- package/src/analytics/nitro-transport.ts +2 -0
- package/src/analytics/plugins/context.ts +10 -1
- package/src/analytics/react-native-client.ts +217 -0
- package/src/diagnostics.ts +119 -0
- package/src/errors.ts +60 -0
- package/src/experiment/experimentClient.ts +116 -3
- package/src/experiment/index.ts +1 -0
- package/src/experiment/stubClient.ts +42 -1
- package/src/experiment/typed-variants.ts +68 -0
- package/src/experiment/types/client.ts +29 -0
- package/src/experiment/types/config.ts +29 -5
- package/src/index.ts +28 -2
- package/src/index.web.ts +33 -5
- package/src/native/http.ts +38 -8
- package/src/native/http.web.ts +2 -0
- package/src/native/hybrid.web.ts +21 -0
- package/src/native/storage.ts +27 -25
- package/src/network.ts +258 -0
- package/src/presets.ts +208 -0
- 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.
|
|
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",
|
package/src/analytics/config.ts
CHANGED
|
@@ -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 {
|
|
20
|
-
import {
|
|
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:
|
|
67
|
+
storageProvider: createDefaultStorage(),
|
|
48
68
|
trackingSessionEvents: false,
|
|
49
69
|
trackingOptions,
|
|
50
|
-
transportProvider:
|
|
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 ??
|
|
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:
|
|
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())) {
|
package/src/analytics/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|