@schematichq/schematic-react 1.2.3 → 1.2.4

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
@@ -126,6 +126,50 @@ const MyComponent = () => {
126
126
  };
127
127
  ```
128
128
 
129
+ ## Troubleshooting
130
+
131
+ For debugging and development, Schematic supports two special modes:
132
+
133
+ ### Debug Mode
134
+
135
+ Enables console logging of all Schematic operations:
136
+
137
+ ```typescript
138
+ // Enable at initialization
139
+ import { SchematicProvider } from "@schematichq/schematic-react";
140
+
141
+ ReactDOM.render(
142
+ <SchematicProvider publishableKey="your-publishable-key" debug={true}>
143
+ <App />
144
+ </SchematicProvider>,
145
+ document.getElementById("root"),
146
+ );
147
+
148
+ // Or via URL parameter
149
+ // https://yoursite.com/?schematic_debug=true
150
+ ```
151
+
152
+ ### Offline Mode
153
+
154
+ Prevents network requests and returns fallback values for all flag checks:
155
+
156
+ ```typescript
157
+ // Enable at initialization
158
+ import { SchematicProvider } from "@schematichq/schematic-react";
159
+
160
+ ReactDOM.render(
161
+ <SchematicProvider publishableKey="your-publishable-key" offline={true}>
162
+ <App />
163
+ </SchematicProvider>,
164
+ document.getElementById("root"),
165
+ );
166
+
167
+ // Or via URL parameter
168
+ // https://yoursite.com/?schematic_offline=true
169
+ ```
170
+
171
+ Offline mode automatically enables debug mode to help with troubleshooting.
172
+
129
173
  ## License
130
174
 
131
175
  MIT
@@ -671,6 +671,26 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
671
671
  value: json["value"]
672
672
  };
673
673
  }
674
+ function EventBodyFlagCheckToJSON(json) {
675
+ return EventBodyFlagCheckToJSONTyped(json, false);
676
+ }
677
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
678
+ if (value == null) {
679
+ return value;
680
+ }
681
+ return {
682
+ company_id: value["companyId"],
683
+ error: value["error"],
684
+ flag_id: value["flagId"],
685
+ flag_key: value["flagKey"],
686
+ reason: value["reason"],
687
+ req_company: value["reqCompany"],
688
+ req_user: value["reqUser"],
689
+ rule_id: value["ruleId"],
690
+ user_id: value["userId"],
691
+ value: value["value"]
692
+ };
693
+ }
674
694
  function CheckFlagResponseFromJSON(json) {
675
695
  return CheckFlagResponseFromJSONTyped(json, false);
676
696
  }
@@ -773,7 +793,7 @@ function contextString(context) {
773
793
  }, {});
774
794
  return JSON.stringify(sortedContext);
775
795
  }
776
- var version = "1.2.1";
796
+ var version = "1.2.2";
777
797
  var anonymousIdKey = "schematicId";
778
798
  var Schematic = class {
779
799
  additionalHeaders = {};
@@ -781,6 +801,8 @@ var Schematic = class {
781
801
  apiUrl = "https://api.schematichq.com";
782
802
  conn = null;
783
803
  context = {};
804
+ debugEnabled = false;
805
+ offlineEnabled = false;
784
806
  eventQueue;
785
807
  eventUrl = "https://c.schematichq.com";
786
808
  flagCheckListeners = {};
@@ -795,6 +817,26 @@ var Schematic = class {
795
817
  this.apiKey = apiKey;
796
818
  this.eventQueue = [];
797
819
  this.useWebSocket = options?.useWebSocket ?? false;
820
+ this.debugEnabled = options?.debug ?? false;
821
+ this.offlineEnabled = options?.offline ?? false;
822
+ if (typeof window !== "undefined" && typeof window.location !== "undefined") {
823
+ const params = new URLSearchParams(window.location.search);
824
+ const debugParam = params.get("schematic_debug");
825
+ if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
826
+ this.debugEnabled = true;
827
+ }
828
+ const offlineParam = params.get("schematic_offline");
829
+ if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
830
+ this.offlineEnabled = true;
831
+ this.debugEnabled = true;
832
+ }
833
+ }
834
+ if (this.offlineEnabled && options?.debug !== false) {
835
+ this.debugEnabled = true;
836
+ }
837
+ if (this.offlineEnabled) {
838
+ this.setIsPending(false);
839
+ }
798
840
  this.additionalHeaders = {
799
841
  "X-Schematic-Client-Version": `schematic-js@${version}`,
800
842
  ...options?.additionalHeaders ?? {}
@@ -818,6 +860,13 @@ var Schematic = class {
818
860
  this.flushEventQueue();
819
861
  });
820
862
  }
863
+ if (this.offlineEnabled) {
864
+ this.debug(
865
+ "Initialized with offline mode enabled - no network requests will be made"
866
+ );
867
+ } else if (this.debugEnabled) {
868
+ this.debug("Initialized with debug mode enabled");
869
+ }
821
870
  }
822
871
  /**
823
872
  * Get value for a single flag.
@@ -830,6 +879,14 @@ var Schematic = class {
830
879
  const { fallback = false, key } = options;
831
880
  const context = options.context || this.context;
832
881
  const contextStr = contextString(context);
882
+ this.debug(`checkFlag: ${key}`, { context, fallback });
883
+ if (this.isOffline()) {
884
+ this.debug(`checkFlag offline result: ${key}`, {
885
+ value: fallback,
886
+ offlineMode: true
887
+ });
888
+ return fallback;
889
+ }
833
890
  if (!this.useWebSocket) {
834
891
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
835
892
  return fetch(requestUrl, {
@@ -846,17 +903,32 @@ var Schematic = class {
846
903
  }
847
904
  return response.json();
848
905
  }).then((response) => {
849
- return CheckFlagResponseFromJSON(response).data.value;
906
+ const parsedResponse = CheckFlagResponseFromJSON(response);
907
+ this.debug(`checkFlag result: ${key}`, parsedResponse);
908
+ const result = CheckFlagReturnFromJSON(parsedResponse.data);
909
+ this.submitFlagCheckEvent(key, result, context);
910
+ return result.value;
850
911
  }).catch((error) => {
851
912
  console.error("There was a problem with the fetch operation:", error);
913
+ const errorResult = {
914
+ flag: key,
915
+ value: fallback,
916
+ reason: "API request failed",
917
+ error: error instanceof Error ? error.message : String(error)
918
+ };
919
+ this.submitFlagCheckEvent(key, errorResult, context);
852
920
  return fallback;
853
921
  });
854
922
  }
855
923
  try {
856
924
  const existingVals = this.checks[contextStr];
857
- if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
925
+ if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
926
+ this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
858
927
  return existingVals[key].value;
859
928
  }
929
+ if (this.isOffline()) {
930
+ return fallback;
931
+ }
860
932
  try {
861
933
  await this.setContext(context);
862
934
  } catch (error) {
@@ -867,16 +939,74 @@ var Schematic = class {
867
939
  return this.fallbackToRest(key, context, fallback);
868
940
  }
869
941
  const contextVals = this.checks[contextStr] ?? {};
870
- return contextVals[key]?.value ?? fallback;
942
+ const flagCheck = contextVals[key];
943
+ const result = flagCheck?.value ?? fallback;
944
+ this.debug(
945
+ `checkFlag WebSocket result: ${key}`,
946
+ typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
947
+ );
948
+ if (typeof flagCheck !== "undefined") {
949
+ this.submitFlagCheckEvent(key, flagCheck, context);
950
+ }
951
+ return result;
871
952
  } catch (error) {
872
953
  console.error("Unexpected error in checkFlag:", error);
954
+ const errorResult = {
955
+ flag: key,
956
+ value: fallback,
957
+ reason: "Unexpected error in flag check",
958
+ error: error instanceof Error ? error.message : String(error)
959
+ };
960
+ this.submitFlagCheckEvent(key, errorResult, context);
873
961
  return fallback;
874
962
  }
875
963
  }
964
+ /**
965
+ * Helper function to log debug messages
966
+ * Only logs if debug mode is enabled
967
+ */
968
+ debug(message, ...args) {
969
+ if (this.debugEnabled) {
970
+ console.log(`[Schematic] ${message}`, ...args);
971
+ }
972
+ }
973
+ /**
974
+ * Helper function to check if client is in offline mode
975
+ */
976
+ isOffline() {
977
+ return this.offlineEnabled;
978
+ }
979
+ /**
980
+ * Submit a flag check event
981
+ * Records data about a flag check for analytics
982
+ */
983
+ submitFlagCheckEvent(flagKey, result, context) {
984
+ const eventBody = {
985
+ flagKey,
986
+ value: result.value,
987
+ reason: result.reason,
988
+ flagId: result.flagId,
989
+ ruleId: result.ruleId,
990
+ companyId: result.companyId,
991
+ userId: result.userId,
992
+ error: result.error,
993
+ reqCompany: context.company,
994
+ reqUser: context.user
995
+ };
996
+ this.debug(`submitting flag check event:`, eventBody);
997
+ return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
998
+ }
876
999
  /**
877
1000
  * Helper method for falling back to REST API when WebSocket connection fails
878
1001
  */
879
1002
  async fallbackToRest(key, context, fallback) {
1003
+ if (this.isOffline()) {
1004
+ this.debug(`fallbackToRest offline result: ${key}`, {
1005
+ value: fallback,
1006
+ offlineMode: true
1007
+ });
1008
+ return fallback;
1009
+ }
880
1010
  try {
881
1011
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
882
1012
  const response = await fetch(requestUrl, {
@@ -891,19 +1021,36 @@ var Schematic = class {
891
1021
  if (!response.ok) {
892
1022
  throw new Error("Network response was not ok");
893
1023
  }
894
- const data = CheckFlagResponseFromJSON(await response.json());
895
- return data?.data?.value ?? false;
1024
+ const responseJson = await response.json();
1025
+ const data = CheckFlagResponseFromJSON(responseJson);
1026
+ this.debug(`fallbackToRest result: ${key}`, data);
1027
+ const result = CheckFlagReturnFromJSON(data.data);
1028
+ this.submitFlagCheckEvent(key, result, context);
1029
+ return result.value;
896
1030
  } catch (error) {
897
1031
  console.error("REST API call failed, using fallback value:", error);
1032
+ const errorResult = {
1033
+ flag: key,
1034
+ value: fallback,
1035
+ reason: "API request failed (fallback)",
1036
+ error: error instanceof Error ? error.message : String(error)
1037
+ };
1038
+ this.submitFlagCheckEvent(key, errorResult, context);
898
1039
  return fallback;
899
1040
  }
900
1041
  }
901
1042
  /**
902
1043
  * Make an API call to fetch all flag values for a given context.
903
1044
  * Recommended for use in REST mode only.
1045
+ * In offline mode, returns an empty object.
904
1046
  */
905
1047
  checkFlags = async (context) => {
906
1048
  context = context || this.context;
1049
+ this.debug(`checkFlags`, { context });
1050
+ if (this.isOffline()) {
1051
+ this.debug(`checkFlags offline result: returning empty object`);
1052
+ return {};
1053
+ }
907
1054
  const requestUrl = `${this.apiUrl}/flags/check`;
908
1055
  const requestBody = JSON.stringify(context);
909
1056
  return fetch(requestUrl, {
@@ -921,6 +1068,7 @@ var Schematic = class {
921
1068
  return response.json();
922
1069
  }).then((responseJson) => {
923
1070
  const resp = CheckFlagsResponseFromJSON(responseJson);
1071
+ this.debug(`checkFlags result:`, resp);
924
1072
  return (resp?.data?.flags ?? []).reduce(
925
1073
  (accum, flag) => {
926
1074
  accum[flag.flag] = flag.value;
@@ -939,6 +1087,7 @@ var Schematic = class {
939
1087
  * send an identify event to the Schematic API which will upsert a user and company.
940
1088
  */
941
1089
  identify = (body) => {
1090
+ this.debug(`identify:`, body);
942
1091
  try {
943
1092
  this.setContext({
944
1093
  company: body.company?.keys,
@@ -956,10 +1105,15 @@ var Schematic = class {
956
1105
  * 2. Send the context to the server
957
1106
  * 3. Wait for initial flag values to be returned
958
1107
  * The promise resolves when initial flag values are received.
1108
+ * In offline mode, this will just set the context locally without connecting.
959
1109
  */
960
1110
  setContext = async (context) => {
961
- if (!this.useWebSocket) {
1111
+ if (this.isOffline()) {
962
1112
  this.context = context;
1113
+ this.setIsPending(false);
1114
+ return Promise.resolve();
1115
+ }
1116
+ if (!this.useWebSocket) {
963
1117
  return Promise.resolve();
964
1118
  }
965
1119
  try {
@@ -980,12 +1134,14 @@ var Schematic = class {
980
1134
  */
981
1135
  track = (body) => {
982
1136
  const { company, user, event, traits } = body;
983
- return this.handleEvent("track", {
1137
+ const trackData = {
984
1138
  company: company ?? this.context.company,
985
1139
  event,
986
1140
  traits: traits ?? {},
987
1141
  user: user ?? this.context.user
988
- });
1142
+ };
1143
+ this.debug(`track:`, trackData);
1144
+ return this.handleEvent("track", trackData);
989
1145
  };
990
1146
  /**
991
1147
  * Event processing
@@ -1028,8 +1184,13 @@ var Schematic = class {
1028
1184
  sendEvent = async (event) => {
1029
1185
  const captureUrl = `${this.eventUrl}/e`;
1030
1186
  const payload = JSON.stringify(event);
1187
+ this.debug(`sending event:`, { url: captureUrl, event });
1188
+ if (this.isOffline()) {
1189
+ this.debug(`event not sent (offline mode):`, { event });
1190
+ return Promise.resolve();
1191
+ }
1031
1192
  try {
1032
- await fetch(captureUrl, {
1193
+ const response = await fetch(captureUrl, {
1033
1194
  method: "POST",
1034
1195
  headers: {
1035
1196
  ...this.additionalHeaders ?? {},
@@ -1037,6 +1198,10 @@ var Schematic = class {
1037
1198
  },
1038
1199
  body: payload
1039
1200
  });
1201
+ this.debug(`event sent:`, {
1202
+ status: response.status,
1203
+ statusText: response.statusText
1204
+ });
1040
1205
  } catch (error) {
1041
1206
  console.error("Error sending Schematic event: ", error);
1042
1207
  }
@@ -1051,8 +1216,13 @@ var Schematic = class {
1051
1216
  */
1052
1217
  /**
1053
1218
  * If using websocket mode, close the connection when done.
1219
+ * In offline mode, this is a no-op.
1054
1220
  */
1055
1221
  cleanup = async () => {
1222
+ if (this.isOffline()) {
1223
+ this.debug("cleanup: skipped (offline mode)");
1224
+ return Promise.resolve();
1225
+ }
1056
1226
  if (this.conn) {
1057
1227
  try {
1058
1228
  const socket = await this.conn;
@@ -1066,16 +1236,26 @@ var Schematic = class {
1066
1236
  };
1067
1237
  // Open a websocket connection
1068
1238
  wsConnect = () => {
1239
+ if (this.isOffline()) {
1240
+ this.debug("wsConnect: skipped (offline mode)");
1241
+ return Promise.reject(
1242
+ new Error("WebSocket connection skipped in offline mode")
1243
+ );
1244
+ }
1069
1245
  return new Promise((resolve, reject) => {
1070
1246
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1247
+ this.debug(`connecting to WebSocket:`, wsUrl);
1071
1248
  const webSocket = new WebSocket(wsUrl);
1072
1249
  webSocket.onopen = () => {
1250
+ this.debug(`WebSocket connection opened`);
1073
1251
  resolve(webSocket);
1074
1252
  };
1075
1253
  webSocket.onerror = (error) => {
1254
+ this.debug(`WebSocket connection error:`, error);
1076
1255
  reject(error);
1077
1256
  };
1078
1257
  webSocket.onclose = () => {
1258
+ this.debug(`WebSocket connection closed`);
1079
1259
  this.conn = null;
1080
1260
  };
1081
1261
  });
@@ -1083,21 +1263,37 @@ var Schematic = class {
1083
1263
  // Send a message on the websocket indicating interest in a particular evaluation context
1084
1264
  // and wait for the initial set of flag values to be returned
1085
1265
  wsSendMessage = (socket, context) => {
1266
+ if (this.isOffline()) {
1267
+ this.debug("wsSendMessage: skipped (offline mode)");
1268
+ this.setIsPending(false);
1269
+ return Promise.resolve();
1270
+ }
1086
1271
  return new Promise((resolve, reject) => {
1087
1272
  if (contextString(context) == contextString(this.context)) {
1273
+ this.debug(`WebSocket context unchanged, skipping update`);
1088
1274
  return resolve(this.setIsPending(false));
1089
1275
  }
1276
+ this.debug(`WebSocket context updated:`, context);
1090
1277
  this.context = context;
1091
1278
  const sendMessage = () => {
1092
1279
  let resolved = false;
1093
1280
  const messageHandler = (event) => {
1094
1281
  const message = JSON.parse(event.data);
1282
+ this.debug(`WebSocket message received:`, message);
1095
1283
  if (!(contextString(context) in this.checks)) {
1096
1284
  this.checks[contextString(context)] = {};
1097
1285
  }
1098
1286
  (message.flags ?? []).forEach((flag) => {
1099
1287
  const flagCheck = CheckFlagReturnFromJSON(flag);
1100
1288
  this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1289
+ this.debug(`WebSocket flag update:`, {
1290
+ flag: flagCheck.flag,
1291
+ value: flagCheck.value,
1292
+ flagCheck
1293
+ });
1294
+ if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1295
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1296
+ }
1101
1297
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1102
1298
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1103
1299
  });
@@ -1108,19 +1304,23 @@ var Schematic = class {
1108
1304
  }
1109
1305
  };
1110
1306
  socket.addEventListener("message", messageHandler);
1111
- socket.send(
1112
- JSON.stringify({
1113
- apiKey: this.apiKey,
1114
- clientVersion: `schematic-js@${version}`,
1115
- data: context
1116
- })
1117
- );
1307
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1308
+ const messagePayload = {
1309
+ apiKey: this.apiKey,
1310
+ clientVersion,
1311
+ data: context
1312
+ };
1313
+ this.debug(`WebSocket sending message:`, messagePayload);
1314
+ socket.send(JSON.stringify(messagePayload));
1118
1315
  };
1119
1316
  if (socket.readyState === WebSocket.OPEN) {
1317
+ this.debug(`WebSocket already open, sending message`);
1120
1318
  sendMessage();
1121
1319
  } else if (socket.readyState === WebSocket.CONNECTING) {
1320
+ this.debug(`WebSocket connecting, waiting for open to send message`);
1122
1321
  socket.addEventListener("open", sendMessage);
1123
1322
  } else {
1323
+ this.debug(`WebSocket is closed, cannot send message`);
1124
1324
  reject("WebSocket is not open or connecting");
1125
1325
  }
1126
1326
  });
@@ -1177,10 +1377,22 @@ var Schematic = class {
1177
1377
  };
1178
1378
  notifyFlagCheckListeners = (flagKey, check) => {
1179
1379
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1380
+ if (listeners.size > 0) {
1381
+ this.debug(
1382
+ `Notifying ${listeners.size} flag check listeners for ${flagKey}`,
1383
+ check
1384
+ );
1385
+ }
1180
1386
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1181
1387
  };
1182
1388
  notifyFlagValueListeners = (flagKey, value) => {
1183
1389
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1390
+ if (listeners.size > 0) {
1391
+ this.debug(
1392
+ `Notifying ${listeners.size} flag value listeners for ${flagKey}`,
1393
+ { value }
1394
+ );
1395
+ }
1184
1396
  listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1185
1397
  };
1186
1398
  };
@@ -1210,7 +1422,7 @@ var notifyFlagValueListener = (listener, value) => {
1210
1422
  var import_react = __toESM(require("react"));
1211
1423
 
1212
1424
  // src/version.ts
1213
- var version2 = "1.2.3";
1425
+ var version2 = "1.2.4";
1214
1426
 
1215
1427
  // src/context/schematic.tsx
1216
1428
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -626,6 +626,26 @@ function CheckFlagResponseDataFromJSONTyped(json, ignoreDiscriminator) {
626
626
  value: json["value"]
627
627
  };
628
628
  }
629
+ function EventBodyFlagCheckToJSON(json) {
630
+ return EventBodyFlagCheckToJSONTyped(json, false);
631
+ }
632
+ function EventBodyFlagCheckToJSONTyped(value, ignoreDiscriminator = false) {
633
+ if (value == null) {
634
+ return value;
635
+ }
636
+ return {
637
+ company_id: value["companyId"],
638
+ error: value["error"],
639
+ flag_id: value["flagId"],
640
+ flag_key: value["flagKey"],
641
+ reason: value["reason"],
642
+ req_company: value["reqCompany"],
643
+ req_user: value["reqUser"],
644
+ rule_id: value["ruleId"],
645
+ user_id: value["userId"],
646
+ value: value["value"]
647
+ };
648
+ }
629
649
  function CheckFlagResponseFromJSON(json) {
630
650
  return CheckFlagResponseFromJSONTyped(json, false);
631
651
  }
@@ -728,7 +748,7 @@ function contextString(context) {
728
748
  }, {});
729
749
  return JSON.stringify(sortedContext);
730
750
  }
731
- var version = "1.2.1";
751
+ var version = "1.2.2";
732
752
  var anonymousIdKey = "schematicId";
733
753
  var Schematic = class {
734
754
  additionalHeaders = {};
@@ -736,6 +756,8 @@ var Schematic = class {
736
756
  apiUrl = "https://api.schematichq.com";
737
757
  conn = null;
738
758
  context = {};
759
+ debugEnabled = false;
760
+ offlineEnabled = false;
739
761
  eventQueue;
740
762
  eventUrl = "https://c.schematichq.com";
741
763
  flagCheckListeners = {};
@@ -750,6 +772,26 @@ var Schematic = class {
750
772
  this.apiKey = apiKey;
751
773
  this.eventQueue = [];
752
774
  this.useWebSocket = options?.useWebSocket ?? false;
775
+ this.debugEnabled = options?.debug ?? false;
776
+ this.offlineEnabled = options?.offline ?? false;
777
+ if (typeof window !== "undefined" && typeof window.location !== "undefined") {
778
+ const params = new URLSearchParams(window.location.search);
779
+ const debugParam = params.get("schematic_debug");
780
+ if (debugParam !== null && (debugParam === "" || debugParam === "true" || debugParam === "1")) {
781
+ this.debugEnabled = true;
782
+ }
783
+ const offlineParam = params.get("schematic_offline");
784
+ if (offlineParam !== null && (offlineParam === "" || offlineParam === "true" || offlineParam === "1")) {
785
+ this.offlineEnabled = true;
786
+ this.debugEnabled = true;
787
+ }
788
+ }
789
+ if (this.offlineEnabled && options?.debug !== false) {
790
+ this.debugEnabled = true;
791
+ }
792
+ if (this.offlineEnabled) {
793
+ this.setIsPending(false);
794
+ }
753
795
  this.additionalHeaders = {
754
796
  "X-Schematic-Client-Version": `schematic-js@${version}`,
755
797
  ...options?.additionalHeaders ?? {}
@@ -773,6 +815,13 @@ var Schematic = class {
773
815
  this.flushEventQueue();
774
816
  });
775
817
  }
818
+ if (this.offlineEnabled) {
819
+ this.debug(
820
+ "Initialized with offline mode enabled - no network requests will be made"
821
+ );
822
+ } else if (this.debugEnabled) {
823
+ this.debug("Initialized with debug mode enabled");
824
+ }
776
825
  }
777
826
  /**
778
827
  * Get value for a single flag.
@@ -785,6 +834,14 @@ var Schematic = class {
785
834
  const { fallback = false, key } = options;
786
835
  const context = options.context || this.context;
787
836
  const contextStr = contextString(context);
837
+ this.debug(`checkFlag: ${key}`, { context, fallback });
838
+ if (this.isOffline()) {
839
+ this.debug(`checkFlag offline result: ${key}`, {
840
+ value: fallback,
841
+ offlineMode: true
842
+ });
843
+ return fallback;
844
+ }
788
845
  if (!this.useWebSocket) {
789
846
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
790
847
  return fetch(requestUrl, {
@@ -801,17 +858,32 @@ var Schematic = class {
801
858
  }
802
859
  return response.json();
803
860
  }).then((response) => {
804
- return CheckFlagResponseFromJSON(response).data.value;
861
+ const parsedResponse = CheckFlagResponseFromJSON(response);
862
+ this.debug(`checkFlag result: ${key}`, parsedResponse);
863
+ const result = CheckFlagReturnFromJSON(parsedResponse.data);
864
+ this.submitFlagCheckEvent(key, result, context);
865
+ return result.value;
805
866
  }).catch((error) => {
806
867
  console.error("There was a problem with the fetch operation:", error);
868
+ const errorResult = {
869
+ flag: key,
870
+ value: fallback,
871
+ reason: "API request failed",
872
+ error: error instanceof Error ? error.message : String(error)
873
+ };
874
+ this.submitFlagCheckEvent(key, errorResult, context);
807
875
  return fallback;
808
876
  });
809
877
  }
810
878
  try {
811
879
  const existingVals = this.checks[contextStr];
812
- if (this.conn && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
880
+ if (this.conn !== null && typeof existingVals !== "undefined" && typeof existingVals[key] !== "undefined") {
881
+ this.debug(`checkFlag cached result: ${key}`, existingVals[key]);
813
882
  return existingVals[key].value;
814
883
  }
884
+ if (this.isOffline()) {
885
+ return fallback;
886
+ }
815
887
  try {
816
888
  await this.setContext(context);
817
889
  } catch (error) {
@@ -822,16 +894,74 @@ var Schematic = class {
822
894
  return this.fallbackToRest(key, context, fallback);
823
895
  }
824
896
  const contextVals = this.checks[contextStr] ?? {};
825
- return contextVals[key]?.value ?? fallback;
897
+ const flagCheck = contextVals[key];
898
+ const result = flagCheck?.value ?? fallback;
899
+ this.debug(
900
+ `checkFlag WebSocket result: ${key}`,
901
+ typeof flagCheck !== "undefined" ? flagCheck : { value: fallback, fallbackUsed: true }
902
+ );
903
+ if (typeof flagCheck !== "undefined") {
904
+ this.submitFlagCheckEvent(key, flagCheck, context);
905
+ }
906
+ return result;
826
907
  } catch (error) {
827
908
  console.error("Unexpected error in checkFlag:", error);
909
+ const errorResult = {
910
+ flag: key,
911
+ value: fallback,
912
+ reason: "Unexpected error in flag check",
913
+ error: error instanceof Error ? error.message : String(error)
914
+ };
915
+ this.submitFlagCheckEvent(key, errorResult, context);
828
916
  return fallback;
829
917
  }
830
918
  }
919
+ /**
920
+ * Helper function to log debug messages
921
+ * Only logs if debug mode is enabled
922
+ */
923
+ debug(message, ...args) {
924
+ if (this.debugEnabled) {
925
+ console.log(`[Schematic] ${message}`, ...args);
926
+ }
927
+ }
928
+ /**
929
+ * Helper function to check if client is in offline mode
930
+ */
931
+ isOffline() {
932
+ return this.offlineEnabled;
933
+ }
934
+ /**
935
+ * Submit a flag check event
936
+ * Records data about a flag check for analytics
937
+ */
938
+ submitFlagCheckEvent(flagKey, result, context) {
939
+ const eventBody = {
940
+ flagKey,
941
+ value: result.value,
942
+ reason: result.reason,
943
+ flagId: result.flagId,
944
+ ruleId: result.ruleId,
945
+ companyId: result.companyId,
946
+ userId: result.userId,
947
+ error: result.error,
948
+ reqCompany: context.company,
949
+ reqUser: context.user
950
+ };
951
+ this.debug(`submitting flag check event:`, eventBody);
952
+ return this.handleEvent("flag_check", EventBodyFlagCheckToJSON(eventBody));
953
+ }
831
954
  /**
832
955
  * Helper method for falling back to REST API when WebSocket connection fails
833
956
  */
834
957
  async fallbackToRest(key, context, fallback) {
958
+ if (this.isOffline()) {
959
+ this.debug(`fallbackToRest offline result: ${key}`, {
960
+ value: fallback,
961
+ offlineMode: true
962
+ });
963
+ return fallback;
964
+ }
835
965
  try {
836
966
  const requestUrl = `${this.apiUrl}/flags/${key}/check`;
837
967
  const response = await fetch(requestUrl, {
@@ -846,19 +976,36 @@ var Schematic = class {
846
976
  if (!response.ok) {
847
977
  throw new Error("Network response was not ok");
848
978
  }
849
- const data = CheckFlagResponseFromJSON(await response.json());
850
- return data?.data?.value ?? false;
979
+ const responseJson = await response.json();
980
+ const data = CheckFlagResponseFromJSON(responseJson);
981
+ this.debug(`fallbackToRest result: ${key}`, data);
982
+ const result = CheckFlagReturnFromJSON(data.data);
983
+ this.submitFlagCheckEvent(key, result, context);
984
+ return result.value;
851
985
  } catch (error) {
852
986
  console.error("REST API call failed, using fallback value:", error);
987
+ const errorResult = {
988
+ flag: key,
989
+ value: fallback,
990
+ reason: "API request failed (fallback)",
991
+ error: error instanceof Error ? error.message : String(error)
992
+ };
993
+ this.submitFlagCheckEvent(key, errorResult, context);
853
994
  return fallback;
854
995
  }
855
996
  }
856
997
  /**
857
998
  * Make an API call to fetch all flag values for a given context.
858
999
  * Recommended for use in REST mode only.
1000
+ * In offline mode, returns an empty object.
859
1001
  */
860
1002
  checkFlags = async (context) => {
861
1003
  context = context || this.context;
1004
+ this.debug(`checkFlags`, { context });
1005
+ if (this.isOffline()) {
1006
+ this.debug(`checkFlags offline result: returning empty object`);
1007
+ return {};
1008
+ }
862
1009
  const requestUrl = `${this.apiUrl}/flags/check`;
863
1010
  const requestBody = JSON.stringify(context);
864
1011
  return fetch(requestUrl, {
@@ -876,6 +1023,7 @@ var Schematic = class {
876
1023
  return response.json();
877
1024
  }).then((responseJson) => {
878
1025
  const resp = CheckFlagsResponseFromJSON(responseJson);
1026
+ this.debug(`checkFlags result:`, resp);
879
1027
  return (resp?.data?.flags ?? []).reduce(
880
1028
  (accum, flag) => {
881
1029
  accum[flag.flag] = flag.value;
@@ -894,6 +1042,7 @@ var Schematic = class {
894
1042
  * send an identify event to the Schematic API which will upsert a user and company.
895
1043
  */
896
1044
  identify = (body) => {
1045
+ this.debug(`identify:`, body);
897
1046
  try {
898
1047
  this.setContext({
899
1048
  company: body.company?.keys,
@@ -911,10 +1060,15 @@ var Schematic = class {
911
1060
  * 2. Send the context to the server
912
1061
  * 3. Wait for initial flag values to be returned
913
1062
  * The promise resolves when initial flag values are received.
1063
+ * In offline mode, this will just set the context locally without connecting.
914
1064
  */
915
1065
  setContext = async (context) => {
916
- if (!this.useWebSocket) {
1066
+ if (this.isOffline()) {
917
1067
  this.context = context;
1068
+ this.setIsPending(false);
1069
+ return Promise.resolve();
1070
+ }
1071
+ if (!this.useWebSocket) {
918
1072
  return Promise.resolve();
919
1073
  }
920
1074
  try {
@@ -935,12 +1089,14 @@ var Schematic = class {
935
1089
  */
936
1090
  track = (body) => {
937
1091
  const { company, user, event, traits } = body;
938
- return this.handleEvent("track", {
1092
+ const trackData = {
939
1093
  company: company ?? this.context.company,
940
1094
  event,
941
1095
  traits: traits ?? {},
942
1096
  user: user ?? this.context.user
943
- });
1097
+ };
1098
+ this.debug(`track:`, trackData);
1099
+ return this.handleEvent("track", trackData);
944
1100
  };
945
1101
  /**
946
1102
  * Event processing
@@ -983,8 +1139,13 @@ var Schematic = class {
983
1139
  sendEvent = async (event) => {
984
1140
  const captureUrl = `${this.eventUrl}/e`;
985
1141
  const payload = JSON.stringify(event);
1142
+ this.debug(`sending event:`, { url: captureUrl, event });
1143
+ if (this.isOffline()) {
1144
+ this.debug(`event not sent (offline mode):`, { event });
1145
+ return Promise.resolve();
1146
+ }
986
1147
  try {
987
- await fetch(captureUrl, {
1148
+ const response = await fetch(captureUrl, {
988
1149
  method: "POST",
989
1150
  headers: {
990
1151
  ...this.additionalHeaders ?? {},
@@ -992,6 +1153,10 @@ var Schematic = class {
992
1153
  },
993
1154
  body: payload
994
1155
  });
1156
+ this.debug(`event sent:`, {
1157
+ status: response.status,
1158
+ statusText: response.statusText
1159
+ });
995
1160
  } catch (error) {
996
1161
  console.error("Error sending Schematic event: ", error);
997
1162
  }
@@ -1006,8 +1171,13 @@ var Schematic = class {
1006
1171
  */
1007
1172
  /**
1008
1173
  * If using websocket mode, close the connection when done.
1174
+ * In offline mode, this is a no-op.
1009
1175
  */
1010
1176
  cleanup = async () => {
1177
+ if (this.isOffline()) {
1178
+ this.debug("cleanup: skipped (offline mode)");
1179
+ return Promise.resolve();
1180
+ }
1011
1181
  if (this.conn) {
1012
1182
  try {
1013
1183
  const socket = await this.conn;
@@ -1021,16 +1191,26 @@ var Schematic = class {
1021
1191
  };
1022
1192
  // Open a websocket connection
1023
1193
  wsConnect = () => {
1194
+ if (this.isOffline()) {
1195
+ this.debug("wsConnect: skipped (offline mode)");
1196
+ return Promise.reject(
1197
+ new Error("WebSocket connection skipped in offline mode")
1198
+ );
1199
+ }
1024
1200
  return new Promise((resolve, reject) => {
1025
1201
  const wsUrl = `${this.webSocketUrl}/flags/bootstrap?apiKey=${this.apiKey}`;
1202
+ this.debug(`connecting to WebSocket:`, wsUrl);
1026
1203
  const webSocket = new WebSocket(wsUrl);
1027
1204
  webSocket.onopen = () => {
1205
+ this.debug(`WebSocket connection opened`);
1028
1206
  resolve(webSocket);
1029
1207
  };
1030
1208
  webSocket.onerror = (error) => {
1209
+ this.debug(`WebSocket connection error:`, error);
1031
1210
  reject(error);
1032
1211
  };
1033
1212
  webSocket.onclose = () => {
1213
+ this.debug(`WebSocket connection closed`);
1034
1214
  this.conn = null;
1035
1215
  };
1036
1216
  });
@@ -1038,21 +1218,37 @@ var Schematic = class {
1038
1218
  // Send a message on the websocket indicating interest in a particular evaluation context
1039
1219
  // and wait for the initial set of flag values to be returned
1040
1220
  wsSendMessage = (socket, context) => {
1221
+ if (this.isOffline()) {
1222
+ this.debug("wsSendMessage: skipped (offline mode)");
1223
+ this.setIsPending(false);
1224
+ return Promise.resolve();
1225
+ }
1041
1226
  return new Promise((resolve, reject) => {
1042
1227
  if (contextString(context) == contextString(this.context)) {
1228
+ this.debug(`WebSocket context unchanged, skipping update`);
1043
1229
  return resolve(this.setIsPending(false));
1044
1230
  }
1231
+ this.debug(`WebSocket context updated:`, context);
1045
1232
  this.context = context;
1046
1233
  const sendMessage = () => {
1047
1234
  let resolved = false;
1048
1235
  const messageHandler = (event) => {
1049
1236
  const message = JSON.parse(event.data);
1237
+ this.debug(`WebSocket message received:`, message);
1050
1238
  if (!(contextString(context) in this.checks)) {
1051
1239
  this.checks[contextString(context)] = {};
1052
1240
  }
1053
1241
  (message.flags ?? []).forEach((flag) => {
1054
1242
  const flagCheck = CheckFlagReturnFromJSON(flag);
1055
1243
  this.checks[contextString(context)][flagCheck.flag] = flagCheck;
1244
+ this.debug(`WebSocket flag update:`, {
1245
+ flag: flagCheck.flag,
1246
+ value: flagCheck.value,
1247
+ flagCheck
1248
+ });
1249
+ if (this.flagCheckListeners[flag.flag]?.size > 0 || this.flagValueListeners[flag.flag]?.size > 0) {
1250
+ this.submitFlagCheckEvent(flagCheck.flag, flagCheck, context);
1251
+ }
1056
1252
  this.notifyFlagCheckListeners(flag.flag, flagCheck);
1057
1253
  this.notifyFlagValueListeners(flag.flag, flagCheck.value);
1058
1254
  });
@@ -1063,19 +1259,23 @@ var Schematic = class {
1063
1259
  }
1064
1260
  };
1065
1261
  socket.addEventListener("message", messageHandler);
1066
- socket.send(
1067
- JSON.stringify({
1068
- apiKey: this.apiKey,
1069
- clientVersion: `schematic-js@${version}`,
1070
- data: context
1071
- })
1072
- );
1262
+ const clientVersion = this.additionalHeaders["X-Schematic-Client-Version"] ?? `schematic-js@${version}`;
1263
+ const messagePayload = {
1264
+ apiKey: this.apiKey,
1265
+ clientVersion,
1266
+ data: context
1267
+ };
1268
+ this.debug(`WebSocket sending message:`, messagePayload);
1269
+ socket.send(JSON.stringify(messagePayload));
1073
1270
  };
1074
1271
  if (socket.readyState === WebSocket.OPEN) {
1272
+ this.debug(`WebSocket already open, sending message`);
1075
1273
  sendMessage();
1076
1274
  } else if (socket.readyState === WebSocket.CONNECTING) {
1275
+ this.debug(`WebSocket connecting, waiting for open to send message`);
1077
1276
  socket.addEventListener("open", sendMessage);
1078
1277
  } else {
1278
+ this.debug(`WebSocket is closed, cannot send message`);
1079
1279
  reject("WebSocket is not open or connecting");
1080
1280
  }
1081
1281
  });
@@ -1132,10 +1332,22 @@ var Schematic = class {
1132
1332
  };
1133
1333
  notifyFlagCheckListeners = (flagKey, check) => {
1134
1334
  const listeners = this.flagCheckListeners?.[flagKey] ?? [];
1335
+ if (listeners.size > 0) {
1336
+ this.debug(
1337
+ `Notifying ${listeners.size} flag check listeners for ${flagKey}`,
1338
+ check
1339
+ );
1340
+ }
1135
1341
  listeners.forEach((listener) => notifyFlagCheckListener(listener, check));
1136
1342
  };
1137
1343
  notifyFlagValueListeners = (flagKey, value) => {
1138
1344
  const listeners = this.flagValueListeners?.[flagKey] ?? [];
1345
+ if (listeners.size > 0) {
1346
+ this.debug(
1347
+ `Notifying ${listeners.size} flag value listeners for ${flagKey}`,
1348
+ { value }
1349
+ );
1350
+ }
1139
1351
  listeners.forEach((listener) => notifyFlagValueListener(listener, value));
1140
1352
  };
1141
1353
  };
@@ -1165,7 +1377,7 @@ var notifyFlagValueListener = (listener, value) => {
1165
1377
  import React, { createContext, useEffect, useMemo, useRef } from "react";
1166
1378
 
1167
1379
  // src/version.ts
1168
- var version2 = "1.2.3";
1380
+ var version2 = "1.2.4";
1169
1381
 
1170
1382
  // src/context/schematic.tsx
1171
1383
  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.4",
4
4
  "main": "dist/schematic-react.cjs.js",
5
5
  "module": "dist/schematic-react.esm.js",
6
6
  "types": "dist/schematic-react.d.ts",
@@ -28,7 +28,7 @@
28
28
  "tsc": "npx tsc"
29
29
  },
30
30
  "dependencies": {
31
- "@schematichq/schematic-js": "^1.2.1"
31
+ "@schematichq/schematic-js": "^1.2.2"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@microsoft/api-extractor": "^7.49.2",
@@ -36,7 +36,7 @@
36
36
  "@types/react": "^19.0.8",
37
37
  "@typescript-eslint/eslint-plugin": "^8.23.0",
38
38
  "@typescript-eslint/parser": "^8.23.0",
39
- "esbuild": "^0.24.2",
39
+ "esbuild": "^0.25.0",
40
40
  "esbuild-jest": "^0.5.0",
41
41
  "eslint": "^8.57.1",
42
42
  "eslint-plugin-import": "^2.31.0",