@shipeasy/sdk 5.4.0 → 6.2.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 +78 -313
- package/dist/client/index.d.mts +102 -16
- package/dist/client/index.d.ts +102 -16
- package/dist/client/index.js +81 -8
- package/dist/client/index.mjs +77 -7
- package/dist/openfeature-server/index.d.mts +33 -20
- package/dist/openfeature-server/index.d.ts +33 -20
- package/dist/openfeature-server/index.js +194 -4
- package/dist/openfeature-server/index.mjs +194 -4
- package/dist/openfeature-web/index.d.mts +12 -12
- package/dist/openfeature-web/index.d.ts +12 -12
- package/dist/server/index.d.mts +180 -21
- package/dist/server/index.d.ts +180 -21
- package/dist/server/index.js +159 -12
- package/dist/server/index.mjs +148 -11
- package/dist/skill-cli.js +56 -0
- package/docs/skill/SKILL.md +145 -0
- package/package.json +7 -1
|
@@ -78,7 +78,7 @@ interface StickyEntry {
|
|
|
78
78
|
/**
|
|
79
79
|
* Pluggable sticky-bucketing store for the server (doc 20 §2). Keyed by the
|
|
80
80
|
* bucketing unit; the value is that unit's per-experiment assignments. Absent
|
|
81
|
-
* from {@link
|
|
81
|
+
* from {@link EngineOptions} ⇒ today's deterministic behaviour. Use
|
|
82
82
|
* {@link createInMemoryStickyStore} or a cookie-bridge built from request
|
|
83
83
|
* cookies.
|
|
84
84
|
*/
|
|
@@ -93,7 +93,7 @@ interface Killswitch {
|
|
|
93
93
|
killed: 0 | 1 | boolean;
|
|
94
94
|
switches?: Record<string, 0 | 1 | boolean>;
|
|
95
95
|
}
|
|
96
|
-
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link
|
|
96
|
+
/** Body of `GET /sdk/flags` — the snapshot's `flags` field. See {@link Engine.fromSnapshot}. */
|
|
97
97
|
interface FlagsBlob {
|
|
98
98
|
version: string;
|
|
99
99
|
plan: string;
|
|
@@ -103,7 +103,7 @@ interface FlagsBlob {
|
|
|
103
103
|
}>;
|
|
104
104
|
killswitches: Record<string, Killswitch>;
|
|
105
105
|
}
|
|
106
|
-
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link
|
|
106
|
+
/** Body of `GET /sdk/experiments` — the snapshot's `experiments` field. See {@link Engine.fromSnapshot}. */
|
|
107
107
|
interface ExpsBlob {
|
|
108
108
|
version: string;
|
|
109
109
|
universes: Record<string, Universe>;
|
|
@@ -115,12 +115,12 @@ interface BootstrapPayload {
|
|
|
115
115
|
experiments: Record<string, ExperimentResult<Record<string, unknown>>>;
|
|
116
116
|
killswitches: Record<string, boolean | Record<string, boolean>>;
|
|
117
117
|
}
|
|
118
|
-
type
|
|
119
|
-
interface
|
|
118
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
119
|
+
interface EngineOptions {
|
|
120
120
|
apiKey: string;
|
|
121
121
|
baseUrl?: string;
|
|
122
122
|
/** Which published env to read values from. Defaults to "prod". */
|
|
123
|
-
env?:
|
|
123
|
+
env?: EngineEnv;
|
|
124
124
|
/**
|
|
125
125
|
* Preload the flags blob synchronously without a network fetch. Primarily
|
|
126
126
|
* for tests; production callers should rely on init()/initOnce().
|
|
@@ -155,12 +155,12 @@ interface FlagsClientOptions {
|
|
|
155
155
|
/**
|
|
156
156
|
* Test mode — no network at all. init()/initOnce() are no-ops (never fetch),
|
|
157
157
|
* track() is a no-op, telemetry is forced off, and the client starts
|
|
158
|
-
* "initialized" with an empty blob. Prefer the {@link
|
|
158
|
+
* "initialized" with an empty blob. Prefer the {@link Engine.forTesting}
|
|
159
159
|
* factory over passing this directly.
|
|
160
160
|
*/
|
|
161
161
|
testMode?: boolean;
|
|
162
162
|
}
|
|
163
|
-
declare class
|
|
163
|
+
declare class Engine {
|
|
164
164
|
private readonly apiKey;
|
|
165
165
|
private readonly baseUrl;
|
|
166
166
|
private readonly env;
|
|
@@ -180,7 +180,7 @@ declare class FlagsClient {
|
|
|
180
180
|
private readonly configOverrides;
|
|
181
181
|
private readonly experimentOverrides;
|
|
182
182
|
private readonly changeListeners;
|
|
183
|
-
constructor(opts:
|
|
183
|
+
constructor(opts: EngineOptions);
|
|
184
184
|
/**
|
|
185
185
|
* Build a no-network, immediately-usable client for tests (Statsig-style).
|
|
186
186
|
* init()/initOnce() are no-ops (never fetch), track() is a no-op, telemetry
|
|
@@ -188,12 +188,12 @@ declare class FlagsClient {
|
|
|
188
188
|
* with overrideFlag/overrideConfig/overrideExperiment. No SDK key required.
|
|
189
189
|
*
|
|
190
190
|
* ```ts
|
|
191
|
-
* const client =
|
|
191
|
+
* const client = Engine.forTesting();
|
|
192
192
|
* client.overrideFlag("new_checkout", true);
|
|
193
193
|
* client.getFlag("new_checkout", { user_id: "u1" }); // true
|
|
194
194
|
* ```
|
|
195
195
|
*/
|
|
196
|
-
static forTesting(opts?: Partial<
|
|
196
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
197
197
|
/**
|
|
198
198
|
* Build a fully OFFLINE client from a pre-captured snapshot — no network ever.
|
|
199
199
|
* Reuses the test-mode plumbing (init()/initOnce()/track() are no-ops,
|
|
@@ -207,14 +207,14 @@ declare class FlagsClient {
|
|
|
207
207
|
static fromSnapshot(snapshot: {
|
|
208
208
|
flags: FlagsBlob;
|
|
209
209
|
experiments: ExpsBlob;
|
|
210
|
-
}):
|
|
210
|
+
}): Engine;
|
|
211
211
|
/**
|
|
212
212
|
* Build a fully OFFLINE client from a snapshot JSON file on disk (Node only —
|
|
213
213
|
* not available in the browser entrypoint). The file must contain
|
|
214
214
|
* `{ "flags": <GET /sdk/flags body>, "experiments": <GET /sdk/experiments body> }`.
|
|
215
|
-
* See {@link
|
|
215
|
+
* See {@link Engine.fromSnapshot}.
|
|
216
216
|
*/
|
|
217
|
-
static fromFile(path: string):
|
|
217
|
+
static fromFile(path: string): Engine;
|
|
218
218
|
init(): Promise<void>;
|
|
219
219
|
initOnce(): Promise<void>;
|
|
220
220
|
/** Force `getFlag(name, …)` to return `value`, ignoring the fetched gate. */
|
|
@@ -298,32 +298,45 @@ declare class FlagsClient {
|
|
|
298
298
|
*
|
|
299
299
|
* ```ts
|
|
300
300
|
* import { OpenFeature } from "@openfeature/server-sdk";
|
|
301
|
-
* import {
|
|
301
|
+
* import { Engine } from "@shipeasy/sdk/server";
|
|
302
302
|
* import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-server";
|
|
303
303
|
*
|
|
304
|
-
* const client = new
|
|
304
|
+
* const client = new Engine({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
305
305
|
* await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
|
|
306
306
|
*
|
|
307
307
|
* const ofClient = OpenFeature.getClient();
|
|
308
308
|
* const on = await ofClient.getBooleanValue("new_checkout", false, { targetingKey: "u1" });
|
|
309
309
|
* ```
|
|
310
310
|
*
|
|
311
|
-
* Pure adapter over `
|
|
311
|
+
* Pure adapter over `Engine` — no change to evaluation. `@openfeature/server-sdk`
|
|
312
312
|
* is an optional peer dependency; install it in the consuming app.
|
|
313
313
|
*/
|
|
314
314
|
|
|
315
315
|
/**
|
|
316
|
-
* Shipeasy OpenFeature provider (server paradigm). Wraps
|
|
316
|
+
* Shipeasy OpenFeature provider (server paradigm). Wraps an `Engine`;
|
|
317
317
|
* evaluation is local against the cached blob, so resolution is effectively
|
|
318
318
|
* synchronous (the methods are `async` only to satisfy the server contract).
|
|
319
319
|
*/
|
|
320
320
|
declare class ShipeasyProvider implements Provider {
|
|
321
|
-
private readonly client;
|
|
322
321
|
readonly metadata: {
|
|
323
322
|
readonly name: "shipeasy";
|
|
324
323
|
};
|
|
325
324
|
readonly runsOn: "server";
|
|
326
|
-
|
|
325
|
+
private readonly client;
|
|
326
|
+
/**
|
|
327
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
328
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
329
|
+
* configuration without ever naming the `Engine`:
|
|
330
|
+
*
|
|
331
|
+
* ```ts
|
|
332
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
333
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
334
|
+
* ```
|
|
335
|
+
*
|
|
336
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
337
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
338
|
+
*/
|
|
339
|
+
constructor(client?: Engine);
|
|
327
340
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
328
341
|
initialize(): Promise<void>;
|
|
329
342
|
onClose(): Promise<void>;
|
|
@@ -25,6 +25,177 @@ __export(openfeature_server_exports, {
|
|
|
25
25
|
module.exports = __toCommonJS(openfeature_server_exports);
|
|
26
26
|
var import_server_sdk = require("@openfeature/server-sdk");
|
|
27
27
|
|
|
28
|
+
// src/server/index.ts
|
|
29
|
+
var import_node_async_hooks = require("async_hooks");
|
|
30
|
+
|
|
31
|
+
// src/see/core.ts
|
|
32
|
+
var SEE_MAX_SUBJECT = 200;
|
|
33
|
+
var SEE_MAX_EXTRA_VALUE = 200;
|
|
34
|
+
var SEE_MAX_EXTRA_KEYS = 20;
|
|
35
|
+
function causesThe(subject) {
|
|
36
|
+
return {
|
|
37
|
+
to(outcome) {
|
|
38
|
+
return {
|
|
39
|
+
__seConsequence: true,
|
|
40
|
+
subject: truncate(String(subject), SEE_MAX_SUBJECT),
|
|
41
|
+
outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function violation(name) {
|
|
47
|
+
return { __seViolation: true, violationName: String(name) };
|
|
48
|
+
}
|
|
49
|
+
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
50
|
+
function readExpectedMark(err) {
|
|
51
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
52
|
+
const v = err[EXPECTED_SYM];
|
|
53
|
+
return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
|
|
54
|
+
}
|
|
55
|
+
function markExpected(err, because, extras) {
|
|
56
|
+
if (typeof err !== "object" || err === null) return;
|
|
57
|
+
const prev = readExpectedMark(err);
|
|
58
|
+
const clean = sanitizeExtras(extras);
|
|
59
|
+
const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
|
|
60
|
+
const mark = {
|
|
61
|
+
because: String(because),
|
|
62
|
+
...merged ? { extras: merged } : {}
|
|
63
|
+
};
|
|
64
|
+
try {
|
|
65
|
+
Object.defineProperty(err, EXPECTED_SYM, {
|
|
66
|
+
value: mark,
|
|
67
|
+
enumerable: false,
|
|
68
|
+
configurable: true
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function truncate(s, max) {
|
|
74
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
75
|
+
}
|
|
76
|
+
function sanitizeExtras(extras) {
|
|
77
|
+
if (!extras || typeof extras !== "object") return void 0;
|
|
78
|
+
const out = {};
|
|
79
|
+
let n = 0;
|
|
80
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
81
|
+
if (v === null || v === void 0) continue;
|
|
82
|
+
if (n >= SEE_MAX_EXTRA_KEYS) break;
|
|
83
|
+
if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
|
|
84
|
+
else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
85
|
+
else if (typeof v === "boolean") out[k] = v;
|
|
86
|
+
else continue;
|
|
87
|
+
n += 1;
|
|
88
|
+
}
|
|
89
|
+
return n > 0 ? out : void 0;
|
|
90
|
+
}
|
|
91
|
+
var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
92
|
+
void Promise.resolve().then(cb);
|
|
93
|
+
};
|
|
94
|
+
function startSeeChain(getProblem, dispatch) {
|
|
95
|
+
let subject;
|
|
96
|
+
let outcome;
|
|
97
|
+
let collected;
|
|
98
|
+
let flushed = false;
|
|
99
|
+
scheduleMicrotask(() => {
|
|
100
|
+
if (flushed) return;
|
|
101
|
+
flushed = true;
|
|
102
|
+
dispatch(
|
|
103
|
+
getProblem(),
|
|
104
|
+
// Bare noun phrase — titles render as "… causes the {subject} …", so a
|
|
105
|
+
// leading article would double up ("causes the the app").
|
|
106
|
+
causesThe(subject ?? "app").to(outcome ?? "hit an error"),
|
|
107
|
+
collected
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
const tail = {
|
|
111
|
+
extras(x) {
|
|
112
|
+
if (x && typeof x === "object") collected = { ...collected, ...x };
|
|
113
|
+
return tail;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const step = {
|
|
117
|
+
to(o) {
|
|
118
|
+
outcome = String(o);
|
|
119
|
+
return tail;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const start = (s) => {
|
|
123
|
+
subject = String(s);
|
|
124
|
+
return step;
|
|
125
|
+
};
|
|
126
|
+
return { causes_the: start, causesThe: start };
|
|
127
|
+
}
|
|
128
|
+
function startSeeViolationChain(name, dispatch) {
|
|
129
|
+
return startSeeChain(() => violation(name), dispatch);
|
|
130
|
+
}
|
|
131
|
+
function startControlFlowChain(err) {
|
|
132
|
+
return {
|
|
133
|
+
because(reason) {
|
|
134
|
+
markExpected(err, reason);
|
|
135
|
+
const tail = {
|
|
136
|
+
extras(x) {
|
|
137
|
+
markExpected(err, reason, x);
|
|
138
|
+
return tail;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
return tail;
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/server/index.ts
|
|
147
|
+
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
148
|
+
var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
|
|
149
|
+
var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
|
|
150
|
+
var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
|
|
151
|
+
var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
|
|
152
|
+
globalThis[_I18N_SSR_SYM] = () => {
|
|
153
|
+
const fromALS = _i18nALS.getStore();
|
|
154
|
+
if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
|
|
155
|
+
for (const v of _i18nCache.values()) {
|
|
156
|
+
if (Object.keys(v.strings).length > 0) return v;
|
|
157
|
+
}
|
|
158
|
+
return fromALS ?? null;
|
|
159
|
+
};
|
|
160
|
+
var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
|
|
161
|
+
var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
|
|
162
|
+
var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
|
|
163
|
+
if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
|
|
164
|
+
globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
|
|
165
|
+
}
|
|
166
|
+
Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
|
|
167
|
+
get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
|
|
168
|
+
set: (v) => {
|
|
169
|
+
const b = Boolean(v);
|
|
170
|
+
try {
|
|
171
|
+
_editModeALS.enterWith(b);
|
|
172
|
+
} catch {
|
|
173
|
+
}
|
|
174
|
+
globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
|
|
175
|
+
},
|
|
176
|
+
configurable: true
|
|
177
|
+
});
|
|
178
|
+
var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
|
|
179
|
+
var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new import_node_async_hooks.AsyncLocalStorage());
|
|
180
|
+
var _server = null;
|
|
181
|
+
function getShipeasyServerClient() {
|
|
182
|
+
return _server;
|
|
183
|
+
}
|
|
184
|
+
function dispatchSee(problem, consequence, extras, kind) {
|
|
185
|
+
if (!_server) {
|
|
186
|
+
console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
_server.reportError(problem, consequence, extras, kind);
|
|
190
|
+
}
|
|
191
|
+
var see = Object.assign(
|
|
192
|
+
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
193
|
+
{
|
|
194
|
+
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
195
|
+
ControlFlowException: (err) => startControlFlowChain(err)
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
28
199
|
// src/openfeature/shared.ts
|
|
29
200
|
var REASON_MAP = {
|
|
30
201
|
RULE_MATCH: { reason: "TARGETING_MATCH" },
|
|
@@ -78,12 +249,31 @@ function resolveConfig(raw, defaultValue, type) {
|
|
|
78
249
|
return { value: r.value, reason: "TARGETING_MATCH" };
|
|
79
250
|
}
|
|
80
251
|
var ShipeasyProvider = class {
|
|
81
|
-
constructor(client) {
|
|
82
|
-
this.client = client;
|
|
83
|
-
}
|
|
84
|
-
client;
|
|
85
252
|
metadata = { name: "shipeasy" };
|
|
86
253
|
runsOn = "server";
|
|
254
|
+
client;
|
|
255
|
+
/**
|
|
256
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
257
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
258
|
+
* configuration without ever naming the `Engine`:
|
|
259
|
+
*
|
|
260
|
+
* ```ts
|
|
261
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
262
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
263
|
+
* ```
|
|
264
|
+
*
|
|
265
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
266
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
267
|
+
*/
|
|
268
|
+
constructor(client) {
|
|
269
|
+
const resolved = client ?? getShipeasyServerClient();
|
|
270
|
+
if (!resolved) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
"[shipeasy] new ShipeasyProvider() resolves the configured global engine \u2014 call configure({ apiKey }) first, or pass an Engine explicitly."
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
this.client = resolved;
|
|
276
|
+
}
|
|
87
277
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
88
278
|
async initialize() {
|
|
89
279
|
await this.client.initOnce();
|
|
@@ -1,6 +1,177 @@
|
|
|
1
1
|
// src/openfeature-server/index.ts
|
|
2
2
|
import { ErrorCode } from "@openfeature/server-sdk";
|
|
3
3
|
|
|
4
|
+
// src/server/index.ts
|
|
5
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
6
|
+
|
|
7
|
+
// src/see/core.ts
|
|
8
|
+
var SEE_MAX_SUBJECT = 200;
|
|
9
|
+
var SEE_MAX_EXTRA_VALUE = 200;
|
|
10
|
+
var SEE_MAX_EXTRA_KEYS = 20;
|
|
11
|
+
function causesThe(subject) {
|
|
12
|
+
return {
|
|
13
|
+
to(outcome) {
|
|
14
|
+
return {
|
|
15
|
+
__seConsequence: true,
|
|
16
|
+
subject: truncate(String(subject), SEE_MAX_SUBJECT),
|
|
17
|
+
outcome: truncate(String(outcome), SEE_MAX_SUBJECT)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function violation(name) {
|
|
23
|
+
return { __seViolation: true, violationName: String(name) };
|
|
24
|
+
}
|
|
25
|
+
var EXPECTED_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-expected");
|
|
26
|
+
function readExpectedMark(err) {
|
|
27
|
+
if (typeof err !== "object" || err === null) return void 0;
|
|
28
|
+
const v = err[EXPECTED_SYM];
|
|
29
|
+
return v !== void 0 && v !== null && typeof v === "object" ? v : void 0;
|
|
30
|
+
}
|
|
31
|
+
function markExpected(err, because, extras) {
|
|
32
|
+
if (typeof err !== "object" || err === null) return;
|
|
33
|
+
const prev = readExpectedMark(err);
|
|
34
|
+
const clean = sanitizeExtras(extras);
|
|
35
|
+
const merged = prev?.extras || clean ? { ...prev?.extras, ...clean } : void 0;
|
|
36
|
+
const mark = {
|
|
37
|
+
because: String(because),
|
|
38
|
+
...merged ? { extras: merged } : {}
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
Object.defineProperty(err, EXPECTED_SYM, {
|
|
42
|
+
value: mark,
|
|
43
|
+
enumerable: false,
|
|
44
|
+
configurable: true
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function truncate(s, max) {
|
|
50
|
+
return s.length > max ? s.slice(0, max) : s;
|
|
51
|
+
}
|
|
52
|
+
function sanitizeExtras(extras) {
|
|
53
|
+
if (!extras || typeof extras !== "object") return void 0;
|
|
54
|
+
const out = {};
|
|
55
|
+
let n = 0;
|
|
56
|
+
for (const [k, v] of Object.entries(extras)) {
|
|
57
|
+
if (v === null || v === void 0) continue;
|
|
58
|
+
if (n >= SEE_MAX_EXTRA_KEYS) break;
|
|
59
|
+
if (typeof v === "string") out[k] = truncate(v, SEE_MAX_EXTRA_VALUE);
|
|
60
|
+
else if (typeof v === "number" && Number.isFinite(v)) out[k] = v;
|
|
61
|
+
else if (typeof v === "boolean") out[k] = v;
|
|
62
|
+
else continue;
|
|
63
|
+
n += 1;
|
|
64
|
+
}
|
|
65
|
+
return n > 0 ? out : void 0;
|
|
66
|
+
}
|
|
67
|
+
var scheduleMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : (cb) => {
|
|
68
|
+
void Promise.resolve().then(cb);
|
|
69
|
+
};
|
|
70
|
+
function startSeeChain(getProblem, dispatch) {
|
|
71
|
+
let subject;
|
|
72
|
+
let outcome;
|
|
73
|
+
let collected;
|
|
74
|
+
let flushed = false;
|
|
75
|
+
scheduleMicrotask(() => {
|
|
76
|
+
if (flushed) return;
|
|
77
|
+
flushed = true;
|
|
78
|
+
dispatch(
|
|
79
|
+
getProblem(),
|
|
80
|
+
// Bare noun phrase — titles render as "… causes the {subject} …", so a
|
|
81
|
+
// leading article would double up ("causes the the app").
|
|
82
|
+
causesThe(subject ?? "app").to(outcome ?? "hit an error"),
|
|
83
|
+
collected
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
const tail = {
|
|
87
|
+
extras(x) {
|
|
88
|
+
if (x && typeof x === "object") collected = { ...collected, ...x };
|
|
89
|
+
return tail;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const step = {
|
|
93
|
+
to(o) {
|
|
94
|
+
outcome = String(o);
|
|
95
|
+
return tail;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const start = (s) => {
|
|
99
|
+
subject = String(s);
|
|
100
|
+
return step;
|
|
101
|
+
};
|
|
102
|
+
return { causes_the: start, causesThe: start };
|
|
103
|
+
}
|
|
104
|
+
function startSeeViolationChain(name, dispatch) {
|
|
105
|
+
return startSeeChain(() => violation(name), dispatch);
|
|
106
|
+
}
|
|
107
|
+
function startControlFlowChain(err) {
|
|
108
|
+
return {
|
|
109
|
+
because(reason) {
|
|
110
|
+
markExpected(err, reason);
|
|
111
|
+
const tail = {
|
|
112
|
+
extras(x) {
|
|
113
|
+
markExpected(err, reason, x);
|
|
114
|
+
return tail;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return tail;
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/server/index.ts
|
|
123
|
+
var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
124
|
+
var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
|
|
125
|
+
var _i18nALS = new AsyncLocalStorage();
|
|
126
|
+
var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
|
|
127
|
+
var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
|
|
128
|
+
globalThis[_I18N_SSR_SYM] = () => {
|
|
129
|
+
const fromALS = _i18nALS.getStore();
|
|
130
|
+
if (fromALS && Object.keys(fromALS.strings).length > 0) return fromALS;
|
|
131
|
+
for (const v of _i18nCache.values()) {
|
|
132
|
+
if (Object.keys(v.strings).length > 0) return v;
|
|
133
|
+
}
|
|
134
|
+
return fromALS ?? null;
|
|
135
|
+
};
|
|
136
|
+
var _EDIT_MODE_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-als");
|
|
137
|
+
var _editModeALS = globalThis[_EDIT_MODE_ALS_SYM] ?? (globalThis[_EDIT_MODE_ALS_SYM] = new AsyncLocalStorage());
|
|
138
|
+
var _EDIT_MODE_FALLBACK_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode-fallback");
|
|
139
|
+
if (globalThis[_EDIT_MODE_FALLBACK_SYM] === void 0) {
|
|
140
|
+
globalThis[_EDIT_MODE_FALLBACK_SYM] = false;
|
|
141
|
+
}
|
|
142
|
+
Object.defineProperty(globalThis, _EDIT_MODE_SSR_SYM, {
|
|
143
|
+
get: () => _editModeALS.getStore() ?? globalThis[_EDIT_MODE_FALLBACK_SYM] ?? false,
|
|
144
|
+
set: (v) => {
|
|
145
|
+
const b = Boolean(v);
|
|
146
|
+
try {
|
|
147
|
+
_editModeALS.enterWith(b);
|
|
148
|
+
} catch {
|
|
149
|
+
}
|
|
150
|
+
globalThis[_EDIT_MODE_FALLBACK_SYM] = b;
|
|
151
|
+
},
|
|
152
|
+
configurable: true
|
|
153
|
+
});
|
|
154
|
+
var _SEE_CORR_ALS_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:see-correlation-als");
|
|
155
|
+
var seeContext = globalThis[_SEE_CORR_ALS_SYM] ?? (globalThis[_SEE_CORR_ALS_SYM] = new AsyncLocalStorage());
|
|
156
|
+
var _server = null;
|
|
157
|
+
function getShipeasyServerClient() {
|
|
158
|
+
return _server;
|
|
159
|
+
}
|
|
160
|
+
function dispatchSee(problem, consequence, extras, kind) {
|
|
161
|
+
if (!_server) {
|
|
162
|
+
console.warn("[shipeasy] see() called before shipeasy({ serverKey }) \u2014 error dropped");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
_server.reportError(problem, consequence, extras, kind);
|
|
166
|
+
}
|
|
167
|
+
var see = Object.assign(
|
|
168
|
+
(problem) => startSeeChain(() => problem, dispatchSee),
|
|
169
|
+
{
|
|
170
|
+
Violation: (name) => startSeeViolationChain(name, dispatchSee),
|
|
171
|
+
ControlFlowException: (err) => startControlFlowChain(err)
|
|
172
|
+
}
|
|
173
|
+
);
|
|
174
|
+
|
|
4
175
|
// src/openfeature/shared.ts
|
|
5
176
|
var REASON_MAP = {
|
|
6
177
|
RULE_MATCH: { reason: "TARGETING_MATCH" },
|
|
@@ -54,12 +225,31 @@ function resolveConfig(raw, defaultValue, type) {
|
|
|
54
225
|
return { value: r.value, reason: "TARGETING_MATCH" };
|
|
55
226
|
}
|
|
56
227
|
var ShipeasyProvider = class {
|
|
57
|
-
constructor(client) {
|
|
58
|
-
this.client = client;
|
|
59
|
-
}
|
|
60
|
-
client;
|
|
61
228
|
metadata = { name: "shipeasy" };
|
|
62
229
|
runsOn = "server";
|
|
230
|
+
client;
|
|
231
|
+
/**
|
|
232
|
+
* Construct the provider. The **global form** (no argument) resolves the
|
|
233
|
+
* engine built by `configure({ apiKey })`, so the docs build it after
|
|
234
|
+
* configuration without ever naming the `Engine`:
|
|
235
|
+
*
|
|
236
|
+
* ```ts
|
|
237
|
+
* configure({ apiKey: process.env.SHIPEASY_SERVER_KEY! });
|
|
238
|
+
* await OpenFeature.setProviderAndWait(new ShipeasyProvider());
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* Passing an explicit `Engine` stays supported for advanced/back-compat use.
|
|
242
|
+
* Throws if no engine is passed and `configure()` has not run.
|
|
243
|
+
*/
|
|
244
|
+
constructor(client) {
|
|
245
|
+
const resolved = client ?? getShipeasyServerClient();
|
|
246
|
+
if (!resolved) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"[shipeasy] new ShipeasyProvider() resolves the configured global engine \u2014 call configure({ apiKey }) first, or pass an Engine explicitly."
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
this.client = resolved;
|
|
252
|
+
}
|
|
63
253
|
/** Fetch the rules blob once. The SDK fires `Ready` when this resolves. */
|
|
64
254
|
async initialize() {
|
|
65
255
|
await this.client.initOnce();
|
|
@@ -101,8 +101,8 @@ interface AutoCollectGroups {
|
|
|
101
101
|
errors: boolean;
|
|
102
102
|
engagement: boolean;
|
|
103
103
|
}
|
|
104
|
-
type
|
|
105
|
-
interface
|
|
104
|
+
type EngineEnv = "dev" | "staging" | "prod";
|
|
105
|
+
interface EngineOptions {
|
|
106
106
|
sdkKey: string;
|
|
107
107
|
baseUrl?: string;
|
|
108
108
|
autoGuardrails?: boolean;
|
|
@@ -121,7 +121,7 @@ interface FlagsClientBrowserOptions {
|
|
|
121
121
|
*/
|
|
122
122
|
autoCollectAlways?: boolean;
|
|
123
123
|
/** Which published env to read values from. Defaults to "prod". */
|
|
124
|
-
env?:
|
|
124
|
+
env?: EngineEnv;
|
|
125
125
|
/**
|
|
126
126
|
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
127
127
|
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
@@ -158,11 +158,11 @@ interface FlagsClientBrowserOptions {
|
|
|
158
158
|
* Test mode — no network at all. identify()/init are no-ops (never call
|
|
159
159
|
* /sdk/evaluate), track() is a no-op, telemetry is forced off, and the client
|
|
160
160
|
* starts "ready" with an empty eval result. Prefer the
|
|
161
|
-
* {@link
|
|
161
|
+
* {@link Engine.forTesting} factory over passing this directly.
|
|
162
162
|
*/
|
|
163
163
|
testMode?: boolean;
|
|
164
164
|
}
|
|
165
|
-
declare class
|
|
165
|
+
declare class Engine {
|
|
166
166
|
private readonly sdkKey;
|
|
167
167
|
private readonly baseUrl;
|
|
168
168
|
private readonly autoGuardrails;
|
|
@@ -187,7 +187,7 @@ declare class FlagsClientBrowser {
|
|
|
187
187
|
private readonly configOverrides;
|
|
188
188
|
private readonly experimentOverrides;
|
|
189
189
|
private onOverrideChange;
|
|
190
|
-
constructor(opts:
|
|
190
|
+
constructor(opts: EngineOptions);
|
|
191
191
|
/**
|
|
192
192
|
* Build a no-network, immediately-usable browser client for tests
|
|
193
193
|
* (Statsig-style). identify() is a no-op (never calls /sdk/evaluate), track()
|
|
@@ -196,12 +196,12 @@ declare class FlagsClientBrowser {
|
|
|
196
196
|
* key required.
|
|
197
197
|
*
|
|
198
198
|
* ```ts
|
|
199
|
-
* const client =
|
|
199
|
+
* const client = Engine.forTesting();
|
|
200
200
|
* client.overrideFlag("new_checkout", true);
|
|
201
201
|
* client.getFlag("new_checkout"); // true
|
|
202
202
|
* ```
|
|
203
203
|
*/
|
|
204
|
-
static forTesting(opts?: Partial<
|
|
204
|
+
static forTesting(opts?: Partial<EngineOptions>): Engine;
|
|
205
205
|
identify(user: User): Promise<void>;
|
|
206
206
|
/**
|
|
207
207
|
* Report a structured error into the errors primitive. Flushes immediately
|
|
@@ -289,10 +289,10 @@ interface ShipeasySdkBridge {
|
|
|
289
289
|
*
|
|
290
290
|
* ```ts
|
|
291
291
|
* import { OpenFeature } from "@openfeature/web-sdk";
|
|
292
|
-
* import {
|
|
292
|
+
* import { Engine } from "@shipeasy/sdk/client";
|
|
293
293
|
* import { ShipeasyProvider } from "@shipeasy/sdk/openfeature-web";
|
|
294
294
|
*
|
|
295
|
-
* const client = new
|
|
295
|
+
* const client = new Engine({ sdkKey: NEXT_PUBLIC_SHIPEASY_CLIENT_KEY });
|
|
296
296
|
* await OpenFeature.setContext({ targetingKey: "u1", plan: "pro" });
|
|
297
297
|
* await OpenFeature.setProviderAndWait(new ShipeasyProvider(client));
|
|
298
298
|
*
|
|
@@ -307,7 +307,7 @@ interface ShipeasySdkBridge {
|
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
309
|
* Shipeasy OpenFeature provider (client/web paradigm). Wraps a
|
|
310
|
-
* `
|
|
310
|
+
* `Engine`. Context changes are reconciled through `identify()`;
|
|
311
311
|
* flag reads come from the cached eval result.
|
|
312
312
|
*/
|
|
313
313
|
declare class ShipeasyProvider implements Provider {
|
|
@@ -316,7 +316,7 @@ declare class ShipeasyProvider implements Provider {
|
|
|
316
316
|
readonly name: "shipeasy";
|
|
317
317
|
};
|
|
318
318
|
readonly runsOn: "client";
|
|
319
|
-
constructor(client:
|
|
319
|
+
constructor(client: Engine);
|
|
320
320
|
/** Identify with the initial static context so the first read is warm. */
|
|
321
321
|
initialize(context?: EvaluationContext): Promise<void>;
|
|
322
322
|
/** Re-identify when the application author changes the static context. */
|