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.
- package/README.md +547 -55
- package/cpp/bindings/HybridAmplitudeWorker.cpp +27 -7
- 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 +155 -9
- 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/transport/http.js +8 -2
- package/lib/commonjs/experiment/transport/http.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 +387 -13
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/native/context.web.js +26 -0
- package/lib/commonjs/native/context.web.js.map +1 -0
- package/lib/commonjs/native/http.js +23 -8
- package/lib/commonjs/native/http.js.map +1 -1
- package/lib/commonjs/native/http.web.js +17 -0
- package/lib/commonjs/native/http.web.js.map +1 -0
- 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/native/storage.web.js +135 -0
- package/lib/commonjs/native/storage.web.js.map +1 -0
- 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 +154 -9
- 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/transport/http.js +8 -2
- package/lib/module/experiment/transport/http.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 +60 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/native/context.web.js +18 -0
- package/lib/module/native/context.web.js.map +1 -0
- package/lib/module/native/http.js +23 -8
- package/lib/module/native/http.js.map +1 -1
- package/lib/module/native/http.web.js +12 -0
- package/lib/module/native/http.web.js.map +1 -0
- 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/native/storage.web.js +128 -0
- package/lib/module/native/storage.web.js.map +1 -0
- 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 +46 -6
- 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/transport/http.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 +37 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/native/context.web.d.ts +8 -0
- package/lib/typescript/native/context.web.d.ts.map +1 -0
- package/lib/typescript/native/http.d.ts.map +1 -1
- package/lib/typescript/native/http.web.d.ts +6 -0
- package/lib/typescript/native/http.web.d.ts.map +1 -0
- 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/native/storage.web.d.ts +30 -0
- package/lib/typescript/native/storage.web.d.ts.map +1 -0
- 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 +4 -2
- 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 +238 -9
- 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/transport/http.ts +10 -2
- 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 +89 -14
- package/src/native/context.web.ts +38 -0
- package/src/native/http.ts +38 -8
- package/src/native/http.web.ts +24 -0
- package/src/native/hybrid.web.ts +21 -0
- package/src/native/storage.ts +27 -25
- package/src/native/storage.web.ts +152 -0
- package/src/network.ts +258 -0
- package/src/presets.ts +208 -0
- package/src/testing.ts +177 -0
|
@@ -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,14 +42,60 @@ 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>();
|
|
95
|
+
|
|
96
|
+
export function getActiveAnalyticsInstanceNames(): string[] {
|
|
97
|
+
return Array.from(activeConnectorOwnerIds.keys()).sort();
|
|
98
|
+
}
|
|
52
99
|
|
|
53
100
|
export class AmplitudeReactNative
|
|
54
101
|
extends AmplitudeCore
|
|
@@ -58,11 +105,14 @@ 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
|
|
64
114
|
config: ReactNativeConfig;
|
|
65
|
-
userProperties:
|
|
115
|
+
userProperties: Record<string, unknown> | undefined;
|
|
66
116
|
|
|
67
117
|
init(apiKey = "", userId?: string, options?: ReactNativeOptions) {
|
|
68
118
|
this.initPromise =
|
|
@@ -99,7 +149,8 @@ export class AmplitudeReactNative
|
|
|
99
149
|
// Set up the analytics connector to integrate with the experiment SDK.
|
|
100
150
|
// Send events from the experiment SDK and forward identifies to the
|
|
101
151
|
// identity store.
|
|
102
|
-
const
|
|
152
|
+
const connectorInstanceName = this.getConnectorInstanceName();
|
|
153
|
+
const connector = getAnalyticsConnector(connectorInstanceName);
|
|
103
154
|
connector.identityStore.setIdentity({
|
|
104
155
|
userId: this.config.userId,
|
|
105
156
|
deviceId: this.config.deviceId,
|
|
@@ -140,7 +191,7 @@ export class AmplitudeReactNative
|
|
|
140
191
|
this.track(event.eventType, event.eventProperties).promise,
|
|
141
192
|
);
|
|
142
193
|
});
|
|
143
|
-
|
|
194
|
+
activeConnectorOwnerIds.set(connectorInstanceName, this.connectorOwnerId);
|
|
144
195
|
} catch (error) {
|
|
145
196
|
if (appStateHandlerInstalled) {
|
|
146
197
|
this.appStateChangeHandler?.remove();
|
|
@@ -162,11 +213,18 @@ export class AmplitudeReactNative
|
|
|
162
213
|
this.dispatchQ = [];
|
|
163
214
|
this.isReady = false;
|
|
164
215
|
|
|
165
|
-
|
|
166
|
-
|
|
216
|
+
const connectorInstanceName = this.config
|
|
217
|
+
? this.getConnectorInstanceName()
|
|
218
|
+
: undefined;
|
|
219
|
+
if (
|
|
220
|
+
connectorInstanceName &&
|
|
221
|
+
activeConnectorOwnerIds.get(connectorInstanceName) ===
|
|
222
|
+
this.connectorOwnerId
|
|
223
|
+
) {
|
|
224
|
+
const connector = getAnalyticsConnector(connectorInstanceName);
|
|
167
225
|
connector.eventBridge.setEventReceiver(() => undefined);
|
|
168
226
|
connector.identityStore.setIdentity({});
|
|
169
|
-
|
|
227
|
+
activeConnectorOwnerIds.delete(connectorInstanceName);
|
|
170
228
|
}
|
|
171
229
|
}
|
|
172
230
|
|
|
@@ -178,6 +236,10 @@ export class AmplitudeReactNative
|
|
|
178
236
|
});
|
|
179
237
|
}
|
|
180
238
|
|
|
239
|
+
private getConnectorInstanceName() {
|
|
240
|
+
return this.config.instanceName ?? "$default_instance";
|
|
241
|
+
}
|
|
242
|
+
|
|
181
243
|
private cancelDestinationFlushes() {
|
|
182
244
|
this.timeline.plugins.forEach((plugin) => {
|
|
183
245
|
if (plugin.type !== "destination") {
|
|
@@ -232,7 +294,7 @@ export class AmplitudeReactNative
|
|
|
232
294
|
return;
|
|
233
295
|
}
|
|
234
296
|
this.config.userId = userId;
|
|
235
|
-
setConnectorUserId(userId);
|
|
297
|
+
setConnectorUserId(userId, this.getConnectorInstanceName());
|
|
236
298
|
}
|
|
237
299
|
|
|
238
300
|
getDeviceId() {
|
|
@@ -245,7 +307,7 @@ export class AmplitudeReactNative
|
|
|
245
307
|
return;
|
|
246
308
|
}
|
|
247
309
|
this.config.deviceId = deviceId;
|
|
248
|
-
setConnectorDeviceId(deviceId);
|
|
310
|
+
setConnectorDeviceId(deviceId, this.getConnectorInstanceName());
|
|
249
311
|
}
|
|
250
312
|
|
|
251
313
|
identify(identify: IIdentify, eventOptions?: EventOptions) {
|
|
@@ -267,6 +329,95 @@ export class AmplitudeReactNative
|
|
|
267
329
|
return this.config?.sessionId;
|
|
268
330
|
}
|
|
269
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
|
+
|
|
270
421
|
getIdentity() {
|
|
271
422
|
return {
|
|
272
423
|
userId: this.getUserId(),
|
|
@@ -297,6 +448,66 @@ export class AmplitudeReactNative
|
|
|
297
448
|
this.config.lastEventTime = this.currentTimeMillis();
|
|
298
449
|
}
|
|
299
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
|
+
|
|
300
511
|
private setSessionIdInternal(sessionId: number, eventTime: number) {
|
|
301
512
|
const previousSessionId = this.config.sessionId;
|
|
302
513
|
if (previousSessionId === sessionId) {
|
|
@@ -507,6 +718,12 @@ export const createInstance = (): AmplitudeReactNativeClient => {
|
|
|
507
718
|
getClientLogConfig(client),
|
|
508
719
|
getClientStates(client, ["config.apiKey", "timeline.queue.length"]),
|
|
509
720
|
),
|
|
721
|
+
flushWithResult: debugWrapper(
|
|
722
|
+
client.flushWithResult.bind(client),
|
|
723
|
+
"flushWithResult",
|
|
724
|
+
getClientLogConfig(client),
|
|
725
|
+
getClientStates(client, ["config.apiKey", "timeline.queue.length"]),
|
|
726
|
+
),
|
|
510
727
|
getUserId: debugWrapper(
|
|
511
728
|
client.getUserId.bind(client),
|
|
512
729
|
"getUserId",
|
|
@@ -567,6 +784,18 @@ export const createInstance = (): AmplitudeReactNativeClient => {
|
|
|
567
784
|
getClientLogConfig(client),
|
|
568
785
|
getClientStates(client, ["config", "timeline.plugins"]),
|
|
569
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
|
+
),
|
|
570
799
|
};
|
|
571
800
|
};
|
|
572
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
|
+
}
|
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
} from "./storage/cache";
|
|
31
31
|
import { MemoryStorage } from "./storage/local-storage";
|
|
32
32
|
import { FetchHttpClient, WrapperClient } from "./transport/http";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
Client,
|
|
35
|
+
ExperimentFetchResult,
|
|
36
|
+
ExperimentVariantResult,
|
|
37
|
+
FetchOptions,
|
|
38
|
+
} from "./types/client";
|
|
34
39
|
import { ExperimentConfig, Defaults } from "./types/config";
|
|
35
40
|
import { Exposure } from "./types/exposure";
|
|
36
41
|
import { LogLevel } from "./types/logger";
|
|
@@ -119,6 +124,9 @@ export class ExperimentClient implements Client {
|
|
|
119
124
|
private storedFetchSequenceNumber = 0;
|
|
120
125
|
private readonly fetchVariantsOptions: SingleValueStoreCache<GetVariantsOptions>;
|
|
121
126
|
private readonly stopCallbacks = new Set<() => void>();
|
|
127
|
+
private readonly inFlightFetches = new Map<string, Promise<Variants>>();
|
|
128
|
+
private lastFetchTime: number | undefined;
|
|
129
|
+
private lastFetchFailure: string | undefined;
|
|
122
130
|
|
|
123
131
|
/**
|
|
124
132
|
* Creates a new ExperimentClient instance.
|
|
@@ -341,6 +349,37 @@ export class ExperimentClient implements Client {
|
|
|
341
349
|
return this;
|
|
342
350
|
}
|
|
343
351
|
|
|
352
|
+
public async fetchWithMetadata(
|
|
353
|
+
user: ExperimentUser = this.user,
|
|
354
|
+
options?: FetchOptions,
|
|
355
|
+
): Promise<ExperimentFetchResult> {
|
|
356
|
+
const startedAt = Date.now();
|
|
357
|
+
const fetchUser = user ?? this.user;
|
|
358
|
+
this.setUser(fetchUser);
|
|
359
|
+
try {
|
|
360
|
+
const variants = await this.fetchWithRetries(fetchUser, options);
|
|
361
|
+
const flagKeys = Object.keys(variants);
|
|
362
|
+
return {
|
|
363
|
+
fetched: true,
|
|
364
|
+
flagKeys,
|
|
365
|
+
cacheHit: false,
|
|
366
|
+
durationMillis: Date.now() - startedAt,
|
|
367
|
+
source: "network",
|
|
368
|
+
};
|
|
369
|
+
} catch (error) {
|
|
370
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
371
|
+
this.lastFetchFailure = reason;
|
|
372
|
+
return {
|
|
373
|
+
fetched: false,
|
|
374
|
+
flagKeys: options?.flagKeys ?? Object.keys(this.variants.getAll()),
|
|
375
|
+
cacheHit: Object.keys(this.variants.getAll()).length > 0,
|
|
376
|
+
durationMillis: Date.now() - startedAt,
|
|
377
|
+
source: "cache",
|
|
378
|
+
failureReason: reason,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
344
383
|
public async fetchOrThrow(
|
|
345
384
|
user: ExperimentUser = this.user,
|
|
346
385
|
options?: FetchOptions,
|
|
@@ -380,6 +419,45 @@ export class ExperimentClient implements Client {
|
|
|
380
419
|
return sourceVariant.variant || {};
|
|
381
420
|
}
|
|
382
421
|
|
|
422
|
+
public variantWithMetadata(
|
|
423
|
+
key: string,
|
|
424
|
+
fallback?: string | Variant,
|
|
425
|
+
): ExperimentVariantResult {
|
|
426
|
+
if (!this.apiKey) {
|
|
427
|
+
return {
|
|
428
|
+
variant: { value: undefined },
|
|
429
|
+
fallback: true,
|
|
430
|
+
stale: false,
|
|
431
|
+
reason: "missing_flag",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const sourceVariant = this.variantAndSource(key, fallback);
|
|
435
|
+
if (this.config.automaticExposureTracking) {
|
|
436
|
+
this.exposureInternal(key, sourceVariant);
|
|
437
|
+
}
|
|
438
|
+
this.logger.debug(
|
|
439
|
+
`[Experiment] variant for ${key} is ${sourceVariant.variant?.value}`,
|
|
440
|
+
);
|
|
441
|
+
const variant = sourceVariant.variant || {};
|
|
442
|
+
const fallbackVariant = isFallback(sourceVariant.source);
|
|
443
|
+
const missingVariant = variant.value === undefined;
|
|
444
|
+
return {
|
|
445
|
+
variant,
|
|
446
|
+
source: sourceVariant.source,
|
|
447
|
+
fallback: fallbackVariant,
|
|
448
|
+
stale: false,
|
|
449
|
+
reason: missingVariant
|
|
450
|
+
? this.lastFetchFailure
|
|
451
|
+
? "fetch_failure"
|
|
452
|
+
: fallbackVariant
|
|
453
|
+
? "fallback"
|
|
454
|
+
: "no_assignment"
|
|
455
|
+
: fallbackVariant
|
|
456
|
+
? "fallback"
|
|
457
|
+
: undefined,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
383
461
|
/**
|
|
384
462
|
* Track an exposure event for the variant associated with the flag/experiment
|
|
385
463
|
* {@link key}.
|
|
@@ -431,6 +509,18 @@ export class ExperimentClient implements Client {
|
|
|
431
509
|
void this.variants.store().catch((e) => this.logger.warn(e));
|
|
432
510
|
}
|
|
433
511
|
|
|
512
|
+
public clearVariants(): void {
|
|
513
|
+
this.clear();
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
public hasCachedVariant(key: string): boolean {
|
|
517
|
+
return this.variants.get(key) !== undefined;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public getLastFetchTime(): number | undefined {
|
|
521
|
+
return this.lastFetchTime;
|
|
522
|
+
}
|
|
523
|
+
|
|
434
524
|
/**
|
|
435
525
|
* Get a copy of the internal {@link ExperimentUser} object if it is set.
|
|
436
526
|
*
|
|
@@ -788,12 +878,35 @@ export class ExperimentClient implements Client {
|
|
|
788
878
|
user: ExperimentUser,
|
|
789
879
|
options?: FetchOptions,
|
|
790
880
|
): Promise<Variants> {
|
|
791
|
-
|
|
881
|
+
const key = JSON.stringify({
|
|
882
|
+
user,
|
|
883
|
+
flagKeys: options?.flagKeys ?? null,
|
|
884
|
+
});
|
|
885
|
+
const inFlightFetch = this.inFlightFetches.get(key);
|
|
886
|
+
if (inFlightFetch) {
|
|
887
|
+
return await inFlightFetch;
|
|
888
|
+
}
|
|
889
|
+
const fetch = this.fetchInternal(
|
|
792
890
|
user,
|
|
793
891
|
this.config.fetchTimeoutMillis,
|
|
794
892
|
this.config.retryFetchOnFailure,
|
|
795
893
|
options,
|
|
796
|
-
)
|
|
894
|
+
)
|
|
895
|
+
.then((variants) => {
|
|
896
|
+
this.lastFetchTime = Date.now();
|
|
897
|
+
this.lastFetchFailure = undefined;
|
|
898
|
+
return variants;
|
|
899
|
+
})
|
|
900
|
+
.catch((error: unknown) => {
|
|
901
|
+
this.lastFetchFailure =
|
|
902
|
+
error instanceof Error ? error.message : String(error);
|
|
903
|
+
throw error;
|
|
904
|
+
})
|
|
905
|
+
.finally(() => {
|
|
906
|
+
this.inFlightFetches.delete(key);
|
|
907
|
+
});
|
|
908
|
+
this.inFlightFetches.set(key, fetch);
|
|
909
|
+
return await fetch;
|
|
797
910
|
}
|
|
798
911
|
|
|
799
912
|
private async doFetch(
|
package/src/experiment/index.ts
CHANGED