@shipeasy/sdk 4.5.0 → 5.1.0
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 +174 -13
- package/dist/client/index.d.mts +110 -17
- package/dist/client/index.d.ts +110 -17
- package/dist/client/index.js +170 -73
- package/dist/client/index.mjs +169 -73
- package/dist/server/index.d.mts +170 -16
- package/dist/server/index.d.ts +170 -16
- package/dist/server/index.js +223 -48
- package/dist/server/index.mjs +228 -48
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/client/index.ts
|
|
21
21
|
var client_exports = {};
|
|
22
22
|
__export(client_exports, {
|
|
23
|
+
FLAG_REASONS: () => FLAG_REASONS,
|
|
23
24
|
FlagsClientBrowser: () => FlagsClientBrowser,
|
|
24
25
|
LABEL_MARKER_END: () => LABEL_MARKER_END,
|
|
25
26
|
LABEL_MARKER_RE: () => LABEL_MARKER_RE,
|
|
@@ -126,25 +127,29 @@ function causesThe(subject) {
|
|
|
126
127
|
};
|
|
127
128
|
}
|
|
128
129
|
function violation(name) {
|
|
129
|
-
|
|
130
|
-
__seViolation: true,
|
|
131
|
-
violationName: String(name),
|
|
132
|
-
...msg !== void 0 ? { violationMessage: msg } : {},
|
|
133
|
-
message(m) {
|
|
134
|
-
return make(String(m));
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
return make();
|
|
130
|
+
return { __seViolation: true, violationName: String(name) };
|
|
138
131
|
}
|
|
139
132
|
function isViolation(p) {
|
|
140
133
|
return typeof p === "object" && p !== null && p.__seViolation === true;
|
|
141
134
|
}
|
|
142
135
|
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
143
|
-
function
|
|
136
|
+
function readExpectedMark(err) {
|
|
137
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
138
|
+
const v = err[EXPECTED_SYM];
|
|
139
|
+
return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
|
|
140
|
+
}
|
|
141
|
+
function markExpected(err, because, extras) {
|
|
144
142
|
if (typeof err !== "object" || err === null) return;
|
|
143
|
+
const prev = readExpectedMark(err);
|
|
144
|
+
const clean = sanitizeExtras(extras);
|
|
145
|
+
const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
|
|
146
|
+
const mark = {
|
|
147
|
+
because: String(because),
|
|
148
|
+
...merged ? { extras: merged } : {}
|
|
149
|
+
};
|
|
145
150
|
try {
|
|
146
151
|
Object.defineProperty(err, EXPECTED_SYM, {
|
|
147
|
-
value:
|
|
152
|
+
value: mark,
|
|
148
153
|
enumerable: false,
|
|
149
154
|
configurable: true
|
|
150
155
|
});
|
|
@@ -225,7 +230,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
225
230
|
let kind;
|
|
226
231
|
if (isViolation(problem)) {
|
|
227
232
|
errorType = problem.violationName;
|
|
228
|
-
message = problem.
|
|
233
|
+
message = problem.violationName;
|
|
229
234
|
stack = captureCallsiteStack();
|
|
230
235
|
kind = kindOverride ?? "violation";
|
|
231
236
|
} else if (problem instanceof Error) {
|
|
@@ -308,19 +313,21 @@ function startSeeChain(getProblem, dispatch) {
|
|
|
308
313
|
return { causes_the: start, causesThe: start };
|
|
309
314
|
}
|
|
310
315
|
function startSeeViolationChain(name, dispatch) {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
return startSeeChain(() => violation(name), dispatch);
|
|
317
|
+
}
|
|
318
|
+
function startControlFlowChain(err) {
|
|
319
|
+
return {
|
|
320
|
+
because(reason) {
|
|
321
|
+
markExpected(err, reason);
|
|
322
|
+
const tail = {
|
|
323
|
+
extras(x) {
|
|
324
|
+
markExpected(err, reason, x);
|
|
325
|
+
return tail;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
return tail;
|
|
321
329
|
}
|
|
322
330
|
};
|
|
323
|
-
return chain;
|
|
324
331
|
}
|
|
325
332
|
function topStackLine(stack) {
|
|
326
333
|
if (!stack) return "";
|
|
@@ -352,6 +359,14 @@ var SeeLimiter = class {
|
|
|
352
359
|
|
|
353
360
|
// src/client/index.ts
|
|
354
361
|
var version = "4.0.0";
|
|
362
|
+
var FLAG_REASONS = [
|
|
363
|
+
"CLIENT_NOT_READY",
|
|
364
|
+
"FLAG_NOT_FOUND",
|
|
365
|
+
"OFF",
|
|
366
|
+
"OVERRIDE",
|
|
367
|
+
"RULE_MATCH",
|
|
368
|
+
"DEFAULT"
|
|
369
|
+
];
|
|
355
370
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
356
371
|
var MAX_BUFFER = 100;
|
|
357
372
|
var ANON_ID_KEY = "__se_anon_id";
|
|
@@ -575,39 +590,11 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
|
|
|
575
590
|
}
|
|
576
591
|
}
|
|
577
592
|
if (groups.errors) {
|
|
578
|
-
const origOnError = window.onerror;
|
|
579
|
-
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
580
|
-
if (!isExpected(err)) {
|
|
581
|
-
const problem = err ?? (typeof msg === "string" && msg ? msg : "Unknown error");
|
|
582
|
-
reportSee(
|
|
583
|
-
problem,
|
|
584
|
-
causesThe("page").to("hit an unhandled error"),
|
|
585
|
-
{
|
|
586
|
-
source: typeof source === "string" ? source : void 0,
|
|
587
|
-
line: lineno ?? void 0
|
|
588
|
-
},
|
|
589
|
-
"uncaught"
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
593
|
-
return false;
|
|
594
|
-
};
|
|
595
|
-
window.addEventListener("unhandledrejection", (e) => {
|
|
596
|
-
const reason = e.reason;
|
|
597
|
-
if (isExpected(reason)) return;
|
|
598
|
-
reportSee(
|
|
599
|
-
reason ?? "Unhandled promise rejection",
|
|
600
|
-
causesThe("page").to("hit an unhandled promise rejection"),
|
|
601
|
-
void 0,
|
|
602
|
-
"unhandled_rejection"
|
|
603
|
-
);
|
|
604
|
-
});
|
|
605
593
|
const origFetch = window.fetch;
|
|
606
594
|
window.fetch = async function(...args) {
|
|
607
595
|
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
608
596
|
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
|
|
609
597
|
const ignored = ignoreUrlPrefixes.some((p) => p && url.startsWith(p));
|
|
610
|
-
const bareUrl = url.split("?")[0].slice(0, 200);
|
|
611
598
|
let corr;
|
|
612
599
|
if (!ignored && sameOrigin(url) && typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
613
600
|
corr = crypto.randomUUID();
|
|
@@ -619,7 +606,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
|
|
|
619
606
|
} catch (err) {
|
|
620
607
|
if (!ignored && !isExpected(err)) {
|
|
621
608
|
reportSee(
|
|
622
|
-
violation("NetworkError")
|
|
609
|
+
violation("NetworkError"),
|
|
623
610
|
causesThe(`request to ${endpointTemplate(url)}`).to("get no response"),
|
|
624
611
|
{ status: 0, url: url.slice(0, 200) },
|
|
625
612
|
"network"
|
|
@@ -630,7 +617,7 @@ function installAutoGuardrails(buffer, userId, anonId, groups, reportSee, ignore
|
|
|
630
617
|
if (!ignored && res.status >= 500) {
|
|
631
618
|
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
632
619
|
reportSee(
|
|
633
|
-
violation("Http5xx")
|
|
620
|
+
violation("Http5xx"),
|
|
634
621
|
causesThe(`request to ${endpointTemplate(url)}`).to("fail with a server error"),
|
|
635
622
|
{ status: res.status, url: url.slice(0, 200), duration_ms: Math.round(elapsed) },
|
|
636
623
|
"network",
|
|
@@ -891,7 +878,7 @@ function readExperimentOverridesFromUrl() {
|
|
|
891
878
|
}
|
|
892
879
|
return out;
|
|
893
880
|
}
|
|
894
|
-
var FlagsClientBrowser = class {
|
|
881
|
+
var FlagsClientBrowser = class _FlagsClientBrowser {
|
|
895
882
|
sdkKey;
|
|
896
883
|
baseUrl;
|
|
897
884
|
autoGuardrails;
|
|
@@ -910,6 +897,16 @@ var FlagsClientBrowser = class {
|
|
|
910
897
|
// Monotonic counter so a later identify() always wins even if its /sdk/evaluate
|
|
911
898
|
// response races and lands before an earlier in-flight call's response.
|
|
912
899
|
identifySeq = 0;
|
|
900
|
+
// Test mode: built by `FlagsClientBrowser.forTesting()`. When set, identify()
|
|
901
|
+
// never fetches, track() is a no-op, telemetry is off, and the client is
|
|
902
|
+
// already ready with an empty eval result.
|
|
903
|
+
testMode;
|
|
904
|
+
// Programmatic overrides (Statsig-style). Set on any client via
|
|
905
|
+
// overrideFlag/overrideConfig/overrideExperiment; they win over BOTH the URL
|
|
906
|
+
// overrides and the fetched eval result. Cleared by clearOverrides().
|
|
907
|
+
flagOverrides = /* @__PURE__ */ new Map();
|
|
908
|
+
configOverrides = /* @__PURE__ */ new Map();
|
|
909
|
+
experimentOverrides = /* @__PURE__ */ new Map();
|
|
913
910
|
onOverrideChange = () => {
|
|
914
911
|
this.installBridge();
|
|
915
912
|
this.notify();
|
|
@@ -918,6 +915,7 @@ var FlagsClientBrowser = class {
|
|
|
918
915
|
this.sdkKey = opts.sdkKey;
|
|
919
916
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
920
917
|
this.env = opts.env ?? "prod";
|
|
918
|
+
this.testMode = opts.testMode === true;
|
|
921
919
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
922
920
|
this.autoCollectAlways = opts.autoCollectAlways === true;
|
|
923
921
|
const g = opts.autoGuardrailGroups ?? {};
|
|
@@ -933,11 +931,42 @@ var FlagsClientBrowser = class {
|
|
|
933
931
|
sdkKey: this.sdkKey,
|
|
934
932
|
side: "client",
|
|
935
933
|
env: this.env,
|
|
936
|
-
|
|
934
|
+
// Test mode never talks to the network — telemetry off regardless of opt.
|
|
935
|
+
disabled: this.testMode || opts.disableTelemetry
|
|
936
|
+
});
|
|
937
|
+
if (this.testMode) {
|
|
938
|
+
this.evalResult = { flags: {}, configs: {}, experiments: {}, killswitches: {} };
|
|
939
|
+
} else {
|
|
940
|
+
void this.buffer.flushPendingAlias();
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Build a no-network, immediately-usable browser client for tests
|
|
945
|
+
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
946
|
+
* is a no-op, telemetry is disabled, and the client is already ready — seed
|
|
947
|
+
* every entity with overrideFlag/overrideConfig/overrideExperiment. No SDK
|
|
948
|
+
* key required.
|
|
949
|
+
*
|
|
950
|
+
* ```ts
|
|
951
|
+
* const client = FlagsClientBrowser.forTesting();
|
|
952
|
+
* client.overrideFlag("new_checkout", true);
|
|
953
|
+
* client.getFlag("new_checkout"); // true
|
|
954
|
+
* ```
|
|
955
|
+
*/
|
|
956
|
+
static forTesting(opts) {
|
|
957
|
+
return new _FlagsClientBrowser({
|
|
958
|
+
sdkKey: "",
|
|
959
|
+
autoGuardrails: false,
|
|
960
|
+
...opts,
|
|
961
|
+
testMode: true
|
|
937
962
|
});
|
|
938
|
-
void this.buffer.flushPendingAlias();
|
|
939
963
|
}
|
|
940
964
|
async identify(user) {
|
|
965
|
+
if (this.testMode) {
|
|
966
|
+
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
967
|
+
this.notify();
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
941
970
|
const seq = ++this.identifySeq;
|
|
942
971
|
const prevUserId = this.userId;
|
|
943
972
|
if (user.user_id !== void 0) this.userId = user.user_id;
|
|
@@ -1012,22 +1041,75 @@ var FlagsClientBrowser = class {
|
|
|
1012
1041
|
initFromBootstrap(data) {
|
|
1013
1042
|
this.evalResult = data;
|
|
1014
1043
|
}
|
|
1015
|
-
|
|
1044
|
+
// ---- Local overrides (Statsig-style) ----
|
|
1045
|
+
//
|
|
1046
|
+
// Precedence (highest first): programmatic override (these methods) > URL
|
|
1047
|
+
// override (?se_gate_/?se_config_/?se_exp_) > fetched eval result. A
|
|
1048
|
+
// programmatic override is an explicit in-code decision, so it wins over the
|
|
1049
|
+
// ad-hoc URL/devtools overrides as well as the server's evaluation.
|
|
1050
|
+
/** Force `getFlag(name)` to return `value`, ignoring URL overrides + eval. */
|
|
1051
|
+
overrideFlag(name, value) {
|
|
1052
|
+
this.flagOverrides.set(name, value);
|
|
1053
|
+
}
|
|
1054
|
+
/** Force `getConfig(name)` to return `value`, ignoring URL overrides + eval. */
|
|
1055
|
+
overrideConfig(name, value) {
|
|
1056
|
+
this.configOverrides.set(name, value);
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
1060
|
+
* ignoring URL overrides, the eval result, and exposure logging.
|
|
1061
|
+
*/
|
|
1062
|
+
overrideExperiment(name, group, params) {
|
|
1063
|
+
this.experimentOverrides.set(name, { group, params });
|
|
1064
|
+
}
|
|
1065
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
1066
|
+
clearOverrides() {
|
|
1067
|
+
this.flagOverrides.clear();
|
|
1068
|
+
this.configOverrides.clear();
|
|
1069
|
+
this.experimentOverrides.clear();
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). A
|
|
1073
|
+
* local override OR a ?se_gate_/?se_ks_ URL override short-circuits BEFORE
|
|
1074
|
+
* telemetry; otherwise exactly one "gate" beacon is emitted. The server
|
|
1075
|
+
* pre-evaluates a gate's enabled/killed state into a plain boolean, so OFF
|
|
1076
|
+
* folds into DEFAULT here (the browser can't tell "disabled" from "rule
|
|
1077
|
+
* denied").
|
|
1078
|
+
*/
|
|
1079
|
+
getFlagDetail(name) {
|
|
1080
|
+
const pov = this.flagOverrides.get(name);
|
|
1081
|
+
if (pov !== void 0) return { value: pov, reason: "OVERRIDE" };
|
|
1082
|
+
const urlOv = readGateOverride(name);
|
|
1083
|
+
if (urlOv !== null) return { value: urlOv, reason: "OVERRIDE" };
|
|
1016
1084
|
this.telemetry.emit("gate", name);
|
|
1017
|
-
if (this.evalResult === null) return false;
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
return
|
|
1085
|
+
if (this.evalResult === null) return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1086
|
+
if (!(name in this.evalResult.flags)) return { value: false, reason: "FLAG_NOT_FOUND" };
|
|
1087
|
+
const value = this.evalResult.flags[name] ?? false;
|
|
1088
|
+
return { value, reason: value ? "RULE_MATCH" : "DEFAULT" };
|
|
1021
1089
|
}
|
|
1022
|
-
|
|
1090
|
+
/**
|
|
1091
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
1092
|
+
* evaluated (not ready or flag not found) — never for a gate that legitimately
|
|
1093
|
+
* evaluates to false. Plain `getFlag(name)` keeps returning false for a
|
|
1094
|
+
* missing flag.
|
|
1095
|
+
*/
|
|
1096
|
+
getFlag(name, defaultValue = false) {
|
|
1097
|
+
const d = this.getFlagDetail(name);
|
|
1098
|
+
if (d.reason === "CLIENT_NOT_READY" || d.reason === "FLAG_NOT_FOUND") return defaultValue;
|
|
1099
|
+
return d.value;
|
|
1100
|
+
}
|
|
1101
|
+
getConfig(name, decodeOrOpts) {
|
|
1023
1102
|
this.telemetry.emit("config", name);
|
|
1024
|
-
|
|
1025
|
-
const
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1103
|
+
const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts } : decodeOrOpts ?? {};
|
|
1104
|
+
const fallback = "defaultValue" in opts ? opts.defaultValue : void 0;
|
|
1105
|
+
const hasProgrammatic = this.configOverrides.has(name);
|
|
1106
|
+
if (!hasProgrammatic && this.evalResult === null) return fallback;
|
|
1107
|
+
const urlOv = hasProgrammatic ? void 0 : readConfigOverride(name);
|
|
1108
|
+
const raw = hasProgrammatic ? this.configOverrides.get(name) : urlOv !== void 0 ? urlOv : this.evalResult?.configs?.[name];
|
|
1109
|
+
if (raw === void 0) return fallback;
|
|
1110
|
+
if (!opts.decode) return raw;
|
|
1029
1111
|
try {
|
|
1030
|
-
return decode(raw);
|
|
1112
|
+
return opts.decode(raw);
|
|
1031
1113
|
} catch (err) {
|
|
1032
1114
|
console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
1033
1115
|
return void 0;
|
|
@@ -1040,6 +1122,12 @@ var FlagsClientBrowser = class {
|
|
|
1040
1122
|
group: "control",
|
|
1041
1123
|
params: defaultParams
|
|
1042
1124
|
};
|
|
1125
|
+
const pov = this.experimentOverrides.get(name);
|
|
1126
|
+
if (pov) {
|
|
1127
|
+
const variantParams = variants?.[pov.group];
|
|
1128
|
+
const params = { ...defaultParams, ...pov.params, ...variantParams ?? {} };
|
|
1129
|
+
return { inExperiment: true, group: pov.group, params };
|
|
1130
|
+
}
|
|
1043
1131
|
const ov = readExpOverride(name);
|
|
1044
1132
|
if (ov !== null) {
|
|
1045
1133
|
const variantParams = variants?.[ov];
|
|
@@ -1091,6 +1179,7 @@ var FlagsClientBrowser = class {
|
|
|
1091
1179
|
return bridge;
|
|
1092
1180
|
}
|
|
1093
1181
|
track(eventName, props) {
|
|
1182
|
+
if (this.testMode) return;
|
|
1094
1183
|
this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
|
|
1095
1184
|
}
|
|
1096
1185
|
/**
|
|
@@ -1309,12 +1398,19 @@ var flags = {
|
|
|
1309
1398
|
* Everything else gates on _mountedAndReady to prevent hydration mismatches on
|
|
1310
1399
|
* force-static pages where SSR has no flag data.
|
|
1311
1400
|
*/
|
|
1312
|
-
get(name) {
|
|
1401
|
+
get(name, defaultValue = false) {
|
|
1313
1402
|
const bs = getBootstrap();
|
|
1314
1403
|
if (bs !== null && name in bs.flags) return bs.flags[name];
|
|
1315
|
-
if (!_mountedAndReady) return
|
|
1316
|
-
if (_client) return _client.getFlag(name);
|
|
1317
|
-
return readGateOverride(name) ??
|
|
1404
|
+
if (!_mountedAndReady) return defaultValue;
|
|
1405
|
+
if (_client) return _client.getFlag(name, defaultValue);
|
|
1406
|
+
return readGateOverride(name) ?? defaultValue;
|
|
1407
|
+
},
|
|
1408
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1409
|
+
getDetail(name) {
|
|
1410
|
+
if (_client) return _client.getFlagDetail(name);
|
|
1411
|
+
const ov = readGateOverride(name);
|
|
1412
|
+
if (ov !== null) return { value: ov, reason: "OVERRIDE" };
|
|
1413
|
+
return { value: false, reason: "CLIENT_NOT_READY" };
|
|
1318
1414
|
},
|
|
1319
1415
|
getConfig(name, decode) {
|
|
1320
1416
|
const bs = getBootstrap();
|
|
@@ -1409,7 +1505,7 @@ var see = Object.assign(
|
|
|
1409
1505
|
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
1410
1506
|
{
|
|
1411
1507
|
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
1412
|
-
ControlFlowException:
|
|
1508
|
+
ControlFlowException: (err) => startControlFlowChain(err)
|
|
1413
1509
|
}
|
|
1414
1510
|
);
|
|
1415
1511
|
var LABEL_MARKER_START = "\uFFF9";
|
|
@@ -1646,6 +1742,7 @@ var i18n = {
|
|
|
1646
1742
|
};
|
|
1647
1743
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1648
1744
|
0 && (module.exports = {
|
|
1745
|
+
FLAG_REASONS,
|
|
1649
1746
|
FlagsClientBrowser,
|
|
1650
1747
|
LABEL_MARKER_END,
|
|
1651
1748
|
LABEL_MARKER_RE,
|