@schematichq/schematic-react 1.2.3 → 1.2.5

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:
@@ -126,6 +134,51 @@ const MyComponent = () => {
126
134
  };
127
135
  ```
128
136
 
137
+ ## Troubleshooting
138
+
139
+ For debugging and development, Schematic supports two special modes:
140
+
141
+ ### Debug Mode
142
+
143
+ Enables console logging of all Schematic operations:
144
+
145
+ ```typescript
146
+ // Enable at initialization
147
+ import { SchematicProvider } from "@schematichq/schematic-react";
148
+
149
+ ReactDOM.render(
150
+ <SchematicProvider publishableKey="your-publishable-key" debug={true}>
151
+ <App />
152
+ </SchematicProvider>,
153
+ document.getElementById("root"),
154
+ );
155
+
156
+ // Or via URL parameter
157
+ // https://yoursite.com/?schematic_debug=true
158
+ ```
159
+
160
+ ### Offline Mode
161
+
162
+ Prevents network requests and returns fallback values for all flag checks:
163
+
164
+ ```typescript
165
+ // Enable at initialization
166
+ import { SchematicProvider } from "@schematichq/schematic-react";
167
+
168
+ ReactDOM.render(
169
+ <SchematicProvider publishableKey="your-publishable-key" offline={true}>
170
+ <App />
171
+ </SchematicProvider>,
172
+ document.getElementById("root"),
173
+ );
174
+
175
+ // Or via URL parameter
176
+ // https://yoursite.com/?schematic_offline=true
177
+ ```
178
+
179
+ Offline mode automatically enables debug mode to help with troubleshooting.
180
+
181
+
129
182
  ## License
130
183
 
131
184
  MIT
@@ -660,6 +660,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
660
660
  error: json["error"] == null ? void 0 : json["error"],
661
661
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
662
662
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
663
+ featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
663
664
  featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
664
665
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
665
666
  flag: json["flag"],
@@ -671,6 +672,26 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
671
672
  value: json["value"]
672
673
  };
673
674
  }
675
+ function EventBodyFlagCheckToJSON(json) {
676
+ return EventBodyFlagCheckToJSONTyped(json, false);
677
+ }
678
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
679
+ if (value == null) {
680
+ return value;
681
+ }
682
+ return {
683
+ company_id: value["companyId"],
684
+ error: value["error"],
685
+ flag_id: value["flagId"],
686
+ flag_key: value["flagKey"],
687
+ reason: value["reason"],
688
+ req_company: value["reqCompany"],
689
+ req_user: value["reqUser"],
690
+ rule_id: value["ruleId"],
691
+ user_id: value["userId"],
692
+ value: value["value"]
693
+ };
694
+ }
674
695
  function CheckFlagResponseFromJSON(json) {
675
696
  return CheckFlagResponseFromJSONTyped(json, false);
676
697
  }
@@ -729,6 +750,7 @@ var CheckFlagReturnFromJSON = (json) => {
729
750
  error,
730
751
  featureAllocation,
731
752
  featureUsage,
753
+ featureUsageEvent,
732
754
  featureUsagePeriod,
733
755
  featureUsageResetAt,
734
756
  flag,
@@ -748,6 +770,7 @@ var CheckFlagReturnFromJSON = (json) => {
748
770
  error: error == null ? void 0 : error,
749
771
  featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
750
772
  featureUsage: featureUsage == null ? void 0 : featureUsage,
773
+ featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
751
774
  featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
752
775
  featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
753
776
  flag,
@@ -773,7 +796,7 @@ function contextString(context) {
773
796
  }, {});
774
797
  return JSON.stringify(sortedContext);
775
798
  }
776
- var version = "1.2.1";
799
+ var version = "1.2.3";
777
800
  var anonymousIdKey = "schematicId";
778
801
  var Schematic = class {
779
802
  additionalHeaders = {};
@@ -781,6 +804,8 @@ var Schematic = class {
781
804
  apiUrl = "https://api.schematichq.com";
782
805
  conn = null;
783
806
  context = {};
807
+ debugEnabled = false;
808
+ offlineEnabled = false;
784
809
  eventQueue;
785
810
  eventUrl = "https://c.schematichq.com";
786
811
  flagCheckListeners = {};
@@ -790,11 +815,32 @@ var Schematic = class {
790
815
  storage;
791
816
  useWebSocket = false;
792
817
  checks = {};
818
+ featureUsageEventMap = {};
793
819
  webSocketUrl = "wss://api.schematichq.com";
794
820
  constructor(apiKey, options) {
795
821
  this.apiKey = apiKey;
796
822
  this.eventQueue = [];
797
823
  this.useWebSocket = options?.useWebSocket ?? false;
824
+ this.debugEnabled = options?.debug ?? false;
825
+ this.offlineEnabled = options?.offline ?? false;
826
+ if (typeof window !== "undefined" && typeof window.location !== "undefined") {
827
+ const params = new URLSearchParams(window.location.search);
828
+ const debugParam = params.get("schematic_debug");
829
+ if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
830
+ this.debugEnabled = true;
831
+ }
832
+ const offlineParam = params.get("schematic_offline");
833
+ if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
834
+ this.offlineEnabled = true;
835
+ this.debugEnabled = true;
836
+ }
837
+ }
838
+ if (this.offlineEnabled && options?.debug !== false) {
839
+ this.debugEnabled = true;
840
+ }
841
+ if (this.offlineEnabled) {
842
+ this.setIsPending(false);
843
+ }
798
844
  this.additionalHeaders = {
799
845
  "X-Schematic-Client-Version": `schematic-js@${version}`,
800
846
  ...options?.additionalHeaders ?? {}
@@ -818,6 +864,13 @@ var Schematic = class {
818
864
  this.flushEventQueue();
819
865
  });
820
866
  }
867
+ if (this.offlineEnabled) {
868
+ this.debug(
869
+ "Initialized with offline mode enabled - no network requests will be made"
870
+ );
871
+ } else if (this.debugEnabled) {
872
+ this.debug("Initialized with debug mode enabled");
873
+ }
821
874
  }
822
875
  /**
823
876
  * Get value for a single flag.
@@ -830,6 +883,14 @@ var Schematic = class {
830
883
  const { fallback = false, key } = options;
831
884
  const context = options.context || this.context;
832
885
  const contextStr = contextString(context);
886
+ this.debug(`checkFlag: ${key}`, { context, fallback });
887
+ if (this.isOffline()) {
888
+ this.debug(`checkFlag offline result: ${key}`, {
889
+ value: fallback,
890
+ offlineMode: true
891
+ });
892
+ return fallback;
893
+ }
833
894
  if (!this.useWebSocket) {
834
895
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
835
896
  return fetch(requestUrl, {
@@ -846,17 +907,35 @@ var Schematic = class {
846
907
  }
847
908
  return response.json();
848
909
  }).then((response) => {
849
- return CheckFlagResponseFromJSON(response).data.value;
910
+ const parsedResponse = CheckFlagResponseFromJSON(response);
911
+ this.debug(`checkFlag result: ${key}`, parsedResponse);
912
+ const result = CheckFlagReturnFromJSON(parsedResponse.data);
913
+ if (typeof result.featureUsageEvent === "string") {
914
+ this.updateFeatureUsageEventMap(result);
915
+ }
916
+ this.submitFlagCheckEvent(key, result, context);
917
+ return result.value;
850
918
  }).catch((error) => {
851
919
  console.error("There was a problem with the fetch operation:", error);
920
+ const errorResult = {
921
+ flag: key,
922
+ value: fallback,
923
+ reason: "API request failed",
924
+ error: error instanceof Error ? error.message : String(error)
925
+ };
926
+ this.submitFlagCheckEvent(key, errorResult, context);
852
927
  return fallback;
853
928
  });
854
929
  }
855
930
  try {
856
931
  const existingVals = this.checks[contextStr];
857
- if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
932
+ if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
933
+ this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
858
934
  return existingVals[key].value;
859
935
  }
936
+ if (this.isOffline()) {
937
+ return fallback;
938
+ }
860
939
  try {
861
940
  await this.setContext(context);
862
941
  } catch (error) {
@@ -867,16 +946,74 @@ var Schematic = class {
867
946
  return this.fallbackToRest(key, context, fallback);
868
947
  }
869
948
  const contextVals = this.checks[contextStr] ?? {};
870
- return contextVals[key]?.value ?? fallback;
949
+ const flagCheck = contextVals[key];
950
+ const result = flagCheck?.value ?? fallback;
951
+ this.debug(
952
+ `checkFlag WebSocket result: ${key}`,
953
+ typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
954
+ );
955
+ if (typeof flagCheck !== "undefined") {
956
+ this.submitFlagCheckEvent(key, flagCheck, context);
957
+ }
958
+ return result;
871
959
  } catch (error) {
872
960
  console.error("Unexpected error in checkFlag:", error);
961
+ const errorResult = {
962
+ flag: key,
963
+ value: fallback,
964
+ reason: "Unexpected error in flag check",
965
+ error: error instanceof Error ? error.message : String(error)
966
+ };
967
+ this.submitFlagCheckEvent(key, errorResult, context);
873
968
  return fallback;
874
969
  }
875
970
  }
971
+ /**
972
+ * Helper function to log debug messages
973
+ * Only logs if debug mode is enabled
974
+ */
975
+ debug(message, ...args) {
976
+ if (this.debugEnabled) {
977
+ console.log(`[Schematic] ${message}`, ...args);
978
+ }
979
+ }
980
+ /**
981
+ * Helper function to check if client is in offline mode
982
+ */
983
+ isOffline() {
984
+ return this.offlineEnabled;
985
+ }
986
+ /**
987
+ * Submit a flag check event
988
+ * Records data about a flag check for analytics
989
+ */
990
+ submitFlagCheckEvent(flagKey, result, context) {
991
+ const eventBody = {
992
+ flagKey,
993
+ value: result.value,
994
+ reason: result.reason,
995
+ flagId: result.flagId,
996
+ ruleId: result.ruleId,
997
+ companyId: result.companyId,
998
+ userId: result.userId,
999
+ error: result.error,
1000
+ reqCompany: context.company,
1001
+ reqUser: context.user
1002
+ };
1003
+ this.debug(`submitting flag check event:`, eventBody);
1004
+ return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
1005
+ }
876
1006
  /**
877
1007
  * Helper method for falling back to REST API when WebSocket connection fails
878
1008
  */
879
1009
  async fallbackToRest(key, context, fallback) {
1010
+ if (this.isOffline()) {
1011
+ this.debug(`fallbackToRest offline result: ${key}`, {
1012
+ value: fallback,
1013
+ offlineMode: true
1014
+ });
1015
+ return fallback;
1016
+ }
880
1017
  try {
881
1018
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
882
1019
  const response = await fetch(requestUrl, {
@@ -891,19 +1028,39 @@ var Schematic = class {
891
1028
  if (!response.ok) {
892
1029
  throw new Error("Network response was not ok");
893
1030
  }
894
- const data = CheckFlagResponseFromJSON(await response.json());
895
- return data?.data?.value ?? false;
1031
+ const responseJson = await response.json();
1032
+ const data = CheckFlagResponseFromJSON(responseJson);
1033
+ this.debug(`fallbackToRest result: ${key}`, data);
1034
+ const result = CheckFlagReturnFromJSON(data.data);
1035
+ if (typeof result.featureUsageEvent === "string") {
1036
+ this.updateFeatureUsageEventMap(result);
1037
+ }
1038
+ this.submitFlagCheckEvent(key, result, context);
1039
+ return result.value;
896
1040
  } catch (error) {
897
1041
  console.error("REST API call failed, using fallback value:", error);
1042
+ const errorResult = {
1043
+ flag: key,
1044
+ value: fallback,
1045
+ reason: "API request failed (fallback)",
1046
+ error: error instanceof Error ? error.message : String(error)
1047
+ };
1048
+ this.submitFlagCheckEvent(key, errorResult, context);
898
1049
  return fallback;
899
1050
  }
900
1051
  }
901
1052
  /**
902
1053
  * Make an API call to fetch all flag values for a given context.
903
1054
  * Recommended for use in REST mode only.
1055
+ * In offline mode, returns an empty object.
904
1056
  */
905
1057
  checkFlags = async (context) => {
906
1058
  context = context || this.context;
1059
+ this.debug(`checkFlags`, { context });
1060
+ if (this.isOffline()) {
1061
+ this.debug(`checkFlags offline result: returning empty object`);
1062
+ return {};
1063
+ }
907
1064
  const requestUrl = `${this.apiUrl}/flags/check`;
908
1065
  const requestBody = JSON.stringify(context);
909
1066
  return fetch(requestUrl, {
@@ -921,6 +1078,7 @@ var Schematic = class {
921
1078
  return response.json();
922
1079
  }).then((responseJson) => {
923
1080
  const resp = CheckFlagsResponseFromJSON(responseJson);
1081
+ this.debug(`checkFlags result:`, resp);
924
1082
  return (resp?.data?.flags ?? []).reduce(
925
1083
  (accum, flag) => {
926
1084
  accum[flag.flag] = flag.value;
@@ -939,6 +1097,7 @@ var Schematic = class {
939
1097
  * send an identify event to the Schematic API which will upsert a user and company.
940
1098
  */
941
1099
  identify = (body) => {
1100
+ this.debug(`identify:`, body);
942
1101
  try {
943
1102
  this.setContext({
944
1103
  company: body.company?.keys,
@@ -956,10 +1115,12 @@ var Schematic = class {
956
1115
  * 2. Send the context to the server
957
1116
  * 3. Wait for initial flag values to be returned
958
1117
  * The promise resolves when initial flag values are received.
1118
+ * In offline mode, this will just set the context locally without connecting.
959
1119
  */
960
1120
  setContext = async (context) => {
961
- if (!this.useWebSocket) {
1121
+ if (this.isOffline() || !this.useWebSocket) {
962
1122
  this.context = context;
1123
+ this.setIsPending(false);
963
1124
  return Promise.resolve();
964
1125
  }
965
1126
  try {
@@ -977,14 +1138,67 @@ var Schematic = class {
977
1138
  /**
978
1139
  * Send a track event
979
1140
  * Track usage for a company and/or user.
1141
+ * Optimistically updates feature usage flags if tracking a featureUsageEvent.
980
1142
  */
981
1143
  track = (body) => {
982
- const { company, user, event, traits } = body;
983
- return this.handleEvent("track", {
1144
+ const { company, user, event, traits, quantity = 1 } = body;
1145
+ const trackData = {
984
1146
  company: company ?? this.context.company,
985
1147
  event,
986
1148
  traits: traits ?? {},
987
- user: user ?? this.context.user
1149
+ user: user ?? this.context.user,
1150
+ quantity
1151
+ };
1152
+ this.debug(`track:`, trackData);
1153
+ if (event in this.featureUsageEventMap) {
1154
+ this.optimisticallyUpdateFeatureUsage(event, quantity);
1155
+ }
1156
+ return this.handleEvent("track", trackData);
1157
+ };
1158
+ /**
1159
+ * Optimistically update feature usage flags associated with a tracked event
1160
+ * This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
1161
+ * before the network request completes
1162
+ */
1163
+ optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
1164
+ const flagsForEvent = this.featureUsageEventMap[eventName];
1165
+ if (flagsForEvent === void 0 || flagsForEvent === null) return;
1166
+ this.debug(
1167
+ `Optimistically updating feature usage for event: ${eventName}`,
1168
+ { quantity }
1169
+ );
1170
+ Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1171
+ if (check === void 0) return;
1172
+ const updatedCheck = { ...check };
1173
+ if (typeof updatedCheck.featureUsage === "number") {
1174
+ updatedCheck.featureUsage += quantity;
1175
+ if (typeof updatedCheck.featureAllocation === "number") {
1176
+ const wasExceeded = updatedCheck.featureUsageExceeded === true;
1177
+ const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
1178
+ if (nowExceeded !== wasExceeded) {
1179
+ updatedCheck.featureUsageExceeded = nowExceeded;
1180
+ if (nowExceeded) {
1181
+ updatedCheck.value = false;
1182
+ }
1183
+ this.debug(`Usage limit status changed for flag: ${flagKey}`, {
1184
+ was: wasExceeded ? "exceeded" : "within limits",
1185
+ now: nowExceeded ? "exceeded" : "within limits",
1186
+ featureUsage: updatedCheck.featureUsage,
1187
+ featureAllocation: updatedCheck.featureAllocation,
1188
+ value: updatedCheck.value
1189
+ });
1190
+ }
1191
+ }
1192
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1193
+ this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
1194
+ }
1195
+ const contextStr = contextString(this.context);
1196
+ if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
1197
+ this.checks[contextStr][flagKey] = updatedCheck;
1198
+ }
1199
+ this.notifyFlagCheckListeners(flagKey, updatedCheck);
1200
+ this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1201
+ }
988
1202
  });
989
1203
  };
990
1204
  /**
@@ -1028,8 +1242,13 @@ var Schematic = class {
1028
1242
  sendEvent = async (event) => {
1029
1243
  const captureUrl = `${this.eventUrl}/e`;
1030
1244
  const payload = JSON.stringify(event);
1245
+ this.debug(`sending event:`, { url: captureUrl, event });
1246
+ if (this.isOffline()) {
1247
+ this.debug(`event not sent (offline mode):`, { event });
1248
+ return Promise.resolve();
1249
+ }
1031
1250
  try {
1032
- await fetch(captureUrl, {
1251
+ const response = await fetch(captureUrl, {
1033
1252
  method: "POST",
1034
1253
  headers: {
1035
1254
  ...this.additionalHeaders ?? {},
@@ -1037,6 +1256,10 @@ var Schematic = class {
1037
1256
  },
1038
1257
  body: payload
1039
1258
  });
1259
+ this.debug(`event sent:`, {
1260
+ status: response.status,
1261
+ statusText: response.statusText
1262
+ });
1040
1263
  } catch (error) {
1041
1264
  console.error("Error sending Schematic event: ", error);
1042
1265
  }
@@ -1051,8 +1274,13 @@ var Schematic = class {
1051
1274
  */
1052
1275
  /**
1053
1276
  * If using websocket mode, close the connection when done.
1277
+ * In offline mode, this is a no-op.
1054
1278
  */
1055
1279
  cleanup = async () => {
1280
+ if (this.isOffline()) {
1281
+ this.debug("cleanup: skipped (offline mode)");
1282
+ return Promise.resolve();
1283
+ }
1056
1284
  if (this.conn) {
1057
1285
  try {
1058
1286
  const socket = await this.conn;
@@ -1066,16 +1294,26 @@ var Schematic = class {
1066
1294
  };
1067
1295
  // Open a websocket connection
1068
1296
  wsConnect = () => {
1297
+ if (this.isOffline()) {
1298
+ this.debug("wsConnect: skipped (offline mode)");
1299
+ return Promise.reject(
1300
+ new Error("WebSocket connection skipped in offline mode")
1301
+ );
1302
+ }
1069
1303
  return new Promise((resolve, reject) => {
1070
1304
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1305
+ this.debug(`connecting to WebSocket:`, wsUrl);
1071
1306
  const webSocket = new WebSocket(wsUrl);
1072
1307
  webSocket.onopen = () => {
1308
+ this.debug(`WebSocket connection opened`);
1073
1309
  resolve(webSocket);
1074
1310
  };
1075
1311
  webSocket.onerror = (error) => {
1312
+ this.debug(`WebSocket connection error:`, error);
1076
1313
  reject(error);
1077
1314
  };
1078
1315
  webSocket.onclose = () => {
1316
+ this.debug(`WebSocket connection closed`);
1079
1317
  this.conn = null;
1080
1318
  };
1081
1319
  });
@@ -1083,21 +1321,44 @@ var Schematic = class {
1083
1321
  // Send a message on the websocket indicating interest in a particular evaluation context
1084
1322
  // and wait for the initial set of flag values to be returned
1085
1323
  wsSendMessage = (socket, context) => {
1324
+ if (this.isOffline()) {
1325
+ this.debug("wsSendMessage: skipped (offline mode)");
1326
+ this.setIsPending(false);
1327
+ return Promise.resolve();
1328
+ }
1086
1329
  return new Promise((resolve, reject) => {
1087
1330
  if (contextString(context) == contextString(this.context)) {
1331
+ this.debug(`WebSocket context unchanged, skipping update`);
1088
1332
  return resolve(this.setIsPending(false));
1089
1333
  }
1334
+ this.debug(`WebSocket context updated:`, context);
1090
1335
  this.context = context;
1091
1336
  const sendMessage = () => {
1092
1337
  let resolved = false;
1093
1338
  const messageHandler = (event) => {
1094
1339
  const message = JSON.parse(event.data);
1340
+ this.debug(`WebSocket message received:`, message);
1095
1341
  if (!(contextString(context) in this.checks)) {
1096
1342
  this.checks[contextString(context)] = {};
1097
1343
  }
1098
1344
  (message.flags ?? []).forEach((flag) => {
1099
1345
  const flagCheck = CheckFlagReturnFromJSON(flag);
1100
- this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1346
+ const contextStr = contextString(context);
1347
+ if (this.checks[contextStr] === void 0) {
1348
+ this.checks[contextStr] = {};
1349
+ }
1350
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1351
+ this.debug(`WebSocket flag update:`, {
1352
+ flag: flagCheck.flag,
1353
+ value: flagCheck.value,
1354
+ flagCheck
1355
+ });
1356
+ if (typeof flagCheck.featureUsageEvent === "string") {
1357
+ this.updateFeatureUsageEventMap(flagCheck);
1358
+ }
1359
+ if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1360
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1361
+ }
1101
1362
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1102
1363
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1103
1364
  });
@@ -1108,19 +1369,23 @@ var Schematic = class {
1108
1369
  }
1109
1370
  };
1110
1371
  socket.addEventListener("message", messageHandler);
1111
- socket.send(
1112
- JSON.stringify({
1113
- apiKey: this.apiKey,
1114
- clientVersion: `schematic-js@${version}`,
1115
- data: context
1116
- })
1117
- );
1372
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1373
+ const messagePayload = {
1374
+ apiKey: this.apiKey,
1375
+ clientVersion,
1376
+ data: context
1377
+ };
1378
+ this.debug(`WebSocket sending message:`, messagePayload);
1379
+ socket.send(JSON.stringify(messagePayload));
1118
1380
  };
1119
1381
  if (socket.readyState === WebSocket.OPEN) {
1382
+ this.debug(`WebSocket already open, sending message`);
1120
1383
  sendMessage();
1121
1384
  } else if (socket.readyState === WebSocket.CONNECTING) {
1385
+ this.debug(`WebSocket connecting, waiting for open to send message`);
1122
1386
  socket.addEventListener("open", sendMessage);
1123
1387
  } else {
1388
+ this.debug(`WebSocket is closed, cannot send message`);
1124
1389
  reject("WebSocket is not open or connecting");
1125
1390
  }
1126
1391
  });
@@ -1177,10 +1442,40 @@ var Schematic = class {
1177
1442
  };
1178
1443
  notifyFlagCheckListeners = (flagKey, check) => {
1179
1444
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1445
+ if (listeners.size > 0) {
1446
+ this.debug(
1447
+ `Notifying ${listeners.size} flag check listeners for ${flagKey}`,
1448
+ check
1449
+ );
1450
+ }
1451
+ if (typeof check.featureUsageEvent === "string") {
1452
+ this.updateFeatureUsageEventMap(check);
1453
+ }
1180
1454
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1181
1455
  };
1456
+ /** Add or update a CheckFlagReturn in the featureUsageEventMap */
1457
+ updateFeatureUsageEventMap = (check) => {
1458
+ if (typeof check.featureUsageEvent !== "string") return;
1459
+ const eventName = check.featureUsageEvent;
1460
+ if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
1461
+ this.featureUsageEventMap[eventName] = {};
1462
+ }
1463
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1464
+ this.featureUsageEventMap[eventName][check.flag] = check;
1465
+ }
1466
+ this.debug(
1467
+ `Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
1468
+ check
1469
+ );
1470
+ };
1182
1471
  notifyFlagValueListeners = (flagKey, value) => {
1183
1472
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1473
+ if (listeners.size > 0) {
1474
+ this.debug(
1475
+ `Notifying ${listeners.size} flag value listeners for ${flagKey}`,
1476
+ { value }
1477
+ );
1478
+ }
1184
1479
  listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1185
1480
  };
