@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/server/index.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/server/index.ts
|
|
2
9
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3
10
|
|
|
@@ -81,25 +88,29 @@ function causesThe(subject) {
|
|
|
81
88
|
};
|
|
82
89
|
}
|
|
83
90
|
function violation(name) {
|
|
84
|
-
|
|
85
|
-
__seViolation: true,
|
|
86
|
-
violationName: String(name),
|
|
87
|
-
...msg !== void 0 ? { violationMessage: msg } : {},
|
|
88
|
-
message(m) {
|
|
89
|
-
return make(String(m));
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
return make();
|
|
91
|
+
return { __seViolation: true, violationName: String(name) };
|
|
93
92
|
}
|
|
94
93
|
function isViolation(p) {
|
|
95
94
|
return typeof p === "object" && p !== null && p.__seViolation === true;
|
|
96
95
|
}
|
|
97
96
|
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
98
|
-
function
|
|
97
|
+
function readExpectedMark(err) {
|
|
98
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
99
|
+
const v = err[EXPECTED_SYM];
|
|
100
|
+
return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
|
|
101
|
+
}
|
|
102
|
+
function markExpected(err, because, extras) {
|
|
99
103
|
if (typeof err !== "object" || err === null) return;
|
|
104
|
+
const prev = readExpectedMark(err);
|
|
105
|
+
const clean = sanitizeExtras(extras);
|
|
106
|
+
const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
|
|
107
|
+
const mark = {
|
|
108
|
+
because: String(because),
|
|
109
|
+
...merged ? { extras: merged } : {}
|
|
110
|
+
};
|
|
100
111
|
try {
|
|
101
112
|
Object.defineProperty(err, EXPECTED_SYM, {
|
|
102
|
-
value:
|
|
113
|
+
value: mark,
|
|
103
114
|
enumerable: false,
|
|
104
115
|
configurable: true
|
|
105
116
|
});
|
|
@@ -180,7 +191,7 @@ function buildSeeEvent(problem, consequence, extras, ctx, kindOverride, correlat
|
|
|
180
191
|
let kind;
|
|
181
192
|
if (isViolation(problem)) {
|
|
182
193
|
errorType = problem.violationName;
|
|
183
|
-
message = problem.
|
|
194
|
+
message = problem.violationName;
|
|
184
195
|
stack = captureCallsiteStack();
|
|
185
196
|
kind = kindOverride ?? "violation";
|
|
186
197
|
} else if (problem instanceof Error) {
|
|
@@ -263,19 +274,21 @@ function startSeeChain(getProblem, dispatch) {
|
|
|
263
274
|
return { causes_the: start, causesThe: start };
|
|
264
275
|
}
|
|
265
276
|
function startSeeViolationChain(name, dispatch) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
return startSeeChain(() => violation(name), dispatch);
|
|
278
|
+
}
|
|
279
|
+
function startControlFlowChain(err) {
|
|
280
|
+
return {
|
|
281
|
+
because(reason) {
|
|
282
|
+
markExpected(err, reason);
|
|
283
|
+
const tail = {
|
|
284
|
+
extras(x) {
|
|
285
|
+
markExpected(err, reason, x);
|
|
286
|
+
return tail;
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return tail;
|
|
276
290
|
}
|
|
277
291
|
};
|
|
278
|
-
return chain;
|
|
279
292
|
}
|
|
280
293
|
function topStackLine(stack) {
|
|
281
294
|
if (!stack) return "";
|
|
@@ -309,6 +322,9 @@ var SeeLimiter = class {
|
|
|
309
322
|
var version = "4.0.0";
|
|
310
323
|
var C1 = 3432918353;
|
|
311
324
|
var C2 = 461845907;
|
|
325
|
+
function _murmur3ForTests(key) {
|
|
326
|
+
return murmur3(key);
|
|
327
|
+
}
|
|
312
328
|
function murmur3(key) {
|
|
313
329
|
const bytes = new TextEncoder().encode(key);
|
|
314
330
|
const len = bytes.length;
|
|
@@ -349,6 +365,14 @@ function murmur3(key) {
|
|
|
349
365
|
h1 ^= h1 >>> 16;
|
|
350
366
|
return h1 >>> 0;
|
|
351
367
|
}
|
|
368
|
+
var FLAG_REASONS = [
|
|
369
|
+
"CLIENT_NOT_READY",
|
|
370
|
+
"FLAG_NOT_FOUND",
|
|
371
|
+
"OFF",
|
|
372
|
+
"OVERRIDE",
|
|
373
|
+
"RULE_MATCH",
|
|
374
|
+
"DEFAULT"
|
|
375
|
+
];
|
|
352
376
|
function isEnabled(v) {
|
|
353
377
|
return v === 1 || v === true;
|
|
354
378
|
}
|
|
@@ -456,7 +480,7 @@ function parseOverrides(rawUrl) {
|
|
|
456
480
|
}
|
|
457
481
|
return { gates, configs, experiments };
|
|
458
482
|
}
|
|
459
|
-
var FlagsClient = class {
|
|
483
|
+
var FlagsClient = class _FlagsClient {
|
|
460
484
|
apiKey;
|
|
461
485
|
baseUrl;
|
|
462
486
|
env;
|
|
@@ -469,47 +493,155 @@ var FlagsClient = class {
|
|
|
469
493
|
pollInterval = 30;
|
|
470
494
|
timer = null;
|
|
471
495
|
initialized = false;
|
|
496
|
+
// Test mode: built by `FlagsClient.forTesting()`. When set, init()/initOnce()
|
|
497
|
+
// never fetch, track() is a no-op, and telemetry is off — the client is a
|
|
498
|
+
// fully self-contained, network-free seam for unit tests.
|
|
499
|
+
testMode;
|
|
500
|
+
// Programmatic overrides (Statsig-style). Set on any client via
|
|
501
|
+
// overrideFlag/overrideConfig/overrideExperiment; they win over the fetched
|
|
502
|
+
// blob in getFlag/getConfig/getExperiment. Cleared by clearOverrides().
|
|
503
|
+
flagOverrides = /* @__PURE__ */ new Map();
|
|
504
|
+
configOverrides = /* @__PURE__ */ new Map();
|
|
505
|
+
experimentOverrides = /* @__PURE__ */ new Map();
|
|
506
|
+
// Change listeners fired after a background poll returns NEW data (200, not
|
|
507
|
+
// 304). Never fired in testMode/offline (no polling happens there).
|
|
508
|
+
changeListeners = /* @__PURE__ */ new Set();
|
|
472
509
|
constructor(opts) {
|
|
473
510
|
this.apiKey = opts.apiKey;
|
|
474
511
|
this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
|
|
475
512
|
this.env = opts.env ?? "prod";
|
|
513
|
+
this.testMode = opts.testMode === true;
|
|
476
514
|
this.telemetry = new Telemetry({
|
|
477
515
|
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
478
516
|
sdkKey: this.apiKey,
|
|
479
517
|
side: "server",
|
|
480
518
|
env: this.env,
|
|
481
|
-
|
|
519
|
+
// Test mode never talks to the network — telemetry off regardless of opt.
|
|
520
|
+
disabled: this.testMode || opts.disableTelemetry
|
|
482
521
|
});
|
|
483
|
-
if (opts.initialBlob) {
|
|
484
|
-
this.flagsBlob = opts.initialBlob;
|
|
522
|
+
if (opts.initialBlob || this.testMode) {
|
|
523
|
+
this.flagsBlob = opts.initialBlob ?? { version: "test", plan: "free", gates: {}, configs: {}, killswitches: {} };
|
|
524
|
+
this.expsBlob = this.expsBlob ?? { version: "test", universes: {}, experiments: {} };
|
|
485
525
|
this.initialized = true;
|
|
486
526
|
}
|
|
487
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
530
|
+
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
531
|
+
* is disabled, and the client is already "initialized" — seed every entity
|
|
532
|
+
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
533
|
+
*
|
|
534
|
+
* ```ts
|
|
535
|
+
* const client = FlagsClient.forTesting();
|
|
536
|
+
* client.overrideFlag("new_checkout", true);
|
|
537
|
+
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
static forTesting(opts) {
|
|
541
|
+
return new _FlagsClient({ apiKey: "", ...opts, testMode: true });
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
545
|
+
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
546
|
+
* telemetry off) but, unlike forTesting(), seeds the REAL flags + experiments
|
|
547
|
+
* blobs so evaluations run the canonical eval against the snapshot. Local
|
|
548
|
+
* overrides still apply on top.
|
|
549
|
+
*
|
|
550
|
+
* Snapshot shape mirrors the wire bodies:
|
|
551
|
+
* `{ flags: <GET /sdk/flags body>, experiments: <GET /sdk/experiments body> }`.
|
|
552
|
+
*/
|
|
553
|
+
static fromSnapshot(snapshot) {
|
|
554
|
+
const client = new _FlagsClient({ apiKey: "", testMode: true });
|
|
555
|
+
client.flagsBlob = snapshot.flags;
|
|
556
|
+
client.expsBlob = snapshot.experiments;
|
|
557
|
+
client.initialized = true;
|
|
558
|
+
return client;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
562
|
+
* not available in the browser entrypoint). The file must contain
|
|
563
|
+
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
564
|
+
* See {@link FlagsClient.fromSnapshot}.
|
|
565
|
+
*/
|
|
566
|
+
static fromFile(path) {
|
|
567
|
+
const fs = __require("fs");
|
|
568
|
+
const raw = fs.readFileSync(path, "utf8");
|
|
569
|
+
const snapshot = JSON.parse(raw);
|
|
570
|
+
return _FlagsClient.fromSnapshot(snapshot);
|
|
571
|
+
}
|
|
488
572
|
async init() {
|
|
573
|
+
if (this.testMode) {
|
|
574
|
+
this.initialized = true;
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
489
577
|
await this.fetchAll();
|
|
490
578
|
this.initialized = true;
|
|
491
579
|
this.startPoll();
|
|
492
580
|
}
|
|
493
581
|
async initOnce() {
|
|
494
|
-
if (this.initialized) return;
|
|
582
|
+
if (this.testMode || this.initialized) return;
|
|
495
583
|
await this.fetchAll();
|
|
496
584
|
this.initialized = true;
|
|
497
585
|
}
|
|
586
|
+
// ---- Local overrides (Statsig-style) ----
|
|
587
|
+
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
588
|
+
overrideFlag(name, value) {
|
|
589
|
+
this.flagOverrides.set(name, value);
|
|
590
|
+
}
|
|
591
|
+
/** Force `getConfig(name)` to return `value`, ignoring the fetched config. */
|
|
592
|
+
overrideConfig(name, value) {
|
|
593
|
+
this.configOverrides.set(name, value);
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Force `getExperiment(name, …)` to return `{ inExperiment: true, group, params }`,
|
|
597
|
+
* ignoring allocation, holdouts, and targeting.
|
|
598
|
+
*/
|
|
599
|
+
overrideExperiment(name, group, params) {
|
|
600
|
+
this.experimentOverrides.set(name, { group, params });
|
|
601
|
+
}
|
|
602
|
+
/** Remove every programmatic override set via the override* methods. */
|
|
603
|
+
clearOverrides() {
|
|
604
|
+
this.flagOverrides.clear();
|
|
605
|
+
this.configOverrides.clear();
|
|
606
|
+
this.experimentOverrides.clear();
|
|
607
|
+
}
|
|
498
608
|
destroy() {
|
|
499
609
|
if (this.timer !== null) {
|
|
500
610
|
clearInterval(this.timer);
|
|
501
611
|
this.timer = null;
|
|
502
612
|
}
|
|
503
613
|
}
|
|
614
|
+
/**
|
|
615
|
+
* Subscribe to data-change notifications. The listener fires after a
|
|
616
|
+
* background poll fetch returns NEW data (200, not 304) — i.e. after the
|
|
617
|
+
* cached blob is updated. Never fires in testMode/offline (no polling).
|
|
618
|
+
* Returns an unsubscribe function.
|
|
619
|
+
*/
|
|
620
|
+
onChange(listener) {
|
|
621
|
+
this.changeListeners.add(listener);
|
|
622
|
+
return () => {
|
|
623
|
+
this.changeListeners.delete(listener);
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
notifyChange() {
|
|
627
|
+
for (const l of this.changeListeners) {
|
|
628
|
+
try {
|
|
629
|
+
l();
|
|
630
|
+
} catch (err) {
|
|
631
|
+
console.warn("[shipeasy] onChange listener threw:", String(err));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
504
635
|
startPoll() {
|
|
505
636
|
this.timer = setInterval(() => {
|
|
506
|
-
this.fetchAll().catch(
|
|
637
|
+
this.fetchAll(true).catch(
|
|
507
638
|
(err) => console.warn("[shipeasy] background poll failed:", String(err))
|
|
508
639
|
);
|
|
509
640
|
}, this.pollInterval * 1e3);
|
|
510
641
|
}
|
|
511
|
-
async fetchAll() {
|
|
512
|
-
const [
|
|
642
|
+
async fetchAll(fromPoll = false) {
|
|
643
|
+
const [flagsRes, expsChanged] = await Promise.all([this.fetchFlags(), this.fetchExps()]);
|
|
644
|
+
const { interval, changed: flagsChanged } = flagsRes;
|
|
513
645
|
if (interval !== null && interval !== this.pollInterval) {
|
|
514
646
|
this.pollInterval = interval;
|
|
515
647
|
if (this.timer !== null) {
|
|
@@ -517,41 +649,72 @@ var FlagsClient = class {
|
|
|
517
649
|
this.startPoll();
|
|
518
650
|
}
|
|
519
651
|
}
|
|
652
|
+
if (fromPoll && (flagsChanged || expsChanged)) this.notifyChange();
|
|
520
653
|
}
|
|
521
654
|
async fetchFlags() {
|
|
522
655
|
const headers = { "X-SDK-Key": this.apiKey };
|
|
523
656
|
if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
|
|
524
657
|
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags?env=${this.env}`, { headers });
|
|
525
658
|
const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
|
|
526
|
-
if (res.status === 304) return interval;
|
|
659
|
+
if (res.status === 304) return { interval, changed: false };
|
|
527
660
|
if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
|
|
528
661
|
const etag = res.headers.get("ETag");
|
|
529
662
|
if (etag) this.flagsEtag = etag;
|
|
530
663
|
this.flagsBlob = await res.json();
|
|
531
|
-
return interval;
|
|
664
|
+
return { interval, changed: true };
|
|
532
665
|
}
|
|
533
666
|
async fetchExps() {
|
|
534
667
|
const headers = { "X-SDK-Key": this.apiKey };
|
|
535
668
|
if (this.expsEtag) headers["If-None-Match"] = this.expsEtag;
|
|
536
669
|
const res = await globalThis.fetch(`${this.baseUrl}/sdk/experiments`, { headers });
|
|
537
|
-
if (res.status === 304) return;
|
|
670
|
+
if (res.status === 304) return false;
|
|
538
671
|
if (!res.ok) throw new Error(`/sdk/experiments returned ${res.status}`);
|
|
539
672
|
const etag = res.headers.get("ETag");
|
|
540
673
|
if (etag) this.expsEtag = etag;
|
|
541
674
|
this.expsBlob = await res.json();
|
|
675
|
+
return true;
|
|
542
676
|
}
|
|
543
|
-
|
|
677
|
+
/**
|
|
678
|
+
* Evaluate a gate and report WHY (LaunchDarkly variationDetail parity). The
|
|
679
|
+
* reason is computed entirely at this boundary — the canonical eval
|
|
680
|
+
* (evalGateInternal) is untouched. A local override short-circuits BEFORE
|
|
681
|
+
* telemetry, exactly like getFlag's override path; otherwise exactly one
|
|
682
|
+
* "gate" beacon is emitted.
|
|
683
|
+
*/
|
|
684
|
+
getFlagDetail(name, user) {
|
|
685
|
+
const ov = this.flagOverrides.get(name);
|
|
686
|
+
if (ov !== void 0) return { value: ov, reason: "OVERRIDE" };
|
|
544
687
|
this.telemetry.emit("gate", name);
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
688
|
+
if (!this.flagsBlob) return { value: false, reason: "CLIENT_NOT_READY" };
|
|
689
|
+
const gate = this.flagsBlob.gates[name];
|
|
690
|
+
if (!gate) return { value: false, reason: "FLAG_NOT_FOUND" };
|
|
691
|
+
if (isEnabled(gate.killswitch) || !isEnabled(gate.enabled)) {
|
|
692
|
+
return { value: false, reason: "OFF" };
|
|
693
|
+
}
|
|
694
|
+
const value = evalGateInternal(gate, user);
|
|
695
|
+
return { value, reason: value ? "RULE_MATCH" : "DEFAULT" };
|
|
548
696
|
}
|
|
549
|
-
|
|
697
|
+
/**
|
|
698
|
+
* Read a feature gate. Returns `defaultValue` ONLY when the gate cannot be
|
|
699
|
+
* evaluated (client not initialized or flag not found) — never for a gate that
|
|
700
|
+
* legitimately evaluates to false. Plain `getFlag(name, user)` keeps returning
|
|
701
|
+
* false for a missing flag.
|
|
702
|
+
*/
|
|
703
|
+
getFlag(name, user, defaultValue = false) {
|
|
704
|
+
const d = this.getFlagDetail(name, user);
|
|
705
|
+
if (d.reason === "CLIENT_NOT_READY" || d.reason === "FLAG_NOT_FOUND") return defaultValue;
|
|
706
|
+
return d.value;
|
|
707
|
+
}
|
|
708
|
+
getConfig(name, decodeOrOpts) {
|
|
550
709
|
this.telemetry.emit("config", name);
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
710
|
+
const opts = typeof decodeOrOpts === "function" ? { decode: decodeOrOpts } : decodeOrOpts ?? {};
|
|
711
|
+
const has = this.configOverrides.has(name);
|
|
712
|
+
const raw = has ? this.configOverrides.get(name) : this.flagsBlob?.configs[name]?.value;
|
|
713
|
+
if (raw === void 0 && !has) {
|
|
714
|
+
return "defaultValue" in opts ? opts.defaultValue : void 0;
|
|
715
|
+
}
|
|
716
|
+
if (!opts.decode) return raw;
|
|
717
|
+
return opts.decode(raw);
|
|
555
718
|
}
|
|
556
719
|
getExperiment(name, user, defaultParams, decode) {
|
|
557
720
|
this.telemetry.emit("experiment", name);
|
|
@@ -560,6 +723,16 @@ var FlagsClient = class {
|
|
|
560
723
|
group: "control",
|
|
561
724
|
params: defaultParams
|
|
562
725
|
};
|
|
726
|
+
const ov = this.experimentOverrides.get(name);
|
|
727
|
+
if (ov) {
|
|
728
|
+
if (!decode) return { inExperiment: true, group: ov.group, params: ov.params };
|
|
729
|
+
try {
|
|
730
|
+
return { inExperiment: true, group: ov.group, params: decode(ov.params) };
|
|
731
|
+
} catch (err) {
|
|
732
|
+
console.warn(`[shipeasy] getExperiment('${name}') override decode failed:`, String(err));
|
|
733
|
+
return notIn;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
563
736
|
if (!this.flagsBlob || !this.expsBlob) return notIn;
|
|
564
737
|
const exp = this.expsBlob.experiments[name];
|
|
565
738
|
if (!exp || exp.status !== "running") return notIn;
|
|
@@ -597,6 +770,7 @@ var FlagsClient = class {
|
|
|
597
770
|
return notIn;
|
|
598
771
|
}
|
|
599
772
|
track(userId, eventName, props) {
|
|
773
|
+
if (this.testMode) return;
|
|
600
774
|
const body = JSON.stringify({
|
|
601
775
|
events: [
|
|
602
776
|
{
|
|
@@ -931,11 +1105,15 @@ var flags = {
|
|
|
931
1105
|
destroy() {
|
|
932
1106
|
_server?.destroy();
|
|
933
1107
|
},
|
|
934
|
-
get(name, user) {
|
|
935
|
-
return _server?.getFlag(name, user) ??
|
|
1108
|
+
get(name, user, defaultValue = false) {
|
|
1109
|
+
return _server?.getFlag(name, user, defaultValue) ?? defaultValue;
|
|
1110
|
+
},
|
|
1111
|
+
/** Evaluate a gate and report why (value + reason). See {@link FlagDetail}. */
|
|
1112
|
+
getDetail(name, user) {
|
|
1113
|
+
return _server?.getFlagDetail(name, user) ?? { value: false, reason: "CLIENT_NOT_READY" };
|
|
936
1114
|
},
|
|
937
|
-
getConfig(name,
|
|
938
|
-
return _server?.getConfig(name,
|
|
1115
|
+
getConfig(name, decodeOrOpts) {
|
|
1116
|
+
return _server?.getConfig(name, decodeOrOpts);
|
|
939
1117
|
},
|
|
940
1118
|
getExperiment(name, user, defaultParams, decode) {
|
|
941
1119
|
return _server?.getExperiment(name, user, defaultParams, decode) ?? {
|
|
@@ -980,12 +1158,14 @@ var see = Object.assign(
|
|
|
980
1158
|
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
981
1159
|
{
|
|
982
1160
|
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
983
|
-
ControlFlowException:
|
|
1161
|
+
ControlFlowException: (err) => startControlFlowChain(err)
|
|
984
1162
|
}
|
|
985
1163
|
);
|
|
986
1164
|
export {
|
|
987
1165
|
ANON_ID_COOKIE,
|
|
1166
|
+
FLAG_REASONS,
|
|
988
1167
|
FlagsClient,
|
|
1168
|
+
_murmur3ForTests,
|
|
989
1169
|
_resetShipeasyServerForTests,
|
|
990
1170
|
configureShipeasyServer,
|
|
991
1171
|
fetchLabelsForSSR,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://shipeasy.ai",
|