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