1186
1481
  };
@@ -1210,7 +1505,7 @@ var notifyFlagValueListener = (listener, value) => {
1210
1505
  var import_react = __toESM(require("react"));
1211
1506
 
1212
1507
  // src/version.ts
1213
- var version2 = "1.2.3";
1508
+ var version2 = "1.2.5";
1214
1509
 
1215
1510
  // src/context/schematic.tsx
1216
1511
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -615,6 +615,7 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
615
615
  error: json["error"] == null ? void 0 : json["error"],
616
616
  featureAllocation: json["feature_allocation"] == null ? void 0 : json["feature_allocation"],
617
617
  featureUsage: json["feature_usage"] == null ? void 0 : json["feature_usage"],
618
+ featureUsageEvent: json["feature_usage_event"] == null ? void 0 : json["feature_usage_event"],
618
619
  featureUsagePeriod: json["feature_usage_period"] == null ? void 0 : json["feature_usage_period"],
619
620
  featureUsageResetAt: json["feature_usage_reset_at"] == null ? void 0 : new Date(json["feature_usage_reset_at"]),
620
621
  flag: json["flag"],
@@ -626,6 +627,26 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
626
627
  value: json["value"]
627
628
  };
628
629
  }
630
+ function EventBodyFlagCheckToJSON(json) {
631
+ return EventBodyFlagCheckToJSONTyped(json, false);
632
+ }
633
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
634
+ if (value == null) {
635
+ return value;
636
+ }
637
+ return {
638
+ company_id: value["companyId"],
639
+ error: value["error"],
640
+ flag_id: value["flagId"],
641
+ flag_key: value["flagKey"],
642
+ reason: value["reason"],
643
+ req_company: value["reqCompany"],
644
+ req_user: value["reqUser"],
645
+ rule_id: value["ruleId"],
646
+ user_id: value["userId"],
647
+ value: value["value"]
648
+ };
649
+ }
629
650
  function CheckFlagResponseFromJSON(json) {
630
651
  return CheckFlagResponseFromJSONTyped(json, false);
631
652
  }
