react-native-nitro-amplitude 0.1.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/README.md +547 -55
  2. package/cpp/bindings/HybridAmplitudeWorker.cpp +27 -7
  3. package/lib/commonjs/analytics/config.js +31 -10
  4. package/lib/commonjs/analytics/config.js.map +1 -1
  5. package/lib/commonjs/analytics/index.js +8 -2
  6. package/lib/commonjs/analytics/index.js.map +1 -1
  7. package/lib/commonjs/analytics/network-guarded-fetch-transport.js +16 -0
  8. package/lib/commonjs/analytics/network-guarded-fetch-transport.js.map +1 -0
  9. package/lib/commonjs/analytics/nitro-transport.js +2 -0
  10. package/lib/commonjs/analytics/nitro-transport.js.map +1 -1
  11. package/lib/commonjs/analytics/plugins/context.js +7 -1
  12. package/lib/commonjs/analytics/plugins/context.js.map +1 -1
  13. package/lib/commonjs/analytics/react-native-client.js +155 -9
  14. package/lib/commonjs/analytics/react-native-client.js.map +1 -1
  15. package/lib/commonjs/diagnostics.js +92 -0
  16. package/lib/commonjs/diagnostics.js.map +1 -0
  17. package/lib/commonjs/errors.js +48 -0
  18. package/lib/commonjs/errors.js.map +1 -0
  19. package/lib/commonjs/experiment/experimentClient.js +84 -2
  20. package/lib/commonjs/experiment/experimentClient.js.map +1 -1
  21. package/lib/commonjs/experiment/index.js +12 -0
  22. package/lib/commonjs/experiment/index.js.map +1 -1
  23. package/lib/commonjs/experiment/stubClient.js +25 -0
  24. package/lib/commonjs/experiment/stubClient.js.map +1 -1
  25. package/lib/commonjs/experiment/transport/http.js +8 -2
  26. package/lib/commonjs/experiment/transport/http.js.map +1 -1
  27. package/lib/commonjs/experiment/typed-variants.js +52 -0
  28. package/lib/commonjs/experiment/typed-variants.js.map +1 -0
  29. package/lib/commonjs/experiment/types/config.js +32 -4
  30. package/lib/commonjs/experiment/types/config.js.map +1 -1
  31. package/lib/commonjs/index.js +105 -3
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/commonjs/index.web.js +387 -13
  34. package/lib/commonjs/index.web.js.map +1 -1
  35. package/lib/commonjs/native/context.web.js +26 -0
  36. package/lib/commonjs/native/context.web.js.map +1 -0
  37. package/lib/commonjs/native/http.js +23 -8
  38. package/lib/commonjs/native/http.js.map +1 -1
  39. package/lib/commonjs/native/http.web.js +17 -0
  40. package/lib/commonjs/native/http.web.js.map +1 -0
  41. package/lib/commonjs/native/hybrid.web.js +23 -0
  42. package/lib/commonjs/native/hybrid.web.js.map +1 -0
  43. package/lib/commonjs/native/storage.js +27 -15
  44. package/lib/commonjs/native/storage.js.map +1 -1
  45. package/lib/commonjs/native/storage.web.js +135 -0
  46. package/lib/commonjs/native/storage.web.js.map +1 -0
  47. package/lib/commonjs/network.js +154 -0
  48. package/lib/commonjs/network.js.map +1 -0
  49. package/lib/commonjs/presets.js +118 -0
  50. package/lib/commonjs/presets.js.map +1 -0
  51. package/lib/commonjs/testing.js +166 -0
  52. package/lib/commonjs/testing.js.map +1 -0
  53. package/lib/module/analytics/config.js +32 -11
  54. package/lib/module/analytics/config.js.map +1 -1
  55. package/lib/module/analytics/index.js +4 -1
  56. package/lib/module/analytics/index.js.map +1 -1
  57. package/lib/module/analytics/network-guarded-fetch-transport.js +11 -0
  58. package/lib/module/analytics/network-guarded-fetch-transport.js.map +1 -0
  59. package/lib/module/analytics/nitro-transport.js +2 -0
  60. package/lib/module/analytics/nitro-transport.js.map +1 -1
  61. package/lib/module/analytics/plugins/context.js +7 -1
  62. package/lib/module/analytics/plugins/context.js.map +1 -1
  63. package/lib/module/analytics/react-native-client.js +154 -9
  64. package/lib/module/analytics/react-native-client.js.map +1 -1
  65. package/lib/module/diagnostics.js +85 -0
  66. package/lib/module/diagnostics.js.map +1 -0
  67. package/lib/module/errors.js +41 -0
  68. package/lib/module/errors.js.map +1 -0
  69. package/lib/module/experiment/experimentClient.js +84 -2
  70. package/lib/module/experiment/experimentClient.js.map +1 -1
  71. package/lib/module/experiment/index.js +1 -0
  72. package/lib/module/experiment/index.js.map +1 -1
  73. package/lib/module/experiment/stubClient.js +25 -0
  74. package/lib/module/experiment/stubClient.js.map +1 -1
  75. package/lib/module/experiment/transport/http.js +8 -2
  76. package/lib/module/experiment/transport/http.js.map +1 -1
  77. package/lib/module/experiment/typed-variants.js +43 -0
  78. package/lib/module/experiment/typed-variants.js.map +1 -0
  79. package/lib/module/experiment/types/config.js +31 -4
  80. package/lib/module/experiment/types/config.js.map +1 -1
  81. package/lib/module/index.js +15 -3
  82. package/lib/module/index.js.map +1 -1
  83. package/lib/module/index.web.js +60 -11
  84. package/lib/module/index.web.js.map +1 -1
  85. package/lib/module/native/context.web.js +18 -0
  86. package/lib/module/native/context.web.js.map +1 -0
  87. package/lib/module/native/http.js +23 -8
  88. package/lib/module/native/http.js.map +1 -1
  89. package/lib/module/native/http.web.js +12 -0
  90. package/lib/module/native/http.web.js.map +1 -0
  91. package/lib/module/native/hybrid.web.js +16 -0
  92. package/lib/module/native/hybrid.web.js.map +1 -0
  93. package/lib/module/native/storage.js +27 -15
  94. package/lib/module/native/storage.js.map +1 -1
  95. package/lib/module/native/storage.web.js +128 -0
  96. package/lib/module/native/storage.web.js.map +1 -0
  97. package/lib/module/network.js +138 -0
  98. package/lib/module/network.js.map +1 -0
  99. package/lib/module/presets.js +110 -0
  100. package/lib/module/presets.js.map +1 -0
  101. package/lib/module/testing.js +160 -0
  102. package/lib/module/testing.js.map +1 -0
  103. package/lib/typescript/analytics/config.d.ts +19 -6
  104. package/lib/typescript/analytics/config.d.ts.map +1 -1
  105. package/lib/typescript/analytics/index.d.ts +1 -1
  106. package/lib/typescript/analytics/index.d.ts.map +1 -1
  107. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts +6 -0
  108. package/lib/typescript/analytics/network-guarded-fetch-transport.d.ts.map +1 -0
  109. package/lib/typescript/analytics/nitro-transport.d.ts.map +1 -1
  110. package/lib/typescript/analytics/plugins/context.d.ts +2 -0
  111. package/lib/typescript/analytics/plugins/context.d.ts.map +1 -1
  112. package/lib/typescript/analytics/react-native-client.d.ts +46 -6
  113. package/lib/typescript/analytics/react-native-client.d.ts.map +1 -1
  114. package/lib/typescript/diagnostics.d.ts +21 -0
  115. package/lib/typescript/diagnostics.d.ts.map +1 -0
  116. package/lib/typescript/errors.d.ts +9 -0
  117. package/lib/typescript/errors.d.ts.map +1 -0
  118. package/lib/typescript/experiment/experimentClient.d.ts +9 -1
  119. package/lib/typescript/experiment/experimentClient.d.ts.map +1 -1
  120. package/lib/typescript/experiment/index.d.ts +1 -0
  121. package/lib/typescript/experiment/index.d.ts.map +1 -1
  122. package/lib/typescript/experiment/stubClient.d.ts +6 -1
  123. package/lib/typescript/experiment/stubClient.d.ts.map +1 -1
  124. package/lib/typescript/experiment/transport/http.d.ts.map +1 -1
  125. package/lib/typescript/experiment/typed-variants.d.ts +9 -0
  126. package/lib/typescript/experiment/typed-variants.d.ts.map +1 -0
  127. package/lib/typescript/experiment/types/client.d.ts +21 -0
  128. package/lib/typescript/experiment/types/client.d.ts.map +1 -1
  129. package/lib/typescript/experiment/types/config.d.ts.map +1 -1
  130. package/lib/typescript/index.d.ts +12 -3
  131. package/lib/typescript/index.d.ts.map +1 -1
  132. package/lib/typescript/index.web.d.ts +37 -8
  133. package/lib/typescript/index.web.d.ts.map +1 -1
  134. package/lib/typescript/native/context.web.d.ts +8 -0
  135. package/lib/typescript/native/context.web.d.ts.map +1 -0
  136. package/lib/typescript/native/http.d.ts.map +1 -1
  137. package/lib/typescript/native/http.web.d.ts +6 -0
  138. package/lib/typescript/native/http.web.d.ts.map +1 -0
  139. package/lib/typescript/native/hybrid.web.d.ts +8 -0
  140. package/lib/typescript/native/hybrid.web.d.ts.map +1 -0
  141. package/lib/typescript/native/storage.d.ts.map +1 -1
  142. package/lib/typescript/native/storage.web.d.ts +30 -0
  143. package/lib/typescript/native/storage.web.d.ts.map +1 -0
  144. package/lib/typescript/network.d.ts +50 -0
  145. package/lib/typescript/network.d.ts.map +1 -0
  146. package/lib/typescript/presets.d.ts +50 -0
  147. package/lib/typescript/presets.d.ts.map +1 -0
  148. package/lib/typescript/testing.d.ts +11 -0
  149. package/lib/typescript/testing.d.ts.map +1 -0
  150. package/package.json +4 -2
  151. package/src/analytics/config.ts +33 -8
  152. package/src/analytics/index.ts +3 -0
  153. package/src/analytics/network-guarded-fetch-transport.ts +10 -0
  154. package/src/analytics/nitro-transport.ts +2 -0
  155. package/src/analytics/plugins/context.ts +10 -1
  156. package/src/analytics/react-native-client.ts +238 -9
  157. package/src/diagnostics.ts +119 -0
  158. package/src/errors.ts +60 -0
  159. package/src/experiment/experimentClient.ts +116 -3
  160. package/src/experiment/index.ts +1 -0
  161. package/src/experiment/stubClient.ts +42 -1
  162. package/src/experiment/transport/http.ts +10 -2
  163. package/src/experiment/typed-variants.ts +68 -0
  164. package/src/experiment/types/client.ts +29 -0
  165. package/src/experiment/types/config.ts +29 -5
  166. package/src/index.ts +28 -2
  167. package/src/index.web.ts +89 -14
  168. package/src/native/context.web.ts +38 -0
  169. package/src/native/http.ts +38 -8
  170. package/src/native/http.web.ts +24 -0
  171. package/src/native/hybrid.web.ts +21 -0
  172. package/src/native/storage.ts +27 -25
  173. package/src/native/storage.web.ts +152 -0
  174. package/src/network.ts +258 -0
  175. package/src/presets.ts +208 -0
  176. package/src/testing.ts +177 -0
