@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 +53 -0
- package/dist/schematic-react.cjs.js +315 -20
- package/dist/schematic-react.esm.js +315 -20
- package/package.json +16 -13
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.
|
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
|
-
|
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
|
-
|
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
|
895
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
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.
|
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.
|
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
|
-
|
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
|
-
|
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
|
850
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
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.
|
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
|
+
"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.
|
32
|
+
"@schematichq/schematic-js": "^1.2.3"
|
32
33
|
},
|
33
34
|
"devDependencies": {
|
34
|
-
"@
|
35
|
+
"@eslint/js": "^9.24.0",
|
36
|
+
"@microsoft/api-extractor": "^7.52.2",
|
35
37
|
"@types/jest": "^29.5.14",
|
36
|
-
"@types/react": "^19.
|
37
|
-
"
|
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": "^
|
41
|
+
"eslint": "^9.24.0",
|
42
42
|
"eslint-plugin-import": "^2.31.0",
|
43
|
-
"eslint-plugin-react": "^7.37.
|
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.
|
51
|
-
"ts-jest": "^29.
|
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"
|