@@ -684,6 +705,7 @@ var CheckFlagReturnFromJSON = (json) => {
684
705
  error,
685
706
  featureAllocation,
686
707
  featureUsage,
708
+ featureUsageEvent,
687
709
  featureUsagePeriod,
688
710
  featureUsageResetAt,
689
711
  flag,
@@ -703,6 +725,7 @@ var CheckFlagReturnFromJSON = (json) => {
703
725
  error: error == null ? void 0 : error,
704
726
  featureAllocation: featureAllocation == null ? void 0 : featureAllocation,
705
727
  featureUsage: featureUsage == null ? void 0 : featureUsage,
728
+ featureUsageEvent: featureUsageEvent === null ? void 0 : featureUsageEvent,
706
729
  featureUsagePeriod: featureUsagePeriod == null ? void 0 : featureUsagePeriod,
707
730
  featureUsageResetAt: featureUsageResetAt == null ? void 0 : featureUsageResetAt,
708
731
  flag,
@@ -728,7 +751,7 @@ function contextString(context) {
728
751
  }, {});
729
752
  return JSON.stringify(sortedContext);
730
753
  }
731
- var version = "1.2.1";
754
+ var version = "1.2.3";
732
755
  var anonymousIdKey = "schematicId";
733
756
  var Schematic = class {
734
757
  additionalHeaders = {};
@@ -736,6 +759,8 @@ var Schematic = class {
736
759
  apiUrl = "https://api.schematichq.com";
737
760
  conn = null;
738
761
  context = {};
762
+ debugEnabled = false;
763
+ offlineEnabled = false;
739
764
  eventQueue;
740
765
  eventUrl = "https://c.schematichq.com";
741
766
  flagCheckListeners = {};
@@ -745,11 +770,32 @@ var Schematic = class {
745
770
  storage;
746
771
  useWebSocket = false;
747
772
  checks = {};
773
+ featureUsageEventMap = {};
748
774
  webSocketUrl = "wss://api.schematichq.com";
749
775
  constructor(apiKey, options) {
750
776
  this.apiKey = apiKey;
751
777
  this.eventQueue = [];
752
778
  this.useWebSocket = options?.useWebSocket ?? false;
779
+ this.debugEnabled = options?.debug ?? false;
780
+ this.offlineEnabled = options?.offline ?? false;
781
+ if (typeof window !== "undefined" && typeof window.location !== "undefined") {
782
+ const params = new URLSearchParams(window.location.search);
783
+ const debugParam = params.get("schematic_debug");
784
+ if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
785
+ this.debugEnabled = true;
786
+ }
787
+ const offlineParam = params.get("schematic_offline");
788
+ if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
789
+ this.offlineEnabled = true;
790
+ this.debugEnabled = true;
791
+ }
792
+ }
793
+ if (this.offlineEnabled && options?.debug !== false) {
794
+ this.debugEnabled = true;
795
+ }
796
+ if (this.offlineEnabled) {
797
+ this.setIsPending(false);
798
+ }
753
799
  this.additionalHeaders = {
754
800
  "X-Schematic-Client-Version": `schematic-js@${version}`,
755
801
  ...options?.additionalHeaders ?? {}
@@ -773,6 +819,13 @@ var Schematic = class {
773
819
  this.flushEventQueue();
774
820
  });
775
821
  }
822
+ if (this.offlineEnabled) {
823
+ this.debug(
824
+ "Initialized with offline mode enabled - no network requests will be made"
825
+ );
826
+ } else if (this.debugEnabled) {
827
+ this.debug("Initialized with debug mode enabled");
828
+ }
776
829
  }
