@configdirector/react-native-sdk 1.10.1 → 1.11.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.
@@ -1,4 +1,4 @@
1
- import { c as NativeJsonValue, d as ConfigDirectorContext, i as ConfigDirectorClient, l as NativeValue, p as ConfigDirectorLogger, u as NativeValues } from "./types-DmbDeZqL.mjs";
1
+ import { S as TrackedEventDetails, b as TrackedEvent, c as NativeJsonValue, d as ConfigDirectorContext, i as ConfigDirectorClient, l as NativeValue, p as ConfigDirectorLogger, u as NativeValues, x as TrackedEventDetailValue } from "./types-BCVhD8m2.mjs";
2
2
 
3
3
  //#region ../shared/src/errors.d.ts
4
4
  declare class ConfigDirectorConnectionError extends Error {
@@ -28,7 +28,9 @@ type TestClientOptions = {
28
28
  */
29
29
  timeout?: number;
30
30
  /**
31
- * The logger the SDK client uses. Defaults to the SDK's console logger at `warn` level.
31
+ * The logger the SDK client uses. Defaults to the SDK's console logger at `warn` level. Since the
32
+ * test client sends no telemetry, every tracked event that is not recorded as it was tracked logs
33
+ * a warning, rather than each cause once per telemetry batch.
32
34
  */
33
35
  logger?: ConfigDirectorLogger;
34
36
  };
@@ -97,6 +99,13 @@ interface TestClient {
97
99
  * the client connects without it. Resumes are not recorded.
98
100
  */
99
101
  readonly contextUpdates: ConfigDirectorContext[];
102
+ /**
103
+ * Every event the code under test tracked through the client, in call order, with the context
104
+ * the client had at that moment, as the SDK would report it: an event whose name does not have
105
+ * 1 to 150 characters is not recorded, and a value or details the SDK would leave out are missing
106
+ * from the record. The limit of 1,000 events per telemetry batch does not apply.
107
+ */
108
+ readonly trackedEvents: TrackedEvent[];
100
109
  }
101
110
  //#endregion
102
111
  //#region ../js-client-core/src/testing/installTestClient.d.ts
@@ -146,4 +155,4 @@ declare const installTestClient: (testClient: TestClient) => (() => void);
146
155
  */
147
156
  declare const createTestClient: (options?: TestClientOptions) => TestClient;
148
157
  //#endregion
149
- export { ConfigDirectorConnectionError, ConfigDirectorValidationError, type TestClient, type TestClientOptions, type NativeJsonValue as TestJsonValue, type NativeValue as TestValue, type NativeValues as TestValues, createTestClient, installTestClient };
158
+ export { ConfigDirectorConnectionError, ConfigDirectorValidationError, type TestClient, type TestClientOptions, type NativeJsonValue as TestJsonValue, type NativeValue as TestValue, type NativeValues as TestValues, type TrackedEvent, type TrackedEventDetailValue, type TrackedEventDetails, createTestClient, installTestClient };
@@ -1,4 +1,27 @@
1
- import { c as createDefaultLogger, d as ConfigDirectorConnectionError, f as ConfigDirectorValidationError, l as nativeValueConfigState, m as _defineProperty, p as Emitter, r as DefaultConfigDirectorClient, s as discardingTelemetryClient, t as INSTALLED_TEST_CLIENT_SLOT } from "./installedTestClient-CM98r0qu.mjs";
1
+ import { _ as Emitter, c as trackedEventWarningMessages, d as discardingTelemetryClient, f as createDefaultLogger, g as ConfigDirectorValidationError, h as ConfigDirectorConnectionError, p as nativeValueConfigState, r as DefaultConfigDirectorClient, t as INSTALLED_TEST_CLIENT_SLOT, v as _defineProperty } from "./installedTestClient-BopKe_F5.mjs";
2
+ //#region ../shared/src/telemetry/RecordedTrackedEvents.ts
3
+ const recordedEventOf = (entry, context) => ({
4
+ name: entry.name,
5
+ ...context === void 0 ? {} : { context },
6
+ ...entry.value === void 0 ? {} : { value: entry.value },
7
+ ...entry.details === void 0 ? {} : { details: entry.details }
8
+ });
9
+ var RecordedTrackedEvents = class {
10
+ constructor(logger) {
11
+ this.logger = logger;
12
+ _defineProperty(this, "events", []);
13
+ }
14
+ record(admission, context) {
15
+ admission.warnings.forEach((warning) => this.logger.warn(trackedEventWarningMessages[warning]));
16
+ switch (admission.outcome) {
17
+ case "dropped": return;
18
+ case "admitted":
19
+ this.events.push(recordedEventOf(admission.entry, context));
20
+ return;
21
+ }
22
+ }
23
+ };
24
+ //#endregion
2
25
  //#region ../js-client-core/src/testing/InMemoryConnection.ts
3
26
  const TEST_ENVIRONMENT = {
4
27
  environmentId: "test-environment",
@@ -195,9 +218,10 @@ var InMemoryConnection = class {
195
218
  //#region ../js-client-core/src/testing/TestClient.ts
196
219
  const TEST_CLIENT_SDK_KEY = "test-client";
197
220
  var InMemoryTestClient = class {
198
- constructor(client, connection) {
221
+ constructor(client, connection, trackedEvents) {
199
222
  this.client = client;
200
223
  this.connection = connection;
224
+ this.trackedEvents = trackedEvents;
201
225
  }
202
226
  setValue(key, value) {
203
227
  this.connection.setValue(key, value);
@@ -234,10 +258,14 @@ const createTestClient$1 = (sdkOptions, options) => {
234
258
  var _options$logger, _options$values;
235
259
  const logger = (_options$logger = options === null || options === void 0 ? void 0 : options.logger) !== null && _options$logger !== void 0 ? _options$logger : createDefaultLogger();
236
260
  const connection = new InMemoryConnection(logger, (_options$values = options === null || options === void 0 ? void 0 : options.values) !== null && _options$values !== void 0 ? _options$values : {});
237
- return new InMemoryTestClient(new DefaultConfigDirectorClient(() => discardingTelemetryClient, TEST_CLIENT_SDK_KEY, sdkOptions, {
261
+ const recordedTrackedEvents = new RecordedTrackedEvents(logger);
262
+ return new InMemoryTestClient(new DefaultConfigDirectorClient(() => ({
263
+ ...discardingTelemetryClient,
264
+ trackedEvent: (admission, context) => recordedTrackedEvents.record(admission, context)
265
+ }), TEST_CLIENT_SDK_KEY, sdkOptions, {
238
266
  logger,
239
267
  connection: { timeout: options === null || options === void 0 ? void 0 : options.timeout }
240
- }, { transport: connection }), connection);
268
+ }, { transport: connection }), connection, recordedTrackedEvents.events);
241
269
  };
242
270
  //#endregion
243
271
  //#region ../js-client-core/src/testing/installTestClient.ts
@@ -295,7 +323,7 @@ const installTestClient = (testClient) => {
295
323
  const createTestClient = (options) => {
296
324
  return createTestClient$1({
297
325
  sdkName: "react-native-sdk",
298
- sdkVersion: "1.10.1"
326
+ sdkVersion: "1.11.0"
299
327
  }, options);
300
328
  };
301
329
  //#endregion
@@ -1,4 +1,4 @@
1
- import { _ as ConnectionMode, a as ConfigDirectorClientOptions, d as ConfigDirectorContext, f as ConfigDirectorLogMessageDecorator, g as ConfigValueType, h as ConfigEvaluation, i as ConfigDirectorClient, m as ConfigDirectorLoggingLevel, n as CachingProvider, o as OfflineValue, p as ConfigDirectorLogger, r as ClientHooks, s as OfflineValues, t as CachingOptions, v as EvaluationReason } from "./types-DmbDeZqL.mjs";
1
+ import { C as Variation, S as TrackedEventDetails, _ as ConnectionMode, a as ConfigDirectorClientOptions, d as ConfigDirectorContext, f as ConfigDirectorLogMessageDecorator, g as ConfigValueType, h as ConfigEvaluation, i as ConfigDirectorClient, m as ConfigDirectorLoggingLevel, n as CachingProvider, o as OfflineValue, p as ConfigDirectorLogger, r as ClientHooks, s as OfflineValues, t as CachingOptions, v as EvaluationReason, x as TrackedEventDetailValue, y as TrackOptions } from "./types-BCVhD8m2.mjs";
2
2
  import { Component, PropsWithChildren } from "react";
3
3
  import * as _$react_jsx_runtime0 from "react/jsx-runtime";
4
4
 
@@ -151,4 +151,4 @@ declare class ConfigDirectorReactContextError extends Error {
151
151
  //#region src/logger.d.ts
152
152
  declare const createConsoleLogger: (level?: ConfigDirectorLoggingLevel) => DefaultConsoleLogger;
153
153
  //#endregion
154
- export { type CachingOptions, type CachingProvider, type ClientStatus, type ConfigDirectorClient, type ConfigDirectorClientOptions, type ConfigDirectorContext, type ConfigDirectorLogger, type ConfigDirectorLoggingLevel, ConfigDirectorProvider, type ConfigDirectorProviderClientProps, type ConfigDirectorProviderOptions, type ConfigDirectorProviderProps, ConfigDirectorReactContextError, type ConfigEvaluation, type ConnectionMode, type EvaluationReason, type NetInfoSubscribe, type OfflineValue, type OfflineValues, createClient, createConsoleLogger, useClient, useConfigValue, useContext };
154
+ export { type CachingOptions, type CachingProvider, type ClientStatus, type ConfigDirectorClient, type ConfigDirectorClientOptions, type ConfigDirectorContext, type ConfigDirectorLogger, type ConfigDirectorLoggingLevel, ConfigDirectorProvider, type ConfigDirectorProviderClientProps, type ConfigDirectorProviderOptions, type ConfigDirectorProviderProps, ConfigDirectorReactContextError, type ConfigEvaluation, type ConnectionMode, type EvaluationReason, type NetInfoSubscribe, type OfflineValue, type OfflineValues, type TrackOptions, type TrackedEventDetailValue, type TrackedEventDetails, type Variation, createClient, createConsoleLogger, useClient, useConfigValue, useContext };
@@ -1,4 +1,4 @@
1
- import { a as DEFAULT_CACHING_SIZE_LIMIT_BYTES, c as createDefaultLogger, i as CACHED_VALUES_STORAGE_KEY_PREFIX, m as _defineProperty, n as readInstalledTestClient, o as fetchWithTimeout, r as DefaultConfigDirectorClient, u as isFetchErrorFatal } from "./installedTestClient-CM98r0qu.mjs";
1
+ import { a as DEFAULT_CACHING_SIZE_LIMIT_BYTES, f as createDefaultLogger, i as CACHED_VALUES_STORAGE_KEY_PREFIX, l as utf8ByteLength, m as isFetchErrorFatal, n as readInstalledTestClient, r as DefaultConfigDirectorClient, s as logBatchWarnings, u as fetchWithTimeout, v as _defineProperty } from "./installedTestClient-BopKe_F5.mjs";
2
2
  import React, { Component, useContext as useContext$1 } from "react";
3
3
  import { AppState, NativeModules } from "react-native";
4
4
  import { sha256 } from "@noble/hashes/sha2.js";
@@ -58,6 +58,7 @@ const isDroppedEventsEmpty = (droppedEvents) => {
58
58
  };
59
59
  //#endregion
60
60
  //#region ../js-client-core/src/telemetry/ClientEventReporter.ts
61
+ const LARGEST_BODY_SENT_WITH_KEEPALIVE_BYTES = 64 * 1024 / 2;
61
62
  var ClientEventReporter = class {
62
63
  constructor(options) {
63
64
  _defineProperty(this, "sdkKey", void 0);
@@ -107,10 +108,11 @@ var ClientEventReporter = class {
107
108
  }
108
109
  async sendReport(eventReport) {
109
110
  try {
111
+ const body = JSON.stringify(eventReport);
110
112
  const response = await fetchWithTimeout(5e3, this.url, {
111
113
  method: "POST",
112
- body: JSON.stringify(eventReport),
113
- keepalive: true
114
+ body,
115
+ keepalive: utf8ByteLength(body) <= LARGEST_BODY_SENT_WITH_KEEPALIVE_BYTES
114
116
  }, this.logger);
115
117
  const isFatalError = this.isStatusFatal(response.status);
116
118
  if (isFatalError) this.logger.warn(`[EventReporter] Received a fatal status response (${response.status}) from the telemetry endpoint. No more telemetry data will be sent.`);
@@ -209,6 +211,43 @@ var EventQueue = class {
209
211
  }
210
212
  };
211
213
  //#endregion
214
+ //#region ../shared/src/telemetry/TrackedEventQueue.ts
215
+ var TrackedEventQueue = class {
216
+ constructor() {
217
+ _defineProperty(this, "entries", []);
218
+ _defineProperty(this, "warningsRaisedThisBatch", /* @__PURE__ */ new Set());
219
+ }
220
+ add(admission) {
221
+ return this.warningsOfKeeping(admission).filter((warning) => this.isFirstThisBatch(warning));
222
+ }
223
+ absorb(entries) {
224
+ this.entries.push(...entries);
225
+ }
226
+ takeBatch() {
227
+ const batch = this.entries;
228
+ this.clear();
229
+ return batch;
230
+ }
231
+ clear() {
232
+ this.entries = [];
233
+ this.warningsRaisedThisBatch.clear();
234
+ }
235
+ warningsOfKeeping(admission) {
236
+ switch (admission.outcome) {
237
+ case "dropped": return admission.warnings;
238
+ case "admitted":
239
+ if (this.entries.length >= 1e3) return ["batch-already-full"];
240
+ this.entries.push(admission.entry);
241
+ return admission.warnings;
242
+ }
243
+ }
244
+ isFirstThisBatch(warning) {
245
+ if (this.warningsRaisedThisBatch.has(warning)) return false;
246
+ this.warningsRaisedThisBatch.add(warning);
247
+ return true;
248
+ }
249
+ };
250
+ //#endregion
212
251
  //#region ../shared/src/telemetry/TelemetryEventCollector.ts
213
252
  var TelemetryEventCollector = class {
214
253
  constructor(options) {
@@ -216,6 +255,7 @@ var TelemetryEventCollector = class {
216
255
  _defineProperty(this, "logger", void 0);
217
256
  _defineProperty(this, "evaluationEventQueue", void 0);
218
257
  _defineProperty(this, "aggregator", new EventAggregator());
258
+ _defineProperty(this, "trackedEventQueue", new TrackedEventQueue());
219
259
  _defineProperty(this, "flushIntervalDelay", void 0);
220
260
  _defineProperty(this, "flushTimeout", void 0);
221
261
  _defineProperty(this, "collectEvents", true);
@@ -231,6 +271,10 @@ var TelemetryEventCollector = class {
231
271
  this.started = true;
232
272
  this.flushTimeout = setTimeout(() => this.flushAndScheduleNext(), this.initialFlushIntervalDelay);
233
273
  }
274
+ trackedEvent(admission) {
275
+ if (!this.collectEvents) return;
276
+ logBatchWarnings(this.logger, this.trackedEventQueue.add(admission));
277
+ }
234
278
  async flushAndScheduleNext() {
235
279
  if (!this.collectEvents) return;
236
280
  let response;
@@ -264,6 +308,7 @@ var TelemetryEventCollector = class {
264
308
  this.logger.warn("[TelemetryEventCollector] Error flushing telemetry events during close: ", error);
265
309
  }
266
310
  this.evaluationEventQueue.clear();
311
+ this.trackedEventQueue.clear();
267
312
  }
268
313
  dispose() {
269
314
  this.close();
@@ -375,6 +420,9 @@ var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
375
420
  absorbInvalidContextSubmissions(entries) {
376
421
  this.invalidContextSubmissions.absorb(entries);
377
422
  }
423
+ absorbTrackedEventEntries(entries) {
424
+ this.trackedEventQueue.absorb(entries);
425
+ }
378
426
  async forceFlush() {
379
427
  this.cancelScheduledFlush();
380
428
  await this.flushAndScheduleNext();
@@ -388,7 +436,7 @@ var ClientTelemetryEventCollector = class extends TelemetryEventCollector {
388
436
  const evaluationSnapshot = await this.evaluationEventSnapshotPreprocessor.process(this.evaluationEventQueue.takeSnapshot());
389
437
  return await ((_this$reporter = this.reporter) === null || _this$reporter === void 0 ? void 0 : _this$reporter.report({
390
438
  context: this._context,
391
- discreteEvents: {},
439
+ discreteEvents: { trackedEvents: this.trackedEventQueue.takeBatch() },
392
440
  aggregatedEvents: {
393
441
  evaluatedConfig: this.aggregator.aggregate(evaluationSnapshot),
394
442
  invalidContext: this.invalidContextSubmissions.takeSnapshot()
@@ -734,6 +782,9 @@ var ReactNativeTelemetryClient = class {
734
782
  recordInvalidContext(kindSample) {
735
783
  this.collector.recordInvalidContext(kindSample);
736
784
  }
785
+ trackedEvent(admission) {
786
+ this.collector.trackedEvent(admission);
787
+ }
737
788
  async close() {
738
789
  await this.collector.close();
739
790
  }
@@ -777,7 +828,7 @@ const createReactNativeClient = (clientSdkKey, clientOptions) => {
777
828
  var _clientOptions$logger;
778
829
  const sdkIdentity = {
779
830
  sdkName: "react-native-sdk",
780
- sdkVersion: "1.10.1"
831
+ sdkVersion: "1.11.0"
781
832
  };
782
833
  const logger = (_clientOptions$logger = clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.logger) !== null && _clientOptions$logger !== void 0 ? _clientOptions$logger : createConsoleLogger("warn");
783
834
  const metadata = resolveMetadata(clientOptions === null || clientOptions === void 0 ? void 0 : clientOptions.metadata, logger);
@@ -724,6 +724,27 @@ const offlineSourceOf = (connection) => {
724
724
  };
725
725
  //#endregion
726
726
  //#region ../shared/src/value-parser.ts
727
+ const NO_VARIATION_OR_ROLLOUT_SHARE = {
728
+ variation: void 0,
729
+ ruleId: void 0,
730
+ shareId: void 0
731
+ };
732
+ const variationOf = (configState) => {
733
+ const variation = configState.variation;
734
+ if (variation == null) return;
735
+ return variation.name ? {
736
+ id: variation.id,
737
+ name: variation.name
738
+ } : { id: variation.id };
739
+ };
740
+ const variationAndRolloutShareOf = (configState) => {
741
+ var _configState$ruleId, _configState$shareId;
742
+ return {
743
+ variation: variationOf(configState),
744
+ ruleId: (_configState$ruleId = configState.ruleId) !== null && _configState$ruleId !== void 0 ? _configState$ruleId : void 0,
745
+ shareId: (_configState$shareId = configState.shareId) !== null && _configState$shareId !== void 0 ? _configState$shareId : void 0
746
+ };
747
+ };
727
748
  const getRequestedType = (defaultValue) => {
728
749
  const baseType = typeof defaultValue;
729
750
  if (baseType === "object") try {
@@ -749,6 +770,13 @@ const configTypesNotReadableAsString = new Set([
749
770
  "float"
750
771
  ]);
751
772
  const parseConfigValue = (configState, defaultValue) => {
773
+ const valueAndReason = parseValueAndReason(configState, defaultValue);
774
+ return {
775
+ ...valueAndReason,
776
+ ...valueAndReason.usedDefault ? NO_VARIATION_OR_ROLLOUT_SHARE : variationAndRolloutShareOf(configState)
777
+ };
778
+ };
779
+ const parseValueAndReason = (configState, defaultValue) => {
752
780
  const value = configState.value;
753
781
  const requestedType = getRequestedType(defaultValue);
754
782
  if (value == null || value === "") return {
@@ -954,6 +982,7 @@ const discardingTelemetryClient = {
954
982
  updateContext: async () => {},
955
983
  evaluatedConfig: () => {},
956
984
  recordInvalidContext: () => {},
985
+ trackedEvent: () => {},
957
986
  close: async () => {}
958
987
  };
959
988
  //#endregion
@@ -1338,6 +1367,93 @@ const utf8ByteLength = (text) => {
1338
1367
  return bytes;
1339
1368
  };
1340
1369
  //#endregion
1370
+ //#region ../shared/src/telemetry/reported-context.ts
1371
+ const USER_CONTEXT_KIND = "user";
1372
+ const reportedContextOf = (context) => {
1373
+ var _context$kind;
1374
+ const contextIdentifier = (context === null || context === void 0 ? void 0 : context.anonymous) ? void 0 : context === null || context === void 0 ? void 0 : context.id;
1375
+ if (contextIdentifier === void 0) return {};
1376
+ return {
1377
+ contextKind: (_context$kind = context === null || context === void 0 ? void 0 : context.kind) !== null && _context$kind !== void 0 ? _context$kind : USER_CONTEXT_KIND,
1378
+ contextIdentifier
1379
+ };
1380
+ };
1381
+ //#endregion
1382
+ //#region ../shared/src/telemetry/tracked-events.ts
1383
+ const TRACKED_EVENTS_PER_BATCH = 1e3;
1384
+ const NAME_MAXIMUM_CHARACTERS = 150;
1385
+ const NAME_MAXIMUM_UTF16_UNITS = NAME_MAXIMUM_CHARACTERS * 2;
1386
+ const DETAILS_MAXIMUM_TOP_LEVEL_KEYS = 20;
1387
+ const DETAILS_MAXIMUM_JSON_BYTES = 1024;
1388
+ const trackedEventWarningMessages = {
1389
+ "name-outside-the-character-limit": `[ConfigDirectorClient] A tracked event was not reported because its name does not have 1 to ${NAME_MAXIMUM_CHARACTERS} characters.`,
1390
+ "value-not-a-finite-number": "[ConfigDirectorClient] A tracked event was reported without its value because the value is not a finite number.",
1391
+ "details-not-reportable": `[ConfigDirectorClient] A tracked event was reported without its details because they are not an object, have more than ${DETAILS_MAXIMUM_TOP_LEVEL_KEYS} top-level keys, are larger than ${DETAILS_MAXIMUM_JSON_BYTES} bytes as JSON, or cannot be written as JSON.`,
1392
+ "batch-already-full": `[ConfigDirectorClient] A tracked event was not reported because ${TRACKED_EVENTS_PER_BATCH} tracked events are already waiting for the next telemetry batch.`
1393
+ };
1394
+ const logBatchWarnings = (logger, warnings) => {
1395
+ for (const warning of warnings) logger.warn(`${trackedEventWarningMessages[warning]} Further tracked events like it are not logged until the next telemetry batch is sent.`);
1396
+ };
1397
+ const isGiven = (option) => option !== void 0 && option !== null;
1398
+ const isNonArrayObject = (candidate) => typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
1399
+ const isWithinTheCharacterLimit = (name) => typeof name === "string" && name.length >= 1 && name.length <= NAME_MAXIMUM_UTF16_UNITS && Array.from(name).length <= NAME_MAXIMUM_CHARACTERS;
1400
+ const reportableValueOf = (value) => {
1401
+ if (!isGiven(value)) return { status: "not-given" };
1402
+ if (typeof value === "number" && Number.isFinite(value)) return {
1403
+ status: "reportable",
1404
+ fields: { value }
1405
+ };
1406
+ return {
1407
+ status: "left-out",
1408
+ warning: "value-not-a-finite-number"
1409
+ };
1410
+ };
1411
+ const jsonCopyWithinTheLimits = (details) => {
1412
+ if (!isNonArrayObject(details)) return;
1413
+ try {
1414
+ const json = JSON.stringify(details);
1415
+ if (typeof json !== "string" || utf8ByteLength(json) > DETAILS_MAXIMUM_JSON_BYTES) return;
1416
+ const copy = JSON.parse(json);
1417
+ return isNonArrayObject(copy) && Object.keys(copy).length <= DETAILS_MAXIMUM_TOP_LEVEL_KEYS ? copy : void 0;
1418
+ } catch {
1419
+ return;
1420
+ }
1421
+ };
1422
+ const reportableDetailsOf = (details) => {
1423
+ if (!isGiven(details)) return { status: "not-given" };
1424
+ const jsonCopy = jsonCopyWithinTheLimits(details);
1425
+ if (jsonCopy === void 0) return {
1426
+ status: "left-out",
1427
+ warning: "details-not-reportable"
1428
+ };
1429
+ return {
1430
+ status: "reportable",
1431
+ fields: { details: jsonCopy }
1432
+ };
1433
+ };
1434
+ const fieldsOf = (option) => option.status === "reportable" ? option.fields : void 0;
1435
+ const warningsOf = (...options) => options.flatMap((option) => option.status === "left-out" ? [option.warning] : []);
1436
+ const admitTrackedEvent = (call) => {
1437
+ var _call$options, _call$options2;
1438
+ if (!isWithinTheCharacterLimit(call.name)) return {
1439
+ outcome: "dropped",
1440
+ warnings: ["name-outside-the-character-limit"]
1441
+ };
1442
+ const value = reportableValueOf((_call$options = call.options) === null || _call$options === void 0 ? void 0 : _call$options.value);
1443
+ const details = reportableDetailsOf((_call$options2 = call.options) === null || _call$options2 === void 0 ? void 0 : _call$options2.details);
1444
+ return {
1445
+ outcome: "admitted",
1446
+ entry: {
1447
+ name: call.name,
1448
+ ...reportedContextOf(call.context),
1449
+ ...fieldsOf(value),
1450
+ ...fieldsOf(details),
1451
+ happenedAt: call.happenedAt.toISOString()
1452
+ },
1453
+ warnings: warningsOf(value, details)
1454
+ };
1455
+ };
1456
+ //#endregion
1341
1457
  //#region ../js-client-core/src/cached-values.ts
1342
1458
  const ENTRY_FORMAT_VERSION = 2;
1343
1459
  const FIRST_ENTRY_FORMAT_VERSION = 1;
@@ -1836,7 +1952,7 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1836
1952
  return this.evaluationOf(configKey, configState, defaultValue);
1837
1953
  }
1838
1954
  evaluationOf(configKey, configState, defaultValue) {
1839
- var _this$currentContext2, _parseResult$parsedVa;
1955
+ var _this$currentContext2, _parseResult$variatio, _parseResult$parsedVa;
1840
1956
  if (!configState) {
1841
1957
  var _this$currentContext;
1842
1958
  this.logger.debug(`[ConfigDirectorClient] No config state found for '${configKey}', returning default value '${defaultValue}'`);
@@ -1871,12 +1987,16 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1871
1987
  evaluatedValue: parseResult.parsedValue,
1872
1988
  evaluatedValueId: parseResult.parsedValueId,
1873
1989
  usedDefault: parseResult.usedDefault,
1874
- evaluationReason: reason
1990
+ evaluationReason: reason,
1991
+ ruleId: parseResult.ruleId,
1992
+ shareId: parseResult.shareId,
1993
+ variationId: (_parseResult$variatio = parseResult.variation) === null || _parseResult$variatio === void 0 ? void 0 : _parseResult$variatio.id
1875
1994
  });
1876
1995
  const evaluation = {
1877
1996
  key: configKey,
1878
1997
  value: parseResult.parsedValue,
1879
1998
  valueId: (_parseResult$parsedVa = parseResult.parsedValueId) !== null && _parseResult$parsedVa !== void 0 ? _parseResult$parsedVa : void 0,
1999
+ variation: parseResult.variation,
1880
2000
  isDefaultValue: parseResult.usedDefault,
1881
2001
  reason,
1882
2002
  context: this.context
@@ -1919,6 +2039,19 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1919
2039
  unwatchAll() {
1920
2040
  this.handlersMap.clear();
1921
2041
  }
2042
+ track(eventName, options) {
2043
+ try {
2044
+ const context = this.currentContext;
2045
+ this.telemetryClient.trackedEvent(admitTrackedEvent({
2046
+ name: eventName,
2047
+ context,
2048
+ options,
2049
+ happenedAt: /* @__PURE__ */ new Date()
2050
+ }), context);
2051
+ } catch (error) {
2052
+ this.logger.warn("[ConfigDirectorClient] A tracked event was not reported because it could not be read: ", error);
2053
+ }
2054
+ }
1922
2055
  pauseNetwork() {
1923
2056
  switch (this.connectionPlan.kind) {
1924
2057
  case "stay-offline": return;
@@ -1959,4 +2092,4 @@ var DefaultConfigDirectorClient = class DefaultConfigDirectorClient {
1959
2092
  const INSTALLED_TEST_CLIENT_SLOT = Symbol.for("@configdirector/installed-test-client");
1960
2093
  const readInstalledTestClient = () => globalThis[INSTALLED_TEST_CLIENT_SLOT];
1961
2094
  //#endregion
1962
- export { DEFAULT_CACHING_SIZE_LIMIT_BYTES as a, createDefaultLogger as c, ConfigDirectorConnectionError as d, ConfigDirectorValidationError as f, CACHED_VALUES_STORAGE_KEY_PREFIX as i, nativeValueConfigState as l, _defineProperty as m, readInstalledTestClient as n, fetchWithTimeout as o, Emitter as p, DefaultConfigDirectorClient as r, discardingTelemetryClient as s, INSTALLED_TEST_CLIENT_SLOT as t, isFetchErrorFatal as u };
2095
+ export { Emitter as _, DEFAULT_CACHING_SIZE_LIMIT_BYTES as a, trackedEventWarningMessages as c, discardingTelemetryClient as d, createDefaultLogger as f, ConfigDirectorValidationError as g, ConfigDirectorConnectionError as h, CACHED_VALUES_STORAGE_KEY_PREFIX as i, utf8ByteLength as l, isFetchErrorFatal as m, readInstalledTestClient as n, TRACKED_EVENTS_PER_BATCH as o, nativeValueConfigState as p, DefaultConfigDirectorClient as r, logBatchWarnings as s, INSTALLED_TEST_CLIENT_SLOT as t, fetchWithTimeout as u, _defineProperty as v };
@@ -25,24 +25,72 @@ interface ConfigDirectorLogMessageDecorator {
25
25
  }
26
26
  /**
27
27
  * Why an evaluation returned the value it did. `found-match` returns a value from ConfigDirector,
28
- * and `cached` a value kept on the device while a client SDK waits for live values. `value-missing`
28
+ * and `cached` a value kept on the device while a client SDK waits for live values.
29
+ * `rollout-skipped-no-identifier` returns the default targeting rule value: a server SDK skipped a
30
+ * percentage rollout because the context has no `id` to place it in a share. `value-missing`
29
31
  * returns the in-code default value because the config has no value. Every other reason returns the
30
32
  * in-code default value because of an error: the config does not exist (`config-state-missing`), no
31
33
  * values have arrived yet (`client-not-ready`), or the value cannot be read as the type of the in-code
32
34
  * default value.
33
35
  */
34
- type EvaluationReason = "found-match" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean" | "cached";
36
+ type EvaluationReason = "found-match" | "rollout-skipped-no-identifier" | "config-state-missing" | "client-not-ready" | "type-mismatch" | "value-missing" | "invalid-number" | "invalid-json" | "invalid-boolean" | "cached";
37
+ /**
38
+ * One of the values a config can serve, such as the control or a treatment of an experiment.
39
+ */
40
+ type Variation = {
41
+ /** The ID ConfigDirector assigned to the variation, which stays the same when it is renamed. */id: string; /** The variation's name, when it has one. */
42
+ name?: string;
43
+ };
35
44
  /**
36
45
  * One evaluation of a config: the value it returned, why, and the context it was evaluated for.
37
46
  */
38
47
  type ConfigEvaluation<T extends ConfigValueType = ConfigValueType> = {
39
48
  /** The config key that was evaluated. */key: string; /** The value returned, which is the in-code default value when {@link isDefaultValue} is `true`. */
40
49
  value: T; /** The ID ConfigDirector assigned to the value returned, when ConfigDirector sent one with it. */
41
- valueId?: string; /** Whether the evaluation returned the in-code default value. */
50
+ valueId?: string;
51
+ /**
52
+ * The variation that was served. It is `undefined` when the config has no variations or the
53
+ * in-code default value is returned.
54
+ */
55
+ variation?: Variation; /** Whether the evaluation returned the in-code default value. */
42
56
  isDefaultValue: boolean; /** Why the evaluation returned this value. */
43
57
  reason: EvaluationReason; /** The context the config was evaluated for, if any. */
44
58
  context?: ConfigDirectorContext;
45
59
  };
60
+ /**
61
+ * A value in the details of a tracked event: a string, a number, a boolean, or an array or object
62
+ * of such values.
63
+ */
64
+ type TrackedEventDetailValue = string | number | boolean | TrackedEventDetailValue[] | {
65
+ [key: string]: TrackedEventDetailValue;
66
+ };
67
+ /**
68
+ * Details of a tracked event, such as the plan a customer chose. Details with more than 20
69
+ * top-level keys, or larger than 1 KB as JSON, are left out and the event is still reported.
70
+ */
71
+ type TrackedEventDetails = {
72
+ [key: string]: TrackedEventDetailValue;
73
+ };
74
+ /**
75
+ * What a tracked event carries besides its name.
76
+ */
77
+ type TrackOptions = {
78
+ /**
79
+ * A number the event measures, such as an amount of money, a count or a duration. A value that
80
+ * is not a finite number is left out and the event is still reported.
81
+ */
82
+ value?: number; /** Details of the event. */
83
+ details?: TrackedEventDetails;
84
+ };
85
+ /**
86
+ * An event the code under test tracked, as a test client recorded it.
87
+ */
88
+ type TrackedEvent = {
89
+ /** The name the event was tracked with. */name: string; /** The context the event was tracked for, if any. */
90
+ context?: ConfigDirectorContext; /** The value the event was tracked with, when it is a finite number. */
91
+ value?: number; /** The details the event was tracked with, when they are within the limits. */
92
+ details?: TrackedEventDetails;
93
+ };
46
94
  /**
47
95
  * The context to be sent to ConfigDirector: the user or other entity, such as a device or an
48
96
  * organization, that configs are evaluated for. This context will be used for targeting rules
@@ -363,8 +411,8 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
363
411
  getValue<T extends ConfigValueType>(configKey: string, defaultValue: T): T;
364
412
  /**
365
413
  * Evaluates a config as {@link getValue} does and returns the evaluation: the value, the reason it
366
- * was served and its value ID. Like every read, it fires `configEvaluated` and is counted in
367
- * telemetry.
414
+ * was served, its value ID and its variation. Like every read, it fires `configEvaluated` and is
415
+ * counted in telemetry.
368
416
  *
369
417
  * @returns The evaluation, whose `value` is what {@link getValue} returns
370
418
  * @param configKey The config key to evaluate
@@ -396,6 +444,20 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
396
444
  * Removes all subscribers from all config keys
397
445
  */
398
446
  unwatchAll(): void;
447
+ /**
448
+ * Reports to ConfigDirector that something happened for the current context, such as a completed
449
+ * checkout, so it can be counted. The event is sent with the next telemetry batch. `track` never
450
+ * throws, and does nothing in `offline` mode.
451
+ *
452
+ * An event counts for an experiment only when the current context has an `id`, the one the
453
+ * experiment's config was evaluated for. Before the client has a context, or for a context
454
+ * without an `id`, the event is still reported.
455
+ *
456
+ * @param eventName The name of the event, 1 to 150 characters. An event with any other name is
457
+ * not reported and a warning is logged.
458
+ * @param options The event's {@link TrackOptions.value} and {@link TrackOptions.details} (optional)
459
+ */
460
+ track(eventName: string, options?: TrackOptions): void;
399
461
  /**
400
462
  * Pauses the network connection without clearing event handlers, watch handlers, or config
401
463
  * state. Intended for mobile environments where the OS may kill background connections (e.g.
@@ -418,4 +480,4 @@ interface ConfigDirectorClient extends EventProvider<ClientEvents> {
418
480
  dispose(): void;
419
481
  }
420
482
  //#endregion
421
- export { ConnectionMode as _, ConfigDirectorClientOptions as a, NativeJsonValue as c, ConfigDirectorContext as d, ConfigDirectorLogMessageDecorator as f, ConfigValueType as g, ConfigEvaluation as h, ConfigDirectorClient as i, NativeValue as l, ConfigDirectorLoggingLevel as m, CachingProvider as n, OfflineValue as o, ConfigDirectorLogger as p, ClientHooks as r, OfflineValues as s, CachingOptions as t, NativeValues as u, EvaluationReason as v };
483
+ export { Variation as C, TrackedEventDetails as S, ConnectionMode as _, ConfigDirectorClientOptions as a, TrackedEvent as b, NativeJsonValue as c, ConfigDirectorContext as d, ConfigDirectorLogMessageDecorator as f, ConfigValueType as g, ConfigEvaluation as h, ConfigDirectorClient as i, NativeValue as l, ConfigDirectorLoggingLevel as m, CachingProvider as n, OfflineValue as o, ConfigDirectorLogger as p, ClientHooks as r, OfflineValues as s, CachingOptions as t, NativeValues as u, EvaluationReason as v, TrackedEventDetailValue as x, TrackOptions as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@configdirector/react-native-sdk",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "React Native client SDK for ConfigDirector. ConfigDirector is a remote configuration and feature flag service.",
5
5
  "license": "MIT",
6
6
  "author": "ConfigDirector",