@schematichq/schematic-react 1.2.4 → 1.2.6

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 CHANGED
@@ -53,6 +53,8 @@ const MyComponent = () => {
53
53
  };
54
54
  ```
55
55
 
56
+ To learn more about identifying companies with the `keys` map, see [key management in Schematic public docs](https://docs.schematichq.com/developer_resources/key_management).
57
+
56
58
  ### Tracking usage
57
59
 
58
60
  Once you've set the context with `identify`, you can track events:
@@ -71,6 +73,12 @@ const MyComponent = () => {
71
73
  };
72
74
  ```
73
75
 
76
+ If you want to record large numbers of the same event at once, or perhaps measure usage in terms of a unit like tokens or memory, you can optionally specify a quantity for your event:
77
+
78
+ ```tsx
79
+ track({ event: "query", quantity: 10 });
80
+ ```
81
+
74
82
  ### Checking flags
75
83
 
76
84
  To check a flag, you can use the `useSchematicFlag` hook:
@@ -170,6 +178,7 @@ ReactDOM.render(
170
178
 
171
179
  Offline mode automatically enables debug mode to help with troubleshooting.
172
180
 
181
+
173
182
  ## License
174
183
 
175
184
  MIT
@@ -72,20 +72,20 @@ var __toESM2 = (mod, isNodeMode, target) => (target = mod != null ? __create2(__
72
72
  var require_browser_polyfill = __commonJS({
73
73
  "node_modules/cross-fetch/dist/browser-polyfill.js"(exports) {
74
74
  (function(self2) {
75
- var irrelevant = function(exports2) {
75
+ var irrelevant = (function(exports2) {
76
76
  var g = typeof globalThis !== "undefined" && globalThis || typeof self2 !== "undefined" && self2 || // eslint-disable-next-line no-undef
77
77
  typeof global !== "undefined" && global || {};
78
78
  var support = {
79
79
  searchParams: "URLSearchParams" in g,
80
80
  iterable: "Symbol" in g && "iterator" in Symbol,
81
- blob: "FileReader" in g && "Blob" in g && function() {
81
+ blob: "FileReader" in g && "Blob" in g && (function() {
82
82
  try {
83
83
  new Blob();
84
84
  return true;
85
85
  } catch (e) {
86
86
  return false;
87
87
  }
88
- }(),
88
+ })(),
89
89
  formData: "FormData" in g,
90
90
  arrayBuffer: "ArrayBuffer" in g
91
91
  };
@@ -387,12 +387,12 @@ var require_browser_polyfill = __commonJS({
387
387
  }
388
388
  this.method = normalizeMethod(options.method || this.method || "GET");
389
389
  this.mode = options.mode || this.mode || null;
390
- this.signal = options.signal || this.signal || function() {
390
+ this.signal = options.signal || this.signal || (function() {
391
391
  if ("AbortController" in g) {
392
392
  var ctrl = new AbortController();
393
393
  return ctrl.signal;
394
394
  }
395
- }();
395
+ })();
396
396
  this.referrer = null;
397
397
  if ((this.method === "GET" || this.method === "HEAD") && body) {
398
398
  throw new TypeError("Body not allowed for GET or HEAD requests");
@@ -599,7 +599,7 @@ var require_browser_polyfill = __commonJS({
599
599
  exports2.Response = Response;
600
600
  exports2.fetch = fetch2;
601
601
  return exports2;
602
- }({});
602
+ })({});
603
603
  })(typeof self !== "undefined" ? self : exports);
604
604
  }
605
605
  });
@@ -623,10 +623,7 @@ function rng() {
623
623
  }
624
624
  var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
625
625
  var native_default = { randomUUID };
626
- function v4(options, buf, offset) {
627
- if (native_default.randomUUID && !buf && !options) {
628
- return native_default.randomUUID();
629
- }
626
+ function _v4(options, buf, offset) {
630
627
  options = options || {};
631
628
  const rnds = options.random ?? options.rng?.() ?? rng();
632
629
  if (rnds.length < 16) {
@@ -646,6 +643,12 @@ function v4(options, buf, offset) {
646
643
  }
647
644
  return unsafeStringify(rnds);
648
645
  }
646
+ function v4(options, buf, offset) {
647
+ if (native_default.randomUUID && !buf && !options) {
648
+ return native_default.randomUUID();
649
+ }
650
+ return _v4(options, buf, offset);
651
+ }
649
652
  var v4_default = v4;
650
653
  var import_polyfill = __toESM2(require_browser_polyfill());
651
654
  function CheckFlagResponseDataFromJSON(json) {
@@ -660,6 +663,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
660
663
  error: json["error"] == null ? void 0 : json["error"],
661
664
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
662
665
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
666
+ featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
663
667
  featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
664
668
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
665
669
  flag: json["flag"],
@@ -749,6 +753,7 @@ var CheckFlagReturnFromJSON = (json) => {
749
753
  error,
750
754
  featureAllocation,
751
755
  featureUsage,
756
+ featureUsageEvent,
752
757
  featureUsagePeriod,
753
758
  featureUsageResetAt,
754
759
  flag,
@@ -768,6 +773,7 @@ var CheckFlagReturnFromJSON = (json) => {
768
773
  error: error == null ? void 0 : error,
769
774
  featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
770
775
  featureUsage: featureUsage == null ? void 0 : featureUsage,
776
+ featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
771
777
  featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
772
778
  featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
773
779
  flag,
@@ -793,7 +799,7 @@ function contextString(context) {
793
799
  }, {});
794
800
  return JSON.stringify(sortedContext);
795
801
  }
796
- var version = "1.2.2";
802
+ var version = "1.2.6";
797
803
  var anonymousIdKey = "schematicId";
798
804
  var Schematic = class {
799
805
  additionalHeaders = {};
@@ -804,6 +810,7 @@ var Schematic = class {
804
810
  debugEnabled = false;
805
811
  offlineEnabled = false;
806
812
  eventQueue;
813
+ contextDependentEventQueue;
807
814
  eventUrl = "https://c.schematichq.com";
808
815
  flagCheckListeners = {};
809
816
  flagValueListeners = {};
@@ -812,10 +819,12 @@ var Schematic = class {
812
819
  storage;
813
820
  useWebSocket = false;
814
821
  checks = {};
822
+ featureUsageEventMap = {};
815
823
  webSocketUrl = "wss://api.schematichq.com";
816
824
  constructor(apiKey, options) {
817
825
  this.apiKey = apiKey;
818
826
  this.eventQueue = [];
827
+ this.contextDependentEventQueue = [];
819
828
  this.useWebSocket = options?.useWebSocket ?? false;
820
829
  this.debugEnabled = options?.debug ?? false;
821
830
  this.offlineEnabled = options?.offline ?? false;
@@ -858,6 +867,7 @@ var Schematic = class {
858
867
  if (typeof window !== "undefined" && window?.addEventListener) {
859
868
  window.addEventListener("beforeunload", () => {
860
869
  this.flushEventQueue();
870
+ this.flushContextDependentEventQueue();
861
871
  });
862
872
  }
863
873
  if (this.offlineEnabled) {
@@ -906,6 +916,9 @@ var Schematic = class {
906
916
  const parsedResponse = CheckFlagResponseFromJSON(response);
907
917
  this.debug(`checkFlag result: ${key}`, parsedResponse);
908
918
  const result = CheckFlagReturnFromJSON(parsedResponse.data);
919
+ if (typeof result.featureUsageEvent === "string") {
920
+ this.updateFeatureUsageEventMap(result);
921
+ }
909
922
  this.submitFlagCheckEvent(key, result, context);
910
923
  return result.value;
911
924
  }).catch((error) => {
@@ -1025,6 +1038,9 @@ var Schematic = class {
1025
1038
  const data = CheckFlagResponseFromJSON(responseJson);
1026
1039
  this.debug(`fallbackToRest result: ${key}`, data);
1027
1040
  const result = CheckFlagReturnFromJSON(data.data);
1041
+ if (typeof result.featureUsageEvent === "string") {
1042
+ this.updateFeatureUsageEventMap(result);
1043
+ }
1028
1044
  this.submitFlagCheckEvent(key, result, context);
1029
1045
  return result.value;
1030
1046
  } catch (error) {
@@ -1108,14 +1124,12 @@ var Schematic = class {
1108
1124
  * In offline mode, this will just set the context locally without connecting.
1109
1125
  */
1110
1126
  setContext = async (context) => {
1111
- if (this.isOffline()) {
1127
+ if (this.isOffline() || !this.useWebSocket) {
1112
1128
  this.context = context;
1129
+ this.flushContextDependentEventQueue();
1113
1130
  this.setIsPending(false);
1114
1131
  return Promise.resolve();
1115
1132
  }
1116
- if (!this.useWebSocket) {
1117
- return Promise.resolve();
1118
- }
1119
1133
  try {
1120
1134
  this.setIsPending(true);
1121
1135
  if (!this.conn) {
@@ -1131,21 +1145,123 @@ var Schematic = class {
1131
1145
  /**
1132
1146
  * Send a track event
1133
1147
  * Track usage for a company and/or user.
1148
+ * Optimistically updates feature usage flags if tracking a featureUsageEvent.
1134
1149
  */
1135
1150
  track = (body) => {
1136
- const { company, user, event, traits } = body;
1151
+ const { company, user, event, traits, quantity = 1 } = body;
1152
+ if (!this.hasContext(company, user)) {
1153
+ this.debug(`track: queuing event "${event}" until context is available`);
1154
+ const queuedEvent = {
1155
+ api_key: this.apiKey,
1156
+ body: {
1157
+ company,
1158
+ event,
1159
+ traits: traits ?? {},
1160
+ user,
1161
+ quantity
1162
+ },
1163
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
1164
+ tracker_event_id: v4_default(),
1165
+ tracker_user_id: this.getAnonymousId(),
1166
+ type: "track"
1167
+ };
1168
+ this.contextDependentEventQueue.push(queuedEvent);
1169
+ return Promise.resolve();
1170
+ }
1137
1171
  const trackData = {
1138
1172
  company: company ?? this.context.company,
1139
1173
  event,
1140
1174
  traits: traits ?? {},
1141
- user: user ?? this.context.user
1175
+ user: user ?? this.context.user,
1176
+ quantity
1142
1177
  };
1143
1178
  this.debug(`track:`, trackData);
1179
+ if (event in this.featureUsageEventMap) {
1180
+ this.optimisticallyUpdateFeatureUsage(event, quantity);
1181
+ }
1144
1182
  return this.handleEvent("track", trackData);
1145
1183
  };
1184
+ /**
1185
+ * Optimistically update feature usage flags associated with a tracked event
1186
+ * This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
1187
+ * before the network request completes
1188
+ */
1189
+ optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
1190
+ const flagsForEvent = this.featureUsageEventMap[eventName];
1191
+ if (flagsForEvent === void 0 || flagsForEvent === null) return;
1192
+ this.debug(
1193
+ `Optimistically updating feature usage for event: ${eventName}`,
1194
+ { quantity }
1195
+ );
1196
+ Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1197
+ if (check === void 0) return;
1198
+ const updatedCheck = { ...check };
1199
+ if (typeof updatedCheck.featureUsage === "number") {
1200
+ updatedCheck.featureUsage += quantity;
1201
+ if (typeof updatedCheck.featureAllocation === "number") {
1202
+ const wasExceeded = updatedCheck.featureUsageExceeded === true;
1203
+ const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
1204
+ if (nowExceeded !== wasExceeded) {
1205
+ updatedCheck.featureUsageExceeded = nowExceeded;
1206
+ if (nowExceeded) {
1207
+ updatedCheck.value = false;
1208
+ }
1209
+ this.debug(`Usage limit status changed for flag: ${flagKey}`, {
1210
+ was: wasExceeded ? "exceeded" : "within limits",
1211
+ now: nowExceeded ? "exceeded" : "within limits",
1212
+ featureUsage: updatedCheck.featureUsage,
1213
+ featureAllocation: updatedCheck.featureAllocation,
1214
+ value: updatedCheck.value
1215
+ });
1216
+ }
1217
+ }
1218
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1219
+ this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
1220
+ }
1221
+ const contextStr = contextString(this.context);
1222
+ if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
1223
+ this.checks[contextStr][flagKey] = updatedCheck;
1224
+ }
1225
+ this.notifyFlagCheckListeners(flagKey, updatedCheck);
1226
+ this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1227
+ }
1228
+ });
1229
+ };
1146
1230
  /**
1147
1231
  * Event processing
1148
1232
  */
1233
+ hasContext = (company, user) => {
1234
+ const hasProvidedContext = company !== void 0 && company !== null && Object.keys(company).length > 0 || user !== void 0 && user !== null && Object.keys(user).length > 0;
1235
+ const hasInstanceContext = this.context.company !== void 0 && this.context.company !== null && Object.keys(this.context.company).length > 0 || this.context.user !== void 0 && this.context.user !== null && Object.keys(this.context.user).length > 0;
1236
+ return hasProvidedContext || hasInstanceContext;
1237
+ };
1238
+ flushContextDependentEventQueue = () => {
1239
+ this.debug(
1240
+ `flushing ${this.contextDependentEventQueue.length} context-dependent events`
1241
+ );
1242
+ while (this.contextDependentEventQueue.length > 0) {
1243
+ const event = this.contextDependentEventQueue.shift();
1244
+ if (event) {
1245
+ if (event.type === "track" && typeof event.body === "object" && event.body !== null) {
1246
+ const trackBody = event.body;
1247
+ const updatedBody = {
1248
+ ...trackBody,
1249
+ company: trackBody.company ?? this.context.company,
1250
+ user: trackBody.user ?? this.context.user
1251
+ };
1252
+ const updatedEvent = {
1253
+ ...event,
1254
+ body: updatedBody,
1255
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
1256
+ // Update timestamp to actual send time
1257
+ };
1258
+ this.sendEvent(updatedEvent);
1259
+ } else {
1260
+ this.sendEvent(event);
1261
+ }
1262
+ }
1263
+ }
1264
+ };
1149
1265
  flushEventQueue = () => {
1150
1266
  while (this.eventQueue.length > 0) {
1151
1267
  const event = this.eventQueue.shift();
@@ -1175,7 +1291,7 @@ var Schematic = class {
1175
1291
  tracker_user_id: this.getAnonymousId(),
1176
1292
  type: eventType
1177
1293
  };
1178
- if (document?.hidden) {
1294
+ if (typeof document !== "undefined" && document?.hidden) {
1179
1295
  return this.storeEvent(event);
1180
1296
  } else {
1181
1297
  return this.sendEvent(event);
@@ -1285,18 +1401,26 @@ var Schematic = class {
1285
1401
  }
1286
1402
  (message.flags ?? []).forEach((flag) => {
1287
1403
  const flagCheck = CheckFlagReturnFromJSON(flag);
1288
- this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1404
+ const contextStr = contextString(context);
1405
+ if (this.checks[contextStr] === void 0) {
1406
+ this.checks[contextStr] = {};
1407
+ }
1408
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1289
1409
  this.debug(`WebSocket flag update:`, {
1290
1410
  flag: flagCheck.flag,
1291
1411
  value: flagCheck.value,
1292
1412
  flagCheck
1293
1413
  });
1414
+ if (typeof flagCheck.featureUsageEvent === "string") {
1415
+ this.updateFeatureUsageEventMap(flagCheck);
1416
+ }
1294
1417
  if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1295
1418
  this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1296
1419
  }
1297
1420
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1298
1421
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1299
1422
  });
1423
+ this.flushContextDependentEventQueue();
1300
1424
  this.setIsPending(false);
1301
1425
  if (!resolved) {
1302
1426
  resolved = true;
@@ -1383,8 +1507,26 @@ var Schematic = class {
1383
1507
  check
1384
1508
  );
1385
1509
  }
1510
+ if (typeof check.featureUsageEvent === "string") {
1511
+ this.updateFeatureUsageEventMap(check);
1512
+ }
1386
1513
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1387
1514
  };
1515
+ /** Add or update a CheckFlagReturn in the featureUsageEventMap */
1516
+ updateFeatureUsageEventMap = (check) => {
1517
+ if (typeof check.featureUsageEvent !== "string") return;
1518
+ const eventName = check.featureUsageEvent;
1519
+ if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
1520
+ this.featureUsageEventMap[eventName] = {};
1521
+ }
1522
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1523
+ this.featureUsageEventMap[eventName][check.flag] = check;
1524
+ }
1525
+ this.debug(
1526
+ `Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
1527
+ check
1528
+ );
1529
+ };
1388
1530
  notifyFlagValueListeners = (flagKey, value) => {
1389
1531
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1390
1532
  if (listeners.size > 0) {
@@ -1422,7 +1564,7 @@ var notifyFlagValueListener = (listener, value) => {
1422
1564
  var import_react = __toESM(require("react"));
1423
1565
 
1424
1566
  // src/version.ts
1425
- var version2 = "1.2.4";
1567
+ var version2 = "1.2.6";
1426
1568
 
1427
1569
  // src/context/schematic.tsx
1428
1570
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -11,6 +11,7 @@ import { Schematic } from '@schematichq/schematic-js';
11
11
  import { SchematicContext } from '@schematichq/schematic-js';
12
12
  import * as SchematicJS from '@schematichq/schematic-js';
13
13
  import { SchematicOptions } from '@schematichq/schematic-js';
14
+ import { StoragePersister } from '@schematichq/schematic-js';
14
15
  import { Traits } from '@schematichq/schematic-js';
15
16
  import { UsagePeriod } from '@schematichq/schematic-js';
16
17
 
@@ -62,6 +63,8 @@ declare type SchematicProviderPropsWithPublishableKey = BaseSchematicProviderPro
62
63
  publishableKey: string;
63
64
  };
64
65
 
66
+ export { StoragePersister }
67
+
65
68
  export { Traits }
66
69
 
67
70
  export { UsagePeriod }
@@ -27,20 +27,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var require_browser_polyfill = __commonJS({
28
28
  "node_modules/cross-fetch/dist/browser-polyfill.js"(exports) {
29
29
  (function(self2) {
30
- var irrelevant = function(exports2) {
30
+ var irrelevant = (function(exports2) {
31
31
  var g = typeof globalThis !== "undefined" && globalThis || typeof self2 !== "undefined" && self2 || // eslint-disable-next-line no-undef
32
32
  typeof global !== "undefined" && global || {};
33
33
  var support = {
34
34
  searchParams: "URLSearchParams" in g,
35
35
  iterable: "Symbol" in g && "iterator" in Symbol,
36
- blob: "FileReader" in g && "Blob" in g && function() {
36
+ blob: "FileReader" in g && "Blob" in g && (function() {
37
37
  try {
38
38
  new Blob();
39
39
  return true;
40
40
  } catch (e) {
41
41
  return false;
42
42
  }
43
- }(),
43
+ })(),
44
44
  formData: "FormData" in g,
45
45
  arrayBuffer: "ArrayBuffer" in g
46
46
  };
@@ -342,12 +342,12 @@ var require_browser_polyfill = __commonJS({
342
342
  }
343
343
  this.method = normalizeMethod(options.method || this.method || "GET");
344
344
  this.mode = options.mode || this.mode || null;
345
- this.signal = options.signal || this.signal || function() {
345
+ this.signal = options.signal || this.signal || (function() {
346
346
  if ("AbortController" in g) {
347
347
  var ctrl = new AbortController();
348
348
  return ctrl.signal;
349
349
  }
350
- }();
350
+ })();
351
351
  this.referrer = null;
352
352
  if ((this.method === "GET" || this.method === "HEAD") && body) {
353
353
  throw new TypeError("Body not allowed for GET or HEAD requests");
@@ -554,7 +554,7 @@ var require_browser_polyfill = __commonJS({
554
554
  exports2.Response = Response;
555
555
  exports2.fetch = fetch2;
556
556
  return exports2;
557
- }({});
557
+ })({});
558
558
  })(typeof self !== "undefined" ? self : exports);
559
559
  }
560
560
  });
@@ -578,10 +578,7 @@ function rng() {
578
578
  }
579
579
  var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto);
580
580
  var native_default = { randomUUID };
581
- function v4(options, buf, offset) {
582
- if (native_default.randomUUID && !buf && !options) {
583
- return native_default.randomUUID();
584
- }
581
+ function _v4(options, buf, offset) {
585
582
  options = options || {};
586
583
  const rnds = options.random ?? options.rng?.() ?? rng();
587
584
  if (rnds.length < 16) {
@@ -601,6 +598,12 @@ function v4(options, buf, offset) {
601
598
  }
602
599
  return unsafeStringify(rnds);
603
600
  }
601
+ function v4(options, buf, offset) {
602
+ if (native_default.randomUUID && !buf && !options) {
603
+ return native_default.randomUUID();
604
+ }
605
+ return _v4(options, buf, offset);
606
+ }
604
607
  var v4_default = v4;
605
608
  var import_polyfill = __toESM(require_browser_polyfill());
606
609
  function CheckFlagResponseDataFromJSON(json) {
@@ -615,6 +618,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
615
618
  error: json["error"] == null ? void 0 : json["error"],
616
619
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
617
620
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
621
+ featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
618
622
  featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
619
623
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
620
624
  flag: json["flag"],
@@ -704,6 +708,7 @@ var CheckFlagReturnFromJSON = (json) => {
704
708
  error,
705
709
  featureAllocation,
706
710
  featureUsage,
711
+ featureUsageEvent,
707
712
  featureUsagePeriod,
708
713
  featureUsageResetAt,
709
714
  flag,
@@ -723,6 +728,7 @@ var CheckFlagReturnFromJSON = (json) => {
723
728
  error: error == null ? void 0 : error,
724
729
  featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
725
730
  featureUsage: featureUsage == null ? void 0 : featureUsage,
731
+ featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
726
732
  featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
727
733
  featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
728
734
  flag,
@@ -748,7 +754,7 @@ function contextString(context) {
748
754
  }, {});
749
755
  return JSON.stringify(sortedContext);
750
756
  }
751
- var version = "1.2.2";
757
+ var version = "1.2.6";
752
758
  var anonymousIdKey = "schematicId";
753
759
  var Schematic = class {
754
760
  additionalHeaders = {};
@@ -759,6 +765,7 @@ var Schematic = class {
759
765
  debugEnabled = false;
760
766
  offlineEnabled = false;
761
767
  eventQueue;
768
+ contextDependentEventQueue;
762
769
  eventUrl = "https://c.schematichq.com";
763
770
  flagCheckListeners = {};
764
771
  flagValueListeners = {};
@@ -767,10 +774,12 @@ var Schematic = class {
767
774
  storage;
768
775
  useWebSocket = false;
769
776
  checks = {};
777
+ featureUsageEventMap = {};
770
778
  webSocketUrl = "wss://api.schematichq.com";
771
779
  constructor(apiKey, options) {
772
780
  this.apiKey = apiKey;
773
781
  this.eventQueue = [];
782
+ this.contextDependentEventQueue = [];
774
783
  this.useWebSocket = options?.useWebSocket ?? false;
775
784
  this.debugEnabled = options?.debug ?? false;
776
785
  this.offlineEnabled = options?.offline ?? false;
@@ -813,6 +822,7 @@ var Schematic = class {
813
822
  if (typeof window !== "undefined" && window?.addEventListener) {
814
823
  window.addEventListener("beforeunload", () => {
815
824
  this.flushEventQueue();
825
+ this.flushContextDependentEventQueue();
816
826
  });
817
827
  }
818
828
  if (this.offlineEnabled) {
@@ -861,6 +871,9 @@ var Schematic = class {
861
871
  const parsedResponse = CheckFlagResponseFromJSON(response);
862
872
  this.debug(`checkFlag result: ${key}`, parsedResponse);
863
873
  const result = CheckFlagReturnFromJSON(parsedResponse.data);
874
+ if (typeof result.featureUsageEvent === "string") {
875
+ this.updateFeatureUsageEventMap(result);
876
+ }
864
877
  this.submitFlagCheckEvent(key, result, context);
865
878
  return result.value;
866
879
  }).catch((error) => {
@@ -980,6 +993,9 @@ var Schematic = class {
980
993
  const data = CheckFlagResponseFromJSON(responseJson);
981
994
  this.debug(`fallbackToRest result: ${key}`, data);
982
995
  const result = CheckFlagReturnFromJSON(data.data);
996
+ if (typeof result.featureUsageEvent === "string") {
997
+ this.updateFeatureUsageEventMap(result);
998
+ }
983
999
  this.submitFlagCheckEvent(key, result, context);
984
1000
  return result.value;
985
1001
  } catch (error) {
@@ -1063,14 +1079,12 @@ var Schematic = class {
1063
1079
  * In offline mode, this will just set the context locally without connecting.
1064
1080
  */
1065
1081
  setContext = async (context) => {
1066
- if (this.isOffline()) {
1082
+ if (this.isOffline() || !this.useWebSocket) {
1067
1083
  this.context = context;
1084
+ this.flushContextDependentEventQueue();
1068
1085
  this.setIsPending(false);
1069
1086
  return Promise.resolve();
1070
1087
  }
1071
- if (!this.useWebSocket) {
1072
- return Promise.resolve();
1073
- }
1074
1088
  try {
1075
1089
  this.setIsPending(true);
1076
1090
  if (!this.conn) {
@@ -1086,21 +1100,123 @@ var Schematic = class {
1086
1100
  /**
1087
1101
  * Send a track event
1088
1102
  * Track usage for a company and/or user.
1103
+ * Optimistically updates feature usage flags if tracking a featureUsageEvent.
1089
1104
  */
1090
1105
  track = (body) => {
1091
- const { company, user, event, traits } = body;
1106
+ const { company, user, event, traits, quantity = 1 } = body;
1107
+ if (!this.hasContext(company, user)) {
1108
+ this.debug(`track: queuing event "${event}" until context is available`);
1109
+ const queuedEvent = {
1110
+ api_key: this.apiKey,
1111
+ body: {
1112
+ company,
1113
+ event,
1114
+ traits: traits ?? {},
1115
+ user,
1116
+ quantity
1117
+ },
1118
+ sent_at: (/* @__PURE__ */ new Date()).toISOString(),
1119
+ tracker_event_id: v4_default(),
1120
+ tracker_user_id: this.getAnonymousId(),
1121
+ type: "track"
1122
+ };
1123
+ this.contextDependentEventQueue.push(queuedEvent);
1124
+ return Promise.resolve();
1125
+ }
1092
1126
  const trackData = {
1093
1127
  company: company ?? this.context.company,
1094
1128
  event,
1095
1129
  traits: traits ?? {},
1096
- user: user ?? this.context.user
1130
+ user: user ?? this.context.user,
1131
+ quantity
1097
1132
  };
1098
1133
  this.debug(`track:`, trackData);
1134
+ if (event in this.featureUsageEventMap) {
1135
+ this.optimisticallyUpdateFeatureUsage(event, quantity);
1136
+ }
1099
1137
  return this.handleEvent("track", trackData);
1100
1138
  };
1139
+ /**
1140
+ * Optimistically update feature usage flags associated with a tracked event
1141
+ * This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
1142
+ * before the network request completes
1143
+ */
1144
+ optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
1145
+ const flagsForEvent = this.featureUsageEventMap[eventName];
1146
+ if (flagsForEvent === void 0 || flagsForEvent === null) return;
1147
+ this.debug(
1148
+ `Optimistically updating feature usage for event: ${eventName}`,
1149
+ { quantity }
1150
+ );
1151
+ Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1152
+ if (check === void 0) return;
1153
+ const updatedCheck = { ...check };
1154
+ if (typeof updatedCheck.featureUsage === "number") {
1155
+ updatedCheck.featureUsage += quantity;
1156
+ if (typeof updatedCheck.featureAllocation === "number") {
1157
+ const wasExceeded = updatedCheck.featureUsageExceeded === true;
1158
+ const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
1159
+ if (nowExceeded !== wasExceeded) {
1160
+ updatedCheck.featureUsageExceeded = nowExceeded;
1161
+ if (nowExceeded) {
1162
+ updatedCheck.value = false;
1163
+ }
1164
+ this.debug(`Usage limit status changed for flag: ${flagKey}`, {
1165
+ was: wasExceeded ? "exceeded" : "within limits",
1166
+ now: nowExceeded ? "exceeded" : "within limits",
1167
+ featureUsage: updatedCheck.featureUsage,
1168
+ featureAllocation: updatedCheck.featureAllocation,
1169
+ value: updatedCheck.value
1170
+ });
1171
+ }
1172
+ }
1173
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1174
+ this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
1175
+ }
1176
+ const contextStr = contextString(this.context);
1177
+ if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
1178
+ this.checks[contextStr][flagKey] = updatedCheck;
1179
+ }
1180
+ this.notifyFlagCheckListeners(flagKey, updatedCheck);
1181
+ this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1182
+ }
1183
+ });
1184
+ };
1101
1185
  /**
1102
1186
  * Event processing
1103
1187
  */
1188
+ hasContext = (company, user) => {
1189
+ const hasProvidedContext = company !== void 0 && company !== null && Object.keys(company).length > 0 || user !== void 0 && user !== null && Object.keys(user).length > 0;
1190
+ const hasInstanceContext = this.context.company !== void 0 && this.context.company !== null && Object.keys(this.context.company).length > 0 || this.context.user !== void 0 && this.context.user !== null && Object.keys(this.context.user).length > 0;
1191
+ return hasProvidedContext || hasInstanceContext;
1192
+ };
1193
+ flushContextDependentEventQueue = () => {
1194
+ this.debug(
1195
+ `flushing ${this.contextDependentEventQueue.length} context-dependent events`
1196
+ );
1197
+ while (this.contextDependentEventQueue.length > 0) {
1198
+ const event = this.contextDependentEventQueue.shift();
1199
+ if (event) {
1200
+ if (event.type === "track" && typeof event.body === "object" && event.body !== null) {
1201
+ const trackBody = event.body;
1202
+ const updatedBody = {
1203
+ ...trackBody,
1204
+ company: trackBody.company ?? this.context.company,
1205
+ user: trackBody.user ?? this.context.user
1206
+ };
1207
+ const updatedEvent = {
1208
+ ...event,
1209
+ body: updatedBody,
1210
+ sent_at: (/* @__PURE__ */ new Date()).toISOString()
1211
+ // Update timestamp to actual send time
1212
+ };
1213
+ this.sendEvent(updatedEvent);
1214
+ } else {
1215
+ this.sendEvent(event);
1216
+ }
1217
+ }
1218
+ }
1219
+ };
1104
1220
  flushEventQueue = () => {
1105
1221
  while (this.eventQueue.length > 0) {
1106
1222
  const event = this.eventQueue.shift();
@@ -1130,7 +1246,7 @@ var Schematic = class {
1130
1246
  tracker_user_id: this.getAnonymousId(),
1131
1247
  type: eventType
1132
1248
  };
1133
- if (document?.hidden) {
1249
+ if (typeof document !== "undefined" && document?.hidden) {
1134
1250
  return this.storeEvent(event);
1135
1251
  } else {
1136
1252
  return this.sendEvent(event);
@@ -1240,18 +1356,26 @@ var Schematic = class {
1240
1356
  }
1241
1357
  (message.flags ?? []).forEach((flag) => {
1242
1358
  const flagCheck = CheckFlagReturnFromJSON(flag);
1243
- this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1359
+ const contextStr = contextString(context);
1360
+ if (this.checks[contextStr] === void 0) {
1361
+ this.checks[contextStr] = {};
1362
+ }
1363
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1244
1364
  this.debug(`WebSocket flag update:`, {
1245
1365
  flag: flagCheck.flag,
1246
1366
  value: flagCheck.value,
1247
1367
  flagCheck
1248
1368
  });
1369
+ if (typeof flagCheck.featureUsageEvent === "string") {
1370
+ this.updateFeatureUsageEventMap(flagCheck);
1371
+ }
1249
1372
  if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1250
1373
  this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1251
1374
  }
1252
1375
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1253
1376
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1254
1377
  });
1378
+ this.flushContextDependentEventQueue();
1255
1379
  this.setIsPending(false);
1256
1380
  if (!resolved) {
1257
1381
  resolved = true;
@@ -1338,8 +1462,26 @@ var Schematic = class {
1338
1462
  check
1339
1463
  );
1340
1464
  }
1465
+ if (typeof check.featureUsageEvent === "string") {
1466
+ this.updateFeatureUsageEventMap(check);
1467
+ }
1341
1468
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1342
1469
  };
1470
+ /** Add or update a CheckFlagReturn in the featureUsageEventMap */
1471
+ updateFeatureUsageEventMap = (check) => {
1472
+ if (typeof check.featureUsageEvent !== "string") return;
1473
+ const eventName = check.featureUsageEvent;
1474
+ if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
1475
+ this.featureUsageEventMap[eventName] = {};
1476
+ }
1477
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1478
+ this.featureUsageEventMap[eventName][check.flag] = check;
1479
+ }
1480
+ this.debug(
1481
+ `Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
1482
+ check
1483
+ );
1484
+ };
1343
1485
  notifyFlagValueListeners = (flagKey, value) => {
1344
1486
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1345
1487
  if (listeners.size > 0) {
@@ -1377,7 +1519,7 @@ var notifyFlagValueListener = (listener, value) => {
1377
1519
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1378
1520
 
1379
1521
  // src/version.ts
1380
- var version2 = "1.2.4";
1522
+ var version2 = "1.2.6";
1381
1523
 
1382
1524
  // src/context/schematic.tsx
1383
1525
  import { jsx } from "react/jsx-runtime";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schematichq/schematic-react",
3
- "version": "1.2.4",
3
+ "version": "1.2.6",
4
4
  "main": "dist/schematic-react.cjs.js",
5
5
  "module": "dist/schematic-react.esm.js",
6
6
  "types": "dist/schematic-react.d.ts",
@@ -25,31 +25,39 @@
25
25
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
26
26
  "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
27
27
  "test": "jest --config jest.config.js",
28
- "tsc": "npx tsc"
28
+ "test:reactnative": "jest --config jest.config.reactnative.js",
29
+ "tsc": "npx tsc",
30
+ "prepare": "husky"
29
31
  },
30
32
  "dependencies": {
31
- "@schematichq/schematic-js": "^1.2.2"
33
+ "@schematichq/schematic-js": "^1.2.6"
32
34
  },
33
35
  "devDependencies": {
34
- "@microsoft/api-extractor": "^7.49.2",
35
- "@types/jest": "^29.5.14",
36
- "@types/react": "^19.0.8",
37
- "@typescript-eslint/eslint-plugin": "^8.23.0",
38
- "@typescript-eslint/parser": "^8.23.0",
39
- "esbuild": "^0.25.0",
36
+ "@eslint/js": "^9.24.0",
37
+ "@microsoft/api-extractor": "^7.52.2",
38
+ "@testing-library/dom": "^10.4.1",
39
+ "@testing-library/jest-dom": "^6.8.0",
40
+ "@testing-library/react": "^16.3.0",
41
+ "@types/jest": "^30.0.0",
42
+ "@types/react": "^19.1.1",
43
+ "esbuild": "^0.25.2",
40
44
  "esbuild-jest": "^0.5.0",
41
- "eslint": "^8.57.1",
42
- "eslint-plugin-import": "^2.31.0",
43
- "eslint-plugin-react": "^7.37.4",
44
- "eslint-plugin-react-hooks": "^5.1.0",
45
- "jest": "^29.7.0",
46
- "jest-environment-jsdom": "^29.7.0",
47
- "jest-esbuild": "^0.3.0",
45
+ "eslint": "^9.24.0",
46
+ "eslint-plugin-import": "^2.32.0",
47
+ "eslint-plugin-react": "^7.37.5",
48
+ "eslint-plugin-react-hooks": "^5.2.0",
49
+ "globals": "^16.0.0",
50
+ "husky": "^9.1.7",
51
+ "jest": "^30.0.0",
52
+ "jest-environment-jsdom": "^30.0.0",
53
+ "jest-esbuild": "^0.4.0",
48
54
  "jest-fetch-mock": "^3.0.3",
49
- "prettier": "^3.4.2",
50
- "react": "^19.0.0",
51
- "ts-jest": "^29.2.5",
52
- "typescript": "^5.7.3"
55
+ "prettier": "^3.6.2",
56
+ "react": "^19.1.1",
57
+ "react-dom": "^19.1.1",
58
+ "ts-jest": "^29.3.0",
59
+ "typescript": "^5.9.2",
60
+ "typescript-eslint": "^8.29.1"
53
61
  },
54
62
  "peerDependencies": {
55
63
  "react": ">=18"