777
830
  /**
778
831
  * Get value for a single flag.
@@ -785,6 +838,14 @@ var Schematic = class {
785
838
  const { fallback = false, key } = options;
786
839
  const context = options.context || this.context;
787
840
  const contextStr = contextString(context);
841
+ this.debug(`checkFlag: ${key}`, { context, fallback });
842
+ if (this.isOffline()) {
843
+ this.debug(`checkFlag offline result: ${key}`, {
844
+ value: fallback,
845
+ offlineMode: true
846
+ });
847
+ return fallback;
848
+ }
788
849
  if (!this.useWebSocket) {
789
850
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
790
851
  return fetch(requestUrl, {
@@ -801,17 +862,35 @@ var Schematic = class {
801
862
  }
802
863
  return response.json();
803
864
  }).then((response) => {
804
- return CheckFlagResponseFromJSON(response).data.value;
865
+ const parsedResponse = CheckFlagResponseFromJSON(response);
866
+ this.debug(`checkFlag result: ${key}`, parsedResponse);
867
+ const result = CheckFlagReturnFromJSON(parsedResponse.data);
868
+ if (typeof result.featureUsageEvent === "string") {
869
+ this.updateFeatureUsageEventMap(result);
870
+ }
871
+ this.submitFlagCheckEvent(key, result, context);
872
+ return result.value;
805
873
  }).catch((error) => {
806
874
  console.error("There was a problem with the fetch operation:", error);
875
+ const errorResult = {
876
+ flag: key,
877
+ value: fallback,
878
+ reason: "API request failed",
879
+ error: error instanceof Error ? error.message : String(error)
880
+ };
881
+ this.submitFlagCheckEvent(key, errorResult, context);
807
882
  return fallback;
808
883
  });
809
884
  }
810
885
  try {
811
886
  const existingVals = this.checks[contextStr];
812
- if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
887
+ if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
888
+ this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
813
889
  return existingVals[key].value;
814
890
  }
891
+ if (this.isOffline()) {
892
+ return fallback;
893
+ }
815
894
  try {
816
895
  await this.setContext(context);
817
896
  } catch (error) {
@@ -822,16 +901,74 @@ var Schematic = class {
822
901
  return this.fallbackToRest(key, context, fallback);
823
902
  }
824
903
  const contextVals = this.checks[contextStr] ?? {};
825
- return contextVals[key]?.value ?? fallback;
904
+ const flagCheck = contextVals[key];
905
+ const result = flagCheck?.value ?? fallback;
906
+ this.debug(
907
+ `checkFlag WebSocket result: ${key}`,
908
+ typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
909
+ );
910
+ if (typeof flagCheck !== "undefined") {
911
+ this.submitFlagCheckEvent(key, flagCheck, context);
912
+ }
913
+ return result;
826
914
  } catch (error) {
827
915
  console.error("Unexpected error in checkFlag:", error);
916
+ const errorResult = {
917
+ flag: key,
918
+ value: fallback,
919
+ reason: "Unexpected error in flag check",
920
+ error: error instanceof Error ? error.message : String(error)
921
+ };
922
+ this.submitFlagCheckEvent(key, errorResult, context);
828
923
  return fallback;
829
924
  }
830
925
  }
926
+ /**
927
+ * Helper function to log debug messages
928
+ * Only logs if debug mode is enabled
929
+ */
930
+ debug(message, ...args) {
931
+ if (this.debugEnabled) {
932
+ console.log(`[Schematic] ${message}`, ...args);
933
+ }
934
+ }
935
+ /**
936
+ * Helper function to check if client is in offline mode
937
+ */
938
+ isOffline() {
939
+ return this.offlineEnabled;
940
+ }
941
+ /**
942
+ * Submit a flag check event
943
+ * Records data about a flag check for analytics
944
+ */
945
+ submitFlagCheckEvent(flagKey, result, context) {
946
+ const eventBody = {
947
+ flagKey,
948
+ value: result.value,
949
+ reason: result.reason,
950
+ flagId: result.flagId,
951
+ ruleId: result.ruleId,
952
+ companyId: result.companyId,
953
+ userId: result.userId,
954
+ error: result.error,
955
+ reqCompany: context.company,
956
+ reqUser: context.user
957
+ };
958
+ this.debug(`submitting flag check event:`, eventBody);
959
+ return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
960
+ }
831
961
  /**
832
962
  * Helper method for falling back to REST API when WebSocket connection fails
833
963
  */