package/src/presets.ts ADDED
@@ -0,0 +1,208 @@
1
+ import type {
2
+ Event,
3
+ ReactNativeOptions,
4
+ UserSession,
5
+ } from "@amplitude/analytics-core";
6
+ import { getAnalyticsConnector } from "@amplitude/analytics-core";
7
+ import { createInstance } from "./analytics/react-native-client";
8
+ import type { AmplitudeReactNativeClient } from "./analytics/react-native-client";
9
+ import { NetworkGuardedFetchTransport } from "./analytics/network-guarded-fetch-transport";
10
+ import { isNative } from "./analytics/utils/platform";
11
+ import { Experiment } from "./experiment/factory";
12
+ import type { ExperimentClient } from "./experiment/experimentClient";
13
+ import type { ExperimentConfig } from "./experiment/types/config";
14
+ import type { ExperimentUser } from "./experiment/types/user";
15
+ import { dryRunHttpClient, dryRunTransport } from "./network";
16
+
17
+ function getStorageModule(): typeof import("./native/storage") {
18
+ if (isNative()) {
19
+ return require("./native/storage") as typeof import("./native/storage");
20
+ }
21
+ return require("./native/storage.web") as typeof import("./native/storage");
22
+ }
23
+
24
+ function getAnalyticsTransport() {
25
+ if (isNative()) {
26
+ const { nitroTransport } =
27
+ require("./analytics/nitro-transport") as typeof import("./analytics/nitro-transport");
28
+ return nitroTransport;
29
+ }
30
+ return new NetworkGuardedFetchTransport();
31
+ }
32
+
33
+ export type DurableAmplitudeStoragePreset = {
34
+ analytics: Pick<
35
+ ReactNativeOptions,
36
+ "storageProvider" | "cookieStorage" | "transportProvider"
37
+ >;
38
+ experiment: Pick<ExperimentConfig, "storage">;
39
+ clear: () => Promise<void>;
40
+ };
41
+
42
+ export type DurableAmplitudeStoragePresetOptions = {
43
+ namespace?: string;
44
+ dryRun?: boolean;
45
+ };
46
+
47
+ export type AmplitudeCombinedClientConfig = {
48
+ analyticsApiKey: string;
49
+ experimentDeploymentKey?: string;
50
+ userId?: string;
51
+ instanceName?: string;
52
+ analytics?: ReactNativeOptions;
53
+ experiment?: ExperimentConfig;
54
+ durableStorage?: boolean | DurableAmplitudeStoragePresetOptions;
55
+ dryRun?: boolean;
56
+ };
57
+
58
+ export type AmplitudeCombinedClient = {
59
+ analytics: AmplitudeReactNativeClient;
60
+ experiment?: ExperimentClient;
61
+ init: (user?: ExperimentUser) => Promise<AmplitudeCombinedClient>;
62
+ flush: () => Promise<void>;
63
+ reset: () => void;
64
+ getUserId: () => string | undefined;
65
+ getDeviceId: () => string | undefined;
66
+ };
67
+
68
+ export type AmplitudeCombinedClientConfigWithExperiment =
69
+ AmplitudeCombinedClientConfig & {
70
+ experimentDeploymentKey: string;
71
+ };
72
+
73
+ export type AmplitudeCombinedClientWithExperiment = Omit<
74
+ AmplitudeCombinedClient,
75
+ "experiment" | "init"
76
+ > & {
77
+ experiment: ExperimentClient;
78
+ init: (
79
+ user?: ExperimentUser,
80
+ ) => Promise<AmplitudeCombinedClientWithExperiment>;
81
+ };
82
+
83
+ export function createDurableAmplitudeStoragePreset(
84
+ options: DurableAmplitudeStoragePresetOptions = {},
85
+ ): DurableAmplitudeStoragePreset {
86
+ const namespace = options.namespace ?? "default";
87
+ const { NitroAnalyticsStorage, NitroExperimentStorage } = getStorageModule();
88
+ const analyticsEvents = new NitroAnalyticsStorage<Event[]>(
89
+ `${namespace}:analytics-events`,
90
+ );
91
+ const analyticsSession = new NitroAnalyticsStorage<UserSession>(
92
+ `${namespace}:analytics-session`,
93
+ );
94
+ const experimentVariants = new NitroExperimentStorage(
95
+ `${namespace}:experiment-variants`,
96
+ );
97
+ return {
98
+ analytics: {
99
+ storageProvider: analyticsEvents,
100
+ cookieStorage: analyticsSession,
101
+ transportProvider: options.dryRun
102
+ ? dryRunTransport
103
+ : getAnalyticsTransport(),
104
+ },
105
+ experiment: {
106
+ storage: experimentVariants,
107
+ },
108
+ clear: async () => {
109
+ await analyticsEvents.reset();
110
+ await analyticsSession.reset();
111
+ await experimentVariants.reset?.();
112
+ },
113
+ };
114
+ }
115
+
116
+ export function createPersistentAmplitudeConfig(
117
+ namespaceOrOptions?: string | DurableAmplitudeStoragePresetOptions,
118
+ ): DurableAmplitudeStoragePreset {
119
+ if (typeof namespaceOrOptions === "string") {
120
+ return createDurableAmplitudeStoragePreset({
121
+ namespace: namespaceOrOptions,
122
+ });
123
+ }
124
+ return createDurableAmplitudeStoragePreset(namespaceOrOptions);
125
+ }
126
+
127
+ export function createExperimentUser(user: ExperimentUser): ExperimentUser {
128
+ return {
129
+ ...user,
130
+ user_properties: user.user_properties
131
+ ? { ...user.user_properties }
132
+ : undefined,
133
+ groups: user.groups ? { ...user.groups } : undefined,
134
+ group_properties: user.group_properties
135
+ ? { ...user.group_properties }
136
+ : undefined,
137
+ };
138
+ }
139
+
140
+ export function createAmplitudeClient(
141
+ config: AmplitudeCombinedClientConfigWithExperiment,
142
+ ): AmplitudeCombinedClientWithExperiment;
143
+ export function createAmplitudeClient(
144
+ config: AmplitudeCombinedClientConfig,
145
+ ): AmplitudeCombinedClient;
146
+ export function createAmplitudeClient(config: AmplitudeCombinedClientConfig) {
147
+ const instanceName = config.instanceName ?? "$default_instance";
148
+ const dryRunEnabled =
149
+ config.dryRun ||
150
+ (typeof config.durableStorage === "object" && config.durableStorage.dryRun);
151
+ const storagePreset =
152
+ config.durableStorage === false
153
+ ? undefined
154
+ : createDurableAmplitudeStoragePreset({
155
+ namespace:
156
+ typeof config.durableStorage === "object"
157
+ ? config.durableStorage.namespace
158
+ : instanceName,
159
+ dryRun: dryRunEnabled,
160
+ });
161
+ const analytics = createInstance();
162
+ const experiment =
163
+ config.experimentDeploymentKey !== undefined
164
+ ? Experiment.initializeWithAmplitudeAnalytics(
165
+ config.experimentDeploymentKey,
166
+ {
167
+ ...storagePreset?.experiment,
168
+ ...config.experiment,
169
+ instanceName,
170
+ httpClient: dryRunEnabled
171
+ ? (config.experiment?.httpClient ?? dryRunHttpClient)
172
+ : config.experiment?.httpClient,
173
+ },
174
+ )
175
+ : undefined;
176
+ const combined = {
177
+ analytics,
178
+ experiment,
179
+ init: async (user?: ExperimentUser) => {
180
+ await analytics.init(config.analyticsApiKey, config.userId, {
181
+ ...storagePreset?.analytics,
182
+ ...config.analytics,
183
+ instanceName,
184
+ transportProvider: dryRunEnabled
185
+ ? (config.analytics?.transportProvider ?? dryRunTransport)
186
+ : (config.analytics?.transportProvider ??
187
+ storagePreset?.analytics.transportProvider),
188
+ }).promise;
189
+ if (experiment && user) {
190
+ experiment.setUser(user);
191
+ }
192
+ return combined;
193
+ },
194
+ flush: async () => analytics.flush().promise,
195
+ reset: () => analytics.reset(),
196
+ getUserId: () => analytics.getUserId(),
197
+ getDeviceId: () => analytics.getDeviceId(),
198
+ } as AmplitudeCombinedClient;
199
+
200
+ return combined;
201
+ }
202
+
203
+ export function getConnectorIdentity(instanceName = "$default_instance"): {
204
+ userId?: string;
205
+ deviceId?: string;
206
+ } {
207
+ return getAnalyticsConnector(instanceName).identityStore.getIdentity();
208
+ }
package/src/testing.ts ADDED
@@ -0,0 +1,177 @@
1
+ import type { AmplitudeReturn, Result } from "@amplitude/analytics-core";
2
+ import type { AmplitudeReactNativeClient } from "./analytics/react-native-client";
3
+ import type { Client } from "./experiment/types/client";
4
+ import type { Storage } from "./experiment/types/storage";
5
+ import type { ExperimentUser } from "./experiment/types/user";
6
+ import type { Variant, Variants } from "./experiment/types/variant";
7
+
8
+ type MockFunction<Args extends unknown[], Return> = ((
9
+ ...args: Args
10
+ ) => Return) & {
11
+ calls: Args[];
12
+ };
13
+
14
+ function createMockFunction<Args extends unknown[], Return>(
15
+ implementation: (...args: Args) => Return,
16
+ ): MockFunction<Args, Return> {
17
+ const fn = ((...args: Args) => {
18
+ fn.calls.push(args);
19
+ return implementation(...args);
20
+ }) as MockFunction<Args, Return>;
21
+ fn.calls = [];
22
+ return fn;
23
+ }
24
+
25
+ function createReturn(eventType: string): AmplitudeReturn<Result> {
26
+ const result: Result = {
27
+ event: { event_type: eventType },
28
+ code: 200,
29
+ message: "dry_run",
30
+ };
31
+ return {
32
+ promise: Promise.resolve(result),
33
+ };
34
+ }
35
+
36
+ export type FakeExperimentStorage = Storage & {
37
+ values: Map<string, string>;
38
+ };
39
+
40
+ export function createFakeExperimentStorage(
41
+ initialValues: Record<string, string> = {},
42
+ ): FakeExperimentStorage {
43
+ const values = new Map(Object.entries(initialValues));
44
+ return {
45
+ values,
46
+ get: async (key) => values.get(key) ?? null,
47
+ put: async (key, value) => {
48
+ values.set(key, value);
49
+ },
50
+ delete: async (key) => {
51
+ values.delete(key);
52
+ },
53
+ reset: async () => {
54
+ values.clear();
55
+ },
56
+ };
57
+ }
58
+
59
+ export function createMockAmplitudeClient(): AmplitudeReactNativeClient {
60
+ let userId: string | undefined;
61
+ let deviceId = "mock-device-id";
62
+ let sessionId: number | undefined;
63
+ const client = {
64
+ init: createMockFunction((_: string, nextUserId?: string) => {
65
+ userId = nextUserId;
66
+ return createReturn("$init");
67
+ }),
68
+ track: createMockFunction(() => createReturn("$track")),
69
+ logEvent: createMockFunction(() => createReturn("$track")),
70
+ identify: createMockFunction(() => createReturn("$identify")),
71
+ groupIdentify: createMockFunction(() => createReturn("$groupidentify")),
72
+ setGroup: createMockFunction(() => createReturn("$groupidentify")),
73
+ revenue: createMockFunction(() => createReturn("revenue_amount")),
74
+ flush: createMockFunction(() => createReturn("$flush")),
75
+ flushWithResult: createMockFunction(async () => ({
76
+ ok: true,
77
+ sent: 0,
78
+ failed: 0,
79
+ dropped: 0,
80
+ retried: 0,
81
+ finishedAt: Date.now(),
82
+ })),
83
+ add: createMockFunction(() => createReturn("$add")),
84
+ remove: createMockFunction(() => createReturn("$remove")),
85
+ setUserId: createMockFunction((nextUserId: string | undefined) => {
86
+ userId = nextUserId;
87
+ }),
88
+ getUserId: createMockFunction(() => userId),
89
+ setDeviceId: createMockFunction((nextDeviceId: string) => {
90
+ deviceId = nextDeviceId;
91
+ }),
92
+ getDeviceId: createMockFunction(() => deviceId),
93
+ setSessionId: createMockFunction((nextSessionId: number) => {
94
+ sessionId = nextSessionId;
95
+ }),
96
+ getSessionId: createMockFunction(() => sessionId),
97
+ extendSession: createMockFunction(() => undefined),
98
+ setOptOut: createMockFunction(() => undefined),
99
+ reset: createMockFunction(() => {
100
+ userId = undefined;
101
+ deviceId = "mock-device-id-reset";
102
+ }),
103
+ shutdown: createMockFunction(() => undefined),
104
+ getDiagnostics: createMockFunction(() => ({
105
+ initialized: true,
106
+ userId,
107
+ deviceId,
108
+ sessionId,
109
+ queueSize: 0,
110
+ activeInstanceNames: ["$default_instance"],
111
+ })),
112
+ healthCheck: createMockFunction(async () => ({
113
+ ok: true,
114
+ analyticsInitialized: true,
115
+ nativeAvailable: true,
116
+ storageWritable: true,
117
+ errors: [],
118
+ })),
119
+ };
120
+ return client as unknown as AmplitudeReactNativeClient;
121
+ }
122
+
123
+ export function createMockExperimentClient(
124
+ initialVariants: Variants = {},
125
+ ): Client {
126
+ let user: ExperimentUser = {};
127
+ const variants = new Map<string, Variant>(Object.entries(initialVariants));
128
+ let client: Client;
129
+ client = {
130
+ start: createMockFunction(async (nextUser?: ExperimentUser) => {
131
+ user = nextUser ?? user;
132
+ }),
133
+ stop: createMockFunction(() => undefined),
134
+ fetch: createMockFunction(async () => client),
135
+ fetchWithMetadata: createMockFunction(async () => ({
136
+ fetched: true,
137
+ flagKeys: Array.from(variants.keys()),
138
+ cacheHit: false,
139
+ durationMillis: 0,
140
+ source: "network" as const,
141
+ })),
142
+ fetchOrThrow: createMockFunction(async () => client),
143
+ variant: createMockFunction((key: string, fallback?: string | Variant) => {
144
+ return variants.get(key) ?? normalizeFallback(fallback);
145
+ }),
146
+ variantWithMetadata: createMockFunction(
147
+ (key: string, fallback?: string | Variant) => ({
148
+ variant: variants.get(key) ?? normalizeFallback(fallback),
149
+ fallback: !variants.has(key),
150
+ stale: false,
151
+ reason: variants.has(key) ? undefined : "fallback",
152
+ }),
153
+ ),
154
+ all: createMockFunction(() => Object.fromEntries(variants)),
155
+ clear: createMockFunction(() => variants.clear()),
156
+ clearVariants: createMockFunction(() => variants.clear()),
157
+ hasCachedVariant: createMockFunction((key: string) => variants.has(key)),
158
+ getLastFetchTime: createMockFunction(() => undefined),
159
+ exposure: createMockFunction(() => undefined),
160
+ getUser: createMockFunction(() => ({ ...user })),
161
+ setUser: createMockFunction((nextUser: ExperimentUser) => {
162
+ user = { ...nextUser };
163
+ }),
164
+ getUserProvider: createMockFunction(() => ({
165
+ getUser: async () => ({ ...user }),
166
+ })),
167
+ setUserProvider: createMockFunction(() => client),
168
+ };
169
+ return client;
170
+
171
+ function normalizeFallback(fallback?: string | Variant): Variant {
172
+ if (typeof fallback === "string") {
173
+ return { value: fallback };
174
+ }
175
+ return fallback ?? {};
176
+ }
177
+ }