834
964
  async fallbackToRest(key, context, fallback) {
965
+ if (this.isOffline()) {
966
+ this.debug(`fallbackToRest offline result: ${key}`, {
967
+ value: fallback,
968
+ offlineMode: true
969
+ });
970
+ return fallback;
971
+ }
835
972
  try {
836
973
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
837
974
  const response = await fetch(requestUrl, {
@@ -846,19 +983,39 @@ var Schematic = class {
846
983
  if (!response.ok) {
847
984
  throw new Error("Network response was not ok");
848
985
  }
849
- const data = CheckFlagResponseFromJSON(await response.json());
850
- return data?.data?.value ?? false;
986
+ const responseJson = await response.json();
987
+ const data = CheckFlagResponseFromJSON(responseJson);
988
+ this.debug(`fallbackToRest result: ${key}`, data);
989
+ const result = CheckFlagReturnFromJSON(data.data);
990
+ if (typeof result.featureUsageEvent === "string") {
991
+ this.updateFeatureUsageEventMap(result);
992
+ }
993
+ this.submitFlagCheckEvent(key, result, context);
994
+ return result.value;
851
995
  } catch (error) {
852
996
  console.error("REST API call failed, using fallback value:", error);
997
+ const errorResult = {
998
+ flag: key,
999
+ value: fallback,
1000
+ reason: "API request failed (fallback)",
1001
+ error: error instanceof Error ? error.message : String(error)
1002
+ };
1003
+ this.submitFlagCheckEvent(key, errorResult, context);
853
1004
  return fallback;
854
1005
  }
855
1006
  }
856
1007
  /**
857
1008
  * Make an API call to fetch all flag values for a given context.
858
1009
  * Recommended for use in REST mode only.
1010
+ * In offline mode, returns an empty object.
859
1011
  */
860
1012
  checkFlags = async (context) => {
861
1013
  context = context || this.context;
1014
+ this.debug(`checkFlags`, { context });
1015
+ if (this.isOffline()) {
1016
+ this.debug(`checkFlags offline result: returning empty object`);
1017
+ return {};
1018
+ }
862
1019
  const requestUrl = `${this.apiUrl}/flags/check`;
863
1020
  const requestBody = JSON.stringify(context);
864
1021
  return fetch(requestUrl, {
@@ -876,6 +1033,7 @@ var Schematic = class {
876
1033
  return response.json();
877
1034
  }).then((responseJson) => {
878
1035
  const resp = CheckFlagsResponseFromJSON(responseJson);
1036
+ this.debug(`checkFlags result:`, resp);
879
1037
  return (resp?.data?.flags ?? []).reduce(
880
1038
  (accum, flag) => {
881
1039
  accum[flag.flag] = flag.value;
@@ -894,6 +1052,7 @@ var Schematic = class {
894
1052
  * send an identify event to the Schematic API which will upsert a user and company.
895
1053
  */
896
1054
  identify = (body) => {
1055
+ this.debug(`identify:`, body);
897
1056
  try {
898
1057
  this.setContext({
899
1058
  company: body.company?.keys,
@@ -911,10 +1070,12 @@ var Schematic = class {
911
1070
  * 2. Send the context to the server
912
1071
  * 3. Wait for initial flag values to be returned
913
1072
  * The promise resolves when initial flag values are received.
1073
+ * In offline mode, this will just set the context locally without connecting.
914
1074
  */
915
1075
  setContext = async (context) => {
916
- if (!this.useWebSocket) {
1076
+ if (this.isOffline() || !this.useWebSocket) {
917
1077
  this.context = context;
1078
+ this.setIsPending(false);
918
1079
  return Promise.resolve();
919
1080
  }
920
1081
  try {
@@ -932,14 +1093,67 @@ var Schematic = class {
932
1093
  /**
933
1094
  * Send a track event
934
1095
  * Track usage for a company and/or user.
1096
+ * Optimistically updates feature usage flags if tracking a featureUsageEvent.
935
1097
  */
936
1098
  track = (body) => {
937
- const { company, user, event, traits } = body;
938
- return this.handleEvent("track", {
1099
+ const { company, user, event, traits, quantity = 1 } = body;
1100
+ const trackData = {
939
1101
  company: company ?? this.context.company,
940
1102
  event,
941
1103
  traits: traits ?? {},
942
- user: user ?? this.context.user
1104
+ user: user ?? this.context.user,
1105
+ quantity
1106
+ };
1107
+ this.debug(`track:`, trackData);
1108
+ if (event in this.featureUsageEventMap) {
1109
+ this.optimisticallyUpdateFeatureUsage(event, quantity);
1110
+ }
1111
+ return this.handleEvent("track", trackData);
1112
+ };
1113
+ /**
1114
+ * Optimistically update feature usage flags associated with a tracked event
1115
+ * This updates flags in memory with updated usage counts and value/featureUsageExceeded flags
1116
+ * before the network request completes
1117
+ */
1118
+ optimisticallyUpdateFeatureUsage = (eventName, quantity = 1) => {
1119
+ const flagsForEvent = this.featureUsageEventMap[eventName];
1120
+ if (flagsForEvent === void 0 || flagsForEvent === null) return;
1121
+ this.debug(
1122
+ `Optimistically updating feature usage for event: ${eventName}`,
1123
+ { quantity }
1124
+ );
1125
+ Object.entries(flagsForEvent).forEach(([flagKey, check]) => {
1126
+ if (check === void 0) return;
1127
+ const updatedCheck = { ...check };
1128
+ if (typeof updatedCheck.featureUsage === "number") {
1129
+ updatedCheck.featureUsage += quantity;
1130
+ if (typeof updatedCheck.featureAllocation === "number") {
1131
+ const wasExceeded = updatedCheck.featureUsageExceeded === true;
1132
+ const nowExceeded = updatedCheck.featureUsage >= updatedCheck.featureAllocation;
1133
+ if (nowExceeded !== wasExceeded) {
1134
+ updatedCheck.featureUsageExceeded = nowExceeded;
1135
+ if (nowExceeded) {
1136
+ updatedCheck.value = false;
1137
+ }
1138
+ this.debug(`Usage limit status changed for flag: ${flagKey}`, {
1139
+ was: wasExceeded ? "exceeded" : "within limits",
1140
+ now: nowExceeded ? "exceeded" : "within limits",
1141
+ featureUsage: updatedCheck.featureUsage,
1142
+ featureAllocation: updatedCheck.featureAllocation,
1143
+ value: updatedCheck.value
1144
+ });
1145
+ }
1146
+ }
1147
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1148
+ this.featureUsageEventMap[eventName][flagKey] = updatedCheck;
1149
+ }
1150
+ const contextStr = contextString(this.context);
1151
+ if (this.checks[contextStr] !== void 0 && this.checks[contextStr] !== null) {
1152
+ this.checks[contextStr][flagKey] = updatedCheck;
1153
+ }
1154
+ this.notifyFlagCheckListeners(flagKey, updatedCheck);
1155
+ this.notifyFlagValueListeners(flagKey, updatedCheck.value);
1156
+ }
943
1157
  });
944
1158
  };
945
1159
  /**
@@ -983,8 +1197,13 @@ var Schematic = class {
983
1197
  sendEvent = async (event) => {
984
1198
  const captureUrl = `${this.eventUrl}/e`;
985
1199
  const payload = JSON.stringify(event);
1200
+ this.debug(`sending event:`, { url: captureUrl, event });
1201
+ if (this.isOffline()) {
1202
+ this.debug(`event not sent (offline mode):`, { event });
1203
+ return Promise.resolve();
1204
+ }
986
1205
  try {
987
- await fetch(captureUrl, {
1206
+ const response = await fetch(captureUrl, {
988
1207
  method: "POST",
989
1208
  headers: {
990
1209
  ...this.additionalHeaders ?? {},
@@ -992,6 +1211,10 @@ var Schematic = class {
992
1211
  },
993
1212
  body: payload
994
1213
  });
1214
+ this.debug(`event sent:`, {
1215
+ status: response.status,
1216
+ statusText: response.statusText
1217
+ });
995
1218
  } catch (error) {
996
1219
  console.error("Error sending Schematic event: ", error);
997
1220
  }
@@ -1006,8 +1229,13 @@ var Schematic = class {
1006
1229
  */
1007
1230
  /**
1008
1231
  * If using websocket mode, close the connection when done.
1232
+ * In offline mode, this is a no-op.
1009
1233
  */
1010
1234
  cleanup = async () => {
1235
+ if (this.isOffline()) {
1236
+ this.debug("cleanup: skipped (offline mode)");
1237
+ return Promise.resolve();
1238
+ }
1011
1239
  if (this.conn) {
1012
1240
  try {
1013
1241
  const socket = await this.conn;
@@ -1021,16 +1249,26 @@ var Schematic = class {
1021
1249
  };
1022
1250
  // Open a websocket connection
1023
1251
  wsConnect = () => {
1252
+ if (this.isOffline()) {
1253
+ this.debug("wsConnect: skipped (offline mode)");
1254
+ return Promise.reject(
1255
+ new Error("WebSocket connection skipped in offline mode")
1256
+ );
1257
+ }
1024
1258
  return new Promise((resolve, reject) => {
1025
1259
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1260
+ this.debug(`connecting to WebSocket:`, wsUrl);
1026
1261
  const webSocket = new WebSocket(wsUrl);
1027
1262
  webSocket.onopen = () => {
1263
+ this.debug(`WebSocket connection opened`);
1028
1264
  resolve(webSocket);
1029
1265
  };
1030
1266
  webSocket.onerror = (error) => {
1267
+ this.debug(`WebSocket connection error:`, error);
1031
1268
  reject(error);
1032
1269
  };
1033
1270
  webSocket.onclose = () => {
1271
+ this.debug(`WebSocket connection closed`);
1034
1272
  this.conn = null;
1035
1273
  };
1036
1274
  });
@@ -1038,21 +1276,44 @@ var Schematic = class {
1038
1276
  // Send a message on the websocket indicating interest in a particular evaluation context
1039
1277
  // and wait for the initial set of flag values to be returned
1040
1278
  wsSendMessage = (socket, context) => {
1279
+ if (this.isOffline()) {
1280
+ this.debug("wsSendMessage: skipped (offline mode)");
1281
+ this.setIsPending(false);
1282
+ return Promise.resolve();
1283
+ }
1041
1284
  return new Promise((resolve, reject) => {
1042
1285
  if (contextString(context) == contextString(this.context)) {
1286
+ this.debug(`WebSocket context unchanged, skipping update`);
1043
1287
  return resolve(this.setIsPending(false));
1044
1288
  }
1289
+ this.debug(`WebSocket context updated:`, context);
1045
1290
  this.context = context;
1046
1291
  const sendMessage = () => {
1047
1292
  let resolved = false;
1048
1293
  const messageHandler = (event) => {
1049
1294
  const message = JSON.parse(event.data);
1295
+ this.debug(`WebSocket message received:`, message);
1050
1296
  if (!(contextString(context) in this.checks)) {
1051
1297
  this.checks[contextString(context)] = {};
1052
1298
  }
1053
1299
  (message.flags ?? []).forEach((flag) => {
1054
1300
  const flagCheck = CheckFlagReturnFromJSON(flag);
1055
- this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1301
+ const contextStr = contextString(context);
1302
+ if (this.checks[contextStr] === void 0) {
1303
+ this.checks[contextStr] = {};
1304
+ }
1305
+ this.checks[contextStr][flagCheck.flag] = flagCheck;
1306
+ this.debug(`WebSocket flag update:`, {
1307
+ flag: flagCheck.flag,
1308
+ value: flagCheck.value,
1309
+ flagCheck
1310
+ });
1311
+ if (typeof flagCheck.featureUsageEvent === "string") {
1312
+ this.updateFeatureUsageEventMap(flagCheck);
1313
+ }
1314
+ if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1315
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1316
+ }
1056
1317
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1057
1318
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1058
1319
  });
@@ -1063,19 +1324,23 @@ var Schematic = class {
1063
1324
  }
1064
1325
  };
1065
1326
  socket.addEventListener("message", messageHandler);
1066
- socket.send(
1067
- JSON.stringify({
1068
- apiKey: this.apiKey,
1069
- clientVersion: `schematic-js@${version}`,
1070
- data: context
1071
- })
1072
- );
1327
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1328
+ const messagePayload = {
1329
+ apiKey: this.apiKey,
1330
+ clientVersion,
1331
+ data: context
1332
+ };
1333
+ this.debug(`WebSocket sending message:`, messagePayload);
1334
+ socket.send(JSON.stringify(messagePayload));
1073
1335
  };
1074
1336
  if (socket.readyState === WebSocket.OPEN) {
1337
+ this.debug(`WebSocket already open, sending message`);
1075
1338
  sendMessage();
1076
1339
  } else if (socket.readyState === WebSocket.CONNECTING) {
1340
+ this.debug(`WebSocket connecting, waiting for open to send message`);
1077
1341
  socket.addEventListener("open", sendMessage);
1078
1342
  } else {
1343
+ this.debug(`WebSocket is closed, cannot send message`);
1079
1344
  reject("WebSocket is not open or connecting");
1080
1345
  }
1081
1346
  });
@@ -1132,10 +1397,40 @@ var Schematic = class {
1132
1397
  };
1133
1398
  notifyFlagCheckListeners = (flagKey, check) => {
1134
1399
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1400
+ if (listeners.size > 0) {
1401
+ this.debug(
1402
+ `Notifying ${listeners.size} flag check listeners for ${flagKey}`,
1403
+ check
1404
+ );
1405
+ }
1406
+ if (typeof check.featureUsageEvent === "string") {
1407
+ this.updateFeatureUsageEventMap(check);
1408
+ }
1135
1409
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1136
1410
  };
1411
+ /** Add or update a CheckFlagReturn in the featureUsageEventMap */
1412
+ updateFeatureUsageEventMap = (check) => {
1413
+ if (typeof check.featureUsageEvent !== "string") return;
1414
+ const eventName = check.featureUsageEvent;
1415
+ if (this.featureUsageEventMap[eventName] === void 0 || this.featureUsageEventMap[eventName] === null) {
1416
+ this.featureUsageEventMap[eventName] = {};
1417
+ }
1418
+ if (this.featureUsageEventMap[eventName] !== void 0) {
1419
+ this.featureUsageEventMap[eventName][check.flag] = check;
1420
+ }
1421
+ this.debug(
1422
+ `Updated featureUsageEventMap for event: ${eventName}, flag: ${check.flag}`,
1423
+ check
1424
+ );
1425
+ };
1137
1426
  notifyFlagValueListeners = (flagKey, value) => {
1138
1427
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1428
+ if (listeners.size > 0) {
1429
+ this.debug(
1430
+ `Notifying ${listeners.size} flag value listeners for ${flagKey}`,
1431
+ { value }
1432
+ );
1433
+ }
1139
1434
  listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1140
1435
  };
1141
1436
  };
@@ -1165,7 +1460,7 @@ var notifyFlagValueListener = (listener, value) => {
1165
1460
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1166
1461
 
1167
1462
  // src/version.ts
1168
- var version2 = "1.2.3";
1463
+ var version2 = "1.2.5";
1169
1464
 
1170
1465
  // src/context/schematic.tsx
1171
1466
  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.3",
3
+ "version": "1.2.5",
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,34 @@
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
+ "tsc": "npx tsc",
29
+ "prepare": "husky"
29
30
  },
30
31
  "dependencies": {
31
- "@schematichq/schematic-js": "^1.2.1"
32
+ "@schematichq/schematic-js": "^1.2.3"
32
33
  },
33
34
  "devDependencies": {
34
- "@microsoft/api-extractor": "^7.49.2",
35
+ "@eslint/js": "^9.24.0",
36
+ "@microsoft/api-extractor": "^7.52.2",
35
37
  "@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.24.2",
38
+ "@types/react": "^19.1.1",
39
+ "esbuild": "^0.25.2",
40
40
  "esbuild-jest": "^0.5.0",
41
- "eslint": "^8.57.1",
41
+ "eslint": "^9.24.0",
42
42
  "eslint-plugin-import": "^2.31.0",
43
- "eslint-plugin-react": "^7.37.4",
43
+ "eslint-plugin-react": "^7.37.5",
44
44
  "eslint-plugin-react-hooks": "^5.1.0",
45
+ "globals": "^16.0.0",
46
+ "husky": "^9.1.7",
45
47
  "jest": "^29.7.0",
46
48
  "jest-environment-jsdom": "^29.7.0",
47
49
  "jest-esbuild": "^0.3.0",
48
50
  "jest-fetch-mock": "^3.0.3",
49
51
  "prettier": "^3.4.2",
50
- "react": "^19.0.0",
51
- "ts-jest": "^29.2.5",
52
- "typescript": "^5.7.3"
52
+ "react": "^19.1.0",
53
+ "ts-jest": "^29.3.0",
54
+ "typescript": "^5.7.3",
55
+ "typescript-eslint": "^8.29.1"
53
56
  },
54
57
  "peerDependencies": {
55
58
  "react": ">=18"