@shipeasy/sdk 1.0.0 → 1.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/LICENSE +40 -0
- package/README.md +94 -0
- package/dist/client/index.d.mts +132 -7
- package/dist/client/index.d.ts +132 -7
- package/dist/client/index.js +487 -25
- package/dist/client/index.mjs +476 -25
- package/dist/server/index.d.mts +24 -1
- package/dist/server/index.d.ts +24 -1
- package/dist/server/index.js +62 -1
- package/dist/server/index.mjs +58 -1
- package/package.json +30 -6
package/dist/client/index.mjs
CHANGED
|
@@ -123,13 +123,15 @@ var EventBuffer = class {
|
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
};
|
|
126
|
+
var MAX_ERRORS_PER_SESSION = 5;
|
|
126
127
|
function installAutoGuardrails(buffer, userId, anonId) {
|
|
127
128
|
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
128
129
|
let lcp = null;
|
|
129
130
|
let inp = null;
|
|
130
131
|
let clsBad = false;
|
|
131
|
-
let
|
|
132
|
-
let
|
|
132
|
+
let jsErrorCount = 0;
|
|
133
|
+
let netErrorCount = 0;
|
|
134
|
+
let navTimingFlushed = false;
|
|
133
135
|
try {
|
|
134
136
|
const lcpObs = new PerformanceObserver((list) => {
|
|
135
137
|
const entries = list.getEntries();
|
|
@@ -163,30 +165,112 @@ function installAutoGuardrails(buffer, userId, anonId) {
|
|
|
163
165
|
} catch {
|
|
164
166
|
}
|
|
165
167
|
const origOnError = window.onerror;
|
|
166
|
-
window.onerror = (
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
buffer.pushMetric("__auto_js_error", userId, anonId, {
|
|
168
|
+
window.onerror = (msg, source, lineno, _colno, err) => {
|
|
169
|
+
if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
|
|
170
|
+
jsErrorCount += 1;
|
|
171
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, {
|
|
172
|
+
value: 1,
|
|
173
|
+
kind: "exception",
|
|
174
|
+
message: typeof msg === "string" ? msg.slice(0, 200) : String(err ?? "").slice(0, 200),
|
|
175
|
+
source: typeof source === "string" ? source.slice(0, 200) : "",
|
|
176
|
+
line: lineno ?? 0
|
|
177
|
+
});
|
|
170
178
|
}
|
|
171
|
-
if (typeof origOnError === "function") return origOnError(
|
|
179
|
+
if (typeof origOnError === "function") return origOnError(msg, source, lineno, _colno, err);
|
|
172
180
|
return false;
|
|
173
181
|
};
|
|
174
|
-
window.addEventListener("unhandledrejection", () => {
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
window.addEventListener("unhandledrejection", (e) => {
|
|
183
|
+
if (jsErrorCount < MAX_ERRORS_PER_SESSION) {
|
|
184
|
+
jsErrorCount += 1;
|
|
185
|
+
const reason = e.reason;
|
|
186
|
+
const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : String(reason);
|
|
187
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, {
|
|
188
|
+
value: 1,
|
|
189
|
+
kind: "unhandled_rejection",
|
|
190
|
+
message: message.slice(0, 200)
|
|
191
|
+
});
|
|
178
192
|
}
|
|
179
193
|
});
|
|
180
194
|
const origFetch = window.fetch;
|
|
181
195
|
window.fetch = async function(...args) {
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
196
|
+
const startedAt = typeof performance !== "undefined" ? performance.now() : 0;
|
|
197
|
+
const url = typeof args[0] === "string" ? args[0] : args[0].toString();
|
|
198
|
+
let res;
|
|
199
|
+
try {
|
|
200
|
+
res = await origFetch.apply(this, args);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (netErrorCount < MAX_ERRORS_PER_SESSION) {
|
|
203
|
+
netErrorCount += 1;
|
|
204
|
+
buffer.pushMetric("__auto_network_error", userId, anonId, {
|
|
205
|
+
value: 1,
|
|
206
|
+
kind: "network",
|
|
207
|
+
status: 0,
|
|
208
|
+
url: url.slice(0, 200)
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
throw err;
|
|
212
|
+
}
|
|
213
|
+
if (res.status >= 500 && netErrorCount < MAX_ERRORS_PER_SESSION) {
|
|
214
|
+
netErrorCount += 1;
|
|
215
|
+
const elapsed = typeof performance !== "undefined" ? performance.now() - startedAt : 0;
|
|
216
|
+
buffer.pushMetric("__auto_network_error", userId, anonId, {
|
|
217
|
+
value: 1,
|
|
218
|
+
kind: "5xx",
|
|
219
|
+
status: res.status,
|
|
220
|
+
url: url.slice(0, 200),
|
|
221
|
+
duration_ms: Math.round(elapsed)
|
|
222
|
+
});
|
|
186
223
|
}
|
|
187
224
|
return res;
|
|
188
225
|
};
|
|
189
|
-
const
|
|
226
|
+
const flushNavTiming = () => {
|
|
227
|
+
if (navTimingFlushed) return;
|
|
228
|
+
navTimingFlushed = true;
|
|
229
|
+
try {
|
|
230
|
+
const navList = performance.getEntriesByType("navigation");
|
|
231
|
+
const nav = navList[0];
|
|
232
|
+
if (nav) {
|
|
233
|
+
const start = nav.startTime ?? 0;
|
|
234
|
+
if (nav.loadEventEnd > 0) {
|
|
235
|
+
buffer.pushMetric("__auto_page_load", userId, anonId, {
|
|
236
|
+
value: nav.loadEventEnd - start
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (nav.responseStart > 0) {
|
|
240
|
+
buffer.pushMetric("__auto_ttfb", userId, anonId, {
|
|
241
|
+
value: nav.responseStart - start
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (nav.domContentLoadedEventEnd > 0) {
|
|
245
|
+
buffer.pushMetric("__auto_dom_ready", userId, anonId, {
|
|
246
|
+
value: nav.domContentLoadedEventEnd - start
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const paints = performance.getEntriesByType("paint");
|
|
251
|
+
for (const p of paints) {
|
|
252
|
+
if (p.name === "first-paint") {
|
|
253
|
+
buffer.pushMetric("__auto_fp", userId, anonId, { value: p.startTime });
|
|
254
|
+
} else if (p.name === "first-contentful-paint") {
|
|
255
|
+
buffer.pushMetric("__auto_fcp", userId, anonId, { value: p.startTime });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
if (document.readyState === "complete") {
|
|
262
|
+
setTimeout(flushNavTiming, 0);
|
|
263
|
+
} else {
|
|
264
|
+
window.addEventListener(
|
|
265
|
+
"load",
|
|
266
|
+
() => {
|
|
267
|
+
setTimeout(flushNavTiming, 0);
|
|
268
|
+
},
|
|
269
|
+
{ once: true }
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
const flushOnHide = () => {
|
|
273
|
+
flushNavTiming();
|
|
190
274
|
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
191
275
|
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
192
276
|
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
@@ -195,7 +279,7 @@ function installAutoGuardrails(buffer, userId, anonId) {
|
|
|
195
279
|
buffer.flush(true);
|
|
196
280
|
};
|
|
197
281
|
document.addEventListener("visibilitychange", () => {
|
|
198
|
-
if (document.visibilityState === "hidden")
|
|
282
|
+
if (document.visibilityState === "hidden") flushOnHide();
|
|
199
283
|
});
|
|
200
284
|
}
|
|
201
285
|
function getOrCreateAnonId() {
|
|
@@ -211,18 +295,75 @@ function getOrCreateAnonId() {
|
|
|
211
295
|
}
|
|
212
296
|
return id;
|
|
213
297
|
}
|
|
298
|
+
function collectBrowserAttrs() {
|
|
299
|
+
if (typeof window === "undefined") return {};
|
|
300
|
+
const attrs = {};
|
|
301
|
+
try {
|
|
302
|
+
if (typeof navigator !== "undefined" && navigator.language) attrs.locale = navigator.language;
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
try {
|
|
306
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
307
|
+
if (tz) attrs.timezone = tz;
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
if (document.referrer) attrs.referrer = document.referrer;
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
attrs.path = window.location.pathname;
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
if (window.screen) {
|
|
320
|
+
attrs.screen_width = window.screen.width;
|
|
321
|
+
attrs.screen_height = window.screen.height;
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
if (typeof navigator !== "undefined" && typeof navigator.userAgent === "string") {
|
|
327
|
+
attrs.user_agent = navigator.userAgent;
|
|
328
|
+
}
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
return attrs;
|
|
332
|
+
}
|
|
333
|
+
function readExperimentOverridesFromUrl() {
|
|
334
|
+
if (typeof window === "undefined") return {};
|
|
335
|
+
const out = {};
|
|
336
|
+
try {
|
|
337
|
+
const params = new URLSearchParams(window.location.search);
|
|
338
|
+
for (const [k, v] of params) {
|
|
339
|
+
if (!v || v === "default" || v === "none") continue;
|
|
340
|
+
if (k.startsWith("se_exp_")) out[k.slice("se_exp_".length)] = v;
|
|
341
|
+
else if (k.startsWith("se-exp-")) out[k.slice("se-exp-".length)] = v;
|
|
342
|
+
}
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
214
347
|
var FlagsClientBrowser = class {
|
|
215
348
|
sdkKey;
|
|
216
349
|
baseUrl;
|
|
217
350
|
autoGuardrails;
|
|
351
|
+
env;
|
|
218
352
|
evalResult = null;
|
|
219
353
|
anonId;
|
|
220
354
|
userId = "";
|
|
221
355
|
buffer;
|
|
222
356
|
guardrailsInstalled = false;
|
|
357
|
+
listeners = /* @__PURE__ */ new Set();
|
|
358
|
+
overrideListenerInstalled = false;
|
|
359
|
+
onOverrideChange = () => {
|
|
360
|
+
this.installBridge();
|
|
361
|
+
this.notify();
|
|
362
|
+
};
|
|
223
363
|
constructor(opts) {
|
|
224
364
|
this.sdkKey = opts.sdkKey;
|
|
225
365
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
366
|
+
this.env = opts.env ?? "prod";
|
|
226
367
|
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
227
368
|
this.anonId = getOrCreateAnonId();
|
|
228
369
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
@@ -234,10 +375,18 @@ var FlagsClientBrowser = class {
|
|
|
234
375
|
if (this.anonId && this.userId && this.userId !== prevUserId) {
|
|
235
376
|
await this.buffer.alias(this.anonId, this.userId);
|
|
236
377
|
}
|
|
237
|
-
const
|
|
378
|
+
const userPayload = {
|
|
379
|
+
...collectBrowserAttrs(),
|
|
380
|
+
anonymous_id: this.anonId,
|
|
381
|
+
...user
|
|
382
|
+
};
|
|
383
|
+
const res = await fetch(`${this.baseUrl}/sdk/evaluate?env=${this.env}`, {
|
|
238
384
|
method: "POST",
|
|
239
385
|
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
240
|
-
body: JSON.stringify({
|
|
386
|
+
body: JSON.stringify({
|
|
387
|
+
user: userPayload,
|
|
388
|
+
experiment_overrides: readExperimentOverridesFromUrl()
|
|
389
|
+
})
|
|
241
390
|
});
|
|
242
391
|
if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
|
|
243
392
|
this.evalResult = await res.json();
|
|
@@ -245,26 +394,54 @@ var FlagsClientBrowser = class {
|
|
|
245
394
|
this.guardrailsInstalled = true;
|
|
246
395
|
installAutoGuardrails(this.buffer, this.userId, this.anonId);
|
|
247
396
|
}
|
|
397
|
+
this.notify();
|
|
398
|
+
}
|
|
399
|
+
get ready() {
|
|
400
|
+
return this.evalResult !== null;
|
|
401
|
+
}
|
|
402
|
+
notify() {
|
|
403
|
+
for (const l of this.listeners) {
|
|
404
|
+
try {
|
|
405
|
+
l();
|
|
406
|
+
} catch (err) {
|
|
407
|
+
console.warn("[shipeasy] subscriber threw:", String(err));
|
|
408
|
+
}
|
|
409
|
+
}
|
|
248
410
|
}
|
|
249
411
|
initFromBootstrap(data) {
|
|
250
412
|
this.evalResult = data;
|
|
251
413
|
}
|
|
252
414
|
getFlag(name) {
|
|
253
|
-
|
|
415
|
+
const ov = readGateOverride(name);
|
|
416
|
+
if (ov !== null) return ov;
|
|
417
|
+
return this.evalResult?.flags[name] ?? false;
|
|
254
418
|
}
|
|
255
419
|
getConfig(name, decode) {
|
|
256
|
-
|
|
257
|
-
void
|
|
258
|
-
return void 0;
|
|
420
|
+
const ov = readConfigOverride(name);
|
|
421
|
+
const raw = ov !== void 0 ? ov : this.evalResult?.configs?.[name];
|
|
422
|
+
if (raw === void 0) return void 0;
|
|
423
|
+
if (!decode) return raw;
|
|
424
|
+
try {
|
|
425
|
+
return decode(raw);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.warn(`[shipeasy] getConfig('${name}') decode failed:`, String(err));
|
|
428
|
+
return void 0;
|
|
429
|
+
}
|
|
259
430
|
}
|
|
260
|
-
getExperiment(name, defaultParams, decode) {
|
|
431
|
+
getExperiment(name, defaultParams, decode, variants) {
|
|
261
432
|
const notIn = {
|
|
262
433
|
inExperiment: false,
|
|
263
434
|
group: "control",
|
|
264
435
|
params: defaultParams
|
|
265
436
|
};
|
|
437
|
+
const ov = readExpOverride(name);
|
|
438
|
+
if (ov !== null) {
|
|
439
|
+
const variantParams = variants?.[ov];
|
|
440
|
+
const params = variantParams ? { ...defaultParams, ...variantParams } : defaultParams;
|
|
441
|
+
return { inExperiment: true, group: ov, params };
|
|
442
|
+
}
|
|
266
443
|
const entry = this.evalResult?.experiments[name];
|
|
267
|
-
if (!entry || !entry.
|
|
444
|
+
if (!entry || !entry.inExperiment) return notIn;
|
|
268
445
|
this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
|
|
269
446
|
if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
|
|
270
447
|
try {
|
|
@@ -274,6 +451,39 @@ var FlagsClientBrowser = class {
|
|
|
274
451
|
return notIn;
|
|
275
452
|
}
|
|
276
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Subscribe to state changes — fires after identify() completes and on
|
|
456
|
+
* `se:override:change` events from the devtools overlay. Returns an
|
|
457
|
+
* unsubscribe function. Used by framework adapters to trigger re-renders.
|
|
458
|
+
*/
|
|
459
|
+
subscribe(listener) {
|
|
460
|
+
this.listeners.add(listener);
|
|
461
|
+
if (!this.overrideListenerInstalled && typeof window !== "undefined") {
|
|
462
|
+
this.overrideListenerInstalled = true;
|
|
463
|
+
window.addEventListener("se:override:change", this.onOverrideChange);
|
|
464
|
+
}
|
|
465
|
+
return () => {
|
|
466
|
+
this.listeners.delete(listener);
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Publishes the SDK to `window.__shipeasy` so the devtools overlay can read
|
|
471
|
+
* current values. Idempotent. Returns the bridge object for tests.
|
|
472
|
+
*/
|
|
473
|
+
installBridge() {
|
|
474
|
+
if (typeof window === "undefined") return null;
|
|
475
|
+
const bridge = {
|
|
476
|
+
getFlag: (n) => this.getFlag(n),
|
|
477
|
+
getExperiment: (n) => {
|
|
478
|
+
const r = this.getExperiment(n, {});
|
|
479
|
+
return { inExperiment: r.inExperiment, group: r.group };
|
|
480
|
+
},
|
|
481
|
+
getConfig: (n) => this.getConfig(n)
|
|
482
|
+
};
|
|
483
|
+
window.__shipeasy = bridge;
|
|
484
|
+
window.dispatchEvent(new CustomEvent("se:state:update"));
|
|
485
|
+
return bridge;
|
|
486
|
+
}
|
|
277
487
|
track(eventName, props) {
|
|
278
488
|
this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
|
|
279
489
|
}
|
|
@@ -283,9 +493,250 @@ var FlagsClientBrowser = class {
|
|
|
283
493
|
destroy() {
|
|
284
494
|
this.buffer.flush();
|
|
285
495
|
this.buffer.destroy();
|
|
496
|
+
this.listeners.clear();
|
|
497
|
+
if (this.overrideListenerInstalled && typeof window !== "undefined") {
|
|
498
|
+
window.removeEventListener("se:override:change", this.onOverrideChange);
|
|
499
|
+
this.overrideListenerInstalled = false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
var TRUE_RX = /^(true|on|1|yes)$/i;
|
|
504
|
+
var FALSE_RX = /^(false|off|0|no)$/i;
|
|
505
|
+
function parseBool(raw) {
|
|
506
|
+
if (TRUE_RX.test(raw)) return true;
|
|
507
|
+
if (FALSE_RX.test(raw)) return false;
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
function decodeConfigValue(raw) {
|
|
511
|
+
if (raw.startsWith("b64:")) {
|
|
512
|
+
try {
|
|
513
|
+
const json = atob(raw.slice(4).replace(/-/g, "+").replace(/_/g, "/"));
|
|
514
|
+
return JSON.parse(json);
|
|
515
|
+
} catch {
|
|
516
|
+
return raw;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
return JSON.parse(raw);
|
|
521
|
+
} catch {
|
|
522
|
+
return raw;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function readParam(canonical, legacy) {
|
|
526
|
+
if (typeof window === "undefined" || !window.location) return null;
|
|
527
|
+
const params = new URLSearchParams(window.location.search);
|
|
528
|
+
const direct = params.get(canonical);
|
|
529
|
+
if (direct !== null) return direct;
|
|
530
|
+
if (legacy) {
|
|
531
|
+
const legacyVal = params.get(legacy);
|
|
532
|
+
if (legacyVal !== null) return legacyVal;
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
function readGateOverride(name) {
|
|
537
|
+
const v = readParam(`se_ks_${name}`) ?? readParam(`se_gate_${name}`) ?? readParam(`se-gate-${name}`);
|
|
538
|
+
return v === null ? null : parseBool(v);
|
|
539
|
+
}
|
|
540
|
+
function readConfigOverride(name) {
|
|
541
|
+
const v = readParam(`se_config_${name}`, `se-config-${name}`);
|
|
542
|
+
if (v === null) return void 0;
|
|
543
|
+
return decodeConfigValue(v);
|
|
544
|
+
}
|
|
545
|
+
function readExpOverride(name) {
|
|
546
|
+
const v = readParam(`se_exp_${name}`, `se-exp-${name}`);
|
|
547
|
+
if (v === null || v === "" || v === "default" || v === "none") return null;
|
|
548
|
+
return v;
|
|
549
|
+
}
|
|
550
|
+
function isDevtoolsRequested() {
|
|
551
|
+
if (typeof window === "undefined" || !window.location) return false;
|
|
552
|
+
const p = new URLSearchParams(window.location.search);
|
|
553
|
+
return p.has("se") || p.has("se_devtools") || p.has("se-devtools");
|
|
554
|
+
}
|
|
555
|
+
function loadDevtools(opts = {}) {
|
|
556
|
+
if (typeof window === "undefined") return;
|
|
557
|
+
const wGlobal = window;
|
|
558
|
+
const mod = wGlobal.__shipeasy_devtools_global;
|
|
559
|
+
if (!mod) return;
|
|
560
|
+
mod.init(opts);
|
|
561
|
+
const w = window;
|
|
562
|
+
if (!w.__shipeasy_devtools) {
|
|
563
|
+
let visible = true;
|
|
564
|
+
w.__shipeasy_devtools = {
|
|
565
|
+
toggle() {
|
|
566
|
+
if (visible) {
|
|
567
|
+
mod.destroy();
|
|
568
|
+
visible = false;
|
|
569
|
+
} else {
|
|
570
|
+
mod.init(opts);
|
|
571
|
+
visible = true;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
function attachDevtools(client, opts = {}) {
|
|
578
|
+
if (typeof window === "undefined") return () => {
|
|
579
|
+
};
|
|
580
|
+
const hotkey = opts.hotkey ?? "Shift+Alt+S";
|
|
581
|
+
const parts = hotkey.split("+");
|
|
582
|
+
const key = parts[parts.length - 1];
|
|
583
|
+
const shift = parts.includes("Shift");
|
|
584
|
+
const alt = parts.includes("Alt");
|
|
585
|
+
const ctrl = parts.includes("Ctrl") || parts.includes("Control");
|
|
586
|
+
const meta = parts.includes("Meta") || parts.includes("Cmd");
|
|
587
|
+
client.installBridge();
|
|
588
|
+
if (isDevtoolsRequested()) loadDevtools({ adminUrl: opts.adminUrl, edgeUrl: opts.edgeUrl });
|
|
589
|
+
let loaded = isDevtoolsRequested();
|
|
590
|
+
function onKeyDown(e) {
|
|
591
|
+
if (e.key === key && e.shiftKey === shift && e.altKey === alt && e.ctrlKey === ctrl && e.metaKey === meta) {
|
|
592
|
+
if (!loaded) {
|
|
593
|
+
loaded = true;
|
|
594
|
+
loadDevtools({ adminUrl: opts.adminUrl, edgeUrl: opts.edgeUrl });
|
|
595
|
+
} else {
|
|
596
|
+
window.__shipeasy_devtools?.toggle();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
window.addEventListener("keydown", onKeyDown);
|
|
601
|
+
const unsubBridge = client.subscribe(() => client.installBridge());
|
|
602
|
+
return () => {
|
|
603
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
604
|
+
unsubBridge();
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
var _client = null;
|
|
608
|
+
function configureShipeasy(opts) {
|
|
609
|
+
if (_client) return _client;
|
|
610
|
+
_client = new FlagsClientBrowser(opts);
|
|
611
|
+
return _client;
|
|
612
|
+
}
|
|
613
|
+
function getShipeasyClient() {
|
|
614
|
+
return _client;
|
|
615
|
+
}
|
|
616
|
+
function _resetShipeasyForTests() {
|
|
617
|
+
_client?.destroy();
|
|
618
|
+
_client = null;
|
|
619
|
+
}
|
|
620
|
+
var flags = {
|
|
621
|
+
configure(opts) {
|
|
622
|
+
configureShipeasy(opts);
|
|
623
|
+
},
|
|
624
|
+
identify(user) {
|
|
625
|
+
if (!_client) {
|
|
626
|
+
console.warn("[shipeasy] flags.identify called before configureShipeasy()");
|
|
627
|
+
return Promise.resolve();
|
|
628
|
+
}
|
|
629
|
+
return _client.identify(user);
|
|
630
|
+
},
|
|
631
|
+
/** Read a feature gate. Returns false until identify() resolves. */
|
|
632
|
+
get(name) {
|
|
633
|
+
return _client?.getFlag(name) ?? false;
|
|
634
|
+
},
|
|
635
|
+
getConfig(name, decode) {
|
|
636
|
+
return _client?.getConfig(name, decode);
|
|
637
|
+
},
|
|
638
|
+
getExperiment(name, defaultParams, decode, variants) {
|
|
639
|
+
return _client?.getExperiment(name, defaultParams, decode, variants) ?? {
|
|
640
|
+
inExperiment: false,
|
|
641
|
+
group: "control",
|
|
642
|
+
params: defaultParams
|
|
643
|
+
};
|
|
644
|
+
},
|
|
645
|
+
track(eventName, props) {
|
|
646
|
+
_client?.track(eventName, props);
|
|
647
|
+
},
|
|
648
|
+
flush() {
|
|
649
|
+
return _client?.flush() ?? Promise.resolve();
|
|
650
|
+
},
|
|
651
|
+
/** Subscribe for change notifications (identify/override). Used by framework adapters. */
|
|
652
|
+
subscribe(listener) {
|
|
653
|
+
if (!_client) return () => {
|
|
654
|
+
};
|
|
655
|
+
return _client.subscribe(listener);
|
|
656
|
+
},
|
|
657
|
+
/** True once identify() has completed and flags are available. */
|
|
658
|
+
get ready() {
|
|
659
|
+
return _client?.ready ?? false;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
var _createElement = null;
|
|
663
|
+
var i18n = {
|
|
664
|
+
t(key, variables) {
|
|
665
|
+
if (typeof window !== "undefined" && window.i18n) return window.i18n.t(key, variables);
|
|
666
|
+
return key;
|
|
667
|
+
},
|
|
668
|
+
/**
|
|
669
|
+
* Translate a key and return a framework element (e.g. React <span>)
|
|
670
|
+
* carrying `data-label` / `data-variables` attributes so the ShipEasy
|
|
671
|
+
* devtools "Edit labels" overlay can highlight and edit it in place.
|
|
672
|
+
*
|
|
673
|
+
* Requires a one-time setup call: `i18n.configure({ createElement })`.
|
|
674
|
+
* The returned value is whatever `createElement` returns — pass React's
|
|
675
|
+
* `createElement`, Vue's `h`, Solid's `createSignal`-based factory, etc.
|
|
676
|
+
*
|
|
677
|
+
* Falls back to a plain translated string if `createElement` was not
|
|
678
|
+
* configured (e.g. server-side or in non-JSX contexts).
|
|
679
|
+
*/
|
|
680
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
681
|
+
tEl(key, variables, desc) {
|
|
682
|
+
const text = this.t(key, variables);
|
|
683
|
+
if (!_createElement) return text;
|
|
684
|
+
const props = { "data-label": key };
|
|
685
|
+
if (variables) props["data-variables"] = JSON.stringify(variables);
|
|
686
|
+
if (desc) props["data-label-desc"] = desc;
|
|
687
|
+
return _createElement("span", props, text);
|
|
688
|
+
},
|
|
689
|
+
/** Wire up the element creator once at app startup (call before any tEl use). */
|
|
690
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
691
|
+
configure(opts) {
|
|
692
|
+
_createElement = opts.createElement;
|
|
693
|
+
},
|
|
694
|
+
get locale() {
|
|
695
|
+
if (typeof window !== "undefined" && window.i18n) return window.i18n.locale;
|
|
696
|
+
return null;
|
|
697
|
+
},
|
|
698
|
+
get ready() {
|
|
699
|
+
if (typeof window !== "undefined" && window.i18n) return Boolean(window.i18n.locale);
|
|
700
|
+
return false;
|
|
701
|
+
},
|
|
702
|
+
/** Resolves when the loader has installed window.i18n and fetched a profile. */
|
|
703
|
+
whenReady() {
|
|
704
|
+
if (typeof window === "undefined") return Promise.resolve();
|
|
705
|
+
if (window.i18n?.locale) return Promise.resolve();
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
const handler = () => resolve();
|
|
708
|
+
window.addEventListener("se:i18n:ready", handler, { once: true });
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
/** Subscribe to locale/profile updates. Returns an unsubscribe fn. */
|
|
712
|
+
onUpdate(cb) {
|
|
713
|
+
if (typeof window === "undefined") return () => {
|
|
714
|
+
};
|
|
715
|
+
if (window.i18n) return window.i18n.on("update", cb);
|
|
716
|
+
let unsub = () => {
|
|
717
|
+
};
|
|
718
|
+
const handler = () => {
|
|
719
|
+
if (window.i18n) unsub = window.i18n.on("update", cb);
|
|
720
|
+
};
|
|
721
|
+
window.addEventListener("se:i18n:ready", handler, { once: true });
|
|
722
|
+
return () => {
|
|
723
|
+
window.removeEventListener("se:i18n:ready", handler);
|
|
724
|
+
unsub();
|
|
725
|
+
};
|
|
286
726
|
}
|
|
287
727
|
};
|
|
288
728
|
export {
|
|
289
729
|
FlagsClientBrowser,
|
|
730
|
+
_resetShipeasyForTests,
|
|
731
|
+
attachDevtools,
|
|
732
|
+
configureShipeasy,
|
|
733
|
+
flags,
|
|
734
|
+
getShipeasyClient,
|
|
735
|
+
i18n,
|
|
736
|
+
isDevtoolsRequested,
|
|
737
|
+
loadDevtools,
|
|
738
|
+
readConfigOverride,
|
|
739
|
+
readExpOverride,
|
|
740
|
+
readGateOverride,
|
|
290
741
|
version
|
|
291
742
|
};
|
package/dist/server/index.d.mts
CHANGED
|
@@ -9,13 +9,17 @@ interface ExperimentResult<P> {
|
|
|
9
9
|
group: string;
|
|
10
10
|
params: P;
|
|
11
11
|
}
|
|
12
|
+
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
12
13
|
interface FlagsClientOptions {
|
|
13
14
|
apiKey: string;
|
|
14
15
|
baseUrl?: string;
|
|
16
|
+
/** Which published env to read values from. Defaults to "prod". */
|
|
17
|
+
env?: FlagsClientEnv;
|
|
15
18
|
}
|
|
16
19
|
declare class FlagsClient {
|
|
17
20
|
private readonly apiKey;
|
|
18
21
|
private readonly baseUrl;
|
|
22
|
+
private readonly env;
|
|
19
23
|
private flagsBlob;
|
|
20
24
|
private expsBlob;
|
|
21
25
|
private flagsEtag;
|
|
@@ -36,5 +40,24 @@ declare class FlagsClient {
|
|
|
36
40
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
37
41
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
38
42
|
}
|
|
43
|
+
declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
|
|
44
|
+
declare function getShipeasyServerClient(): FlagsClient | null;
|
|
45
|
+
declare function _resetShipeasyServerForTests(): void;
|
|
46
|
+
declare const flags: {
|
|
47
|
+
configure(opts: FlagsClientOptions): void;
|
|
48
|
+
/**
|
|
49
|
+
* Long-running server: starts the background poll. Call once at app boot.
|
|
50
|
+
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
51
|
+
*/
|
|
52
|
+
init(): Promise<void>;
|
|
53
|
+
/** Serverless / edge: fetch rules once, no background timer. */
|
|
54
|
+
initOnce(): Promise<void>;
|
|
55
|
+
/** Stop background timers. Safe to call repeatedly. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
get(name: string, user: User): boolean;
|
|
58
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
59
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
60
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
61
|
+
};
|
|
39
62
|
|
|
40
|
-
export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
|
|
63
|
+
export { type ExperimentResult, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type User, _resetShipeasyServerForTests, configureShipeasyServer, flags, getShipeasyServerClient, version };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -9,13 +9,17 @@ interface ExperimentResult<P> {
|
|
|
9
9
|
group: string;
|
|
10
10
|
params: P;
|
|
11
11
|
}
|
|
12
|
+
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
12
13
|
interface FlagsClientOptions {
|
|
13
14
|
apiKey: string;
|
|
14
15
|
baseUrl?: string;
|
|
16
|
+
/** Which published env to read values from. Defaults to "prod". */
|
|
17
|
+
env?: FlagsClientEnv;
|
|
15
18
|
}
|
|
16
19
|
declare class FlagsClient {
|
|
17
20
|
private readonly apiKey;
|
|
18
21
|
private readonly baseUrl;
|
|
22
|
+
private readonly env;
|
|
19
23
|
private flagsBlob;
|
|
20
24
|
private expsBlob;
|
|
21
25
|
private flagsEtag;
|
|
@@ -36,5 +40,24 @@ declare class FlagsClient {
|
|
|
36
40
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
37
41
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
38
42
|
}
|
|
43
|
+
declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
|
|
44
|
+
declare function getShipeasyServerClient(): FlagsClient | null;
|
|
45
|
+
declare function _resetShipeasyServerForTests(): void;
|
|
46
|
+
declare const flags: {
|
|
47
|
+
configure(opts: FlagsClientOptions): void;
|
|
48
|
+
/**
|
|
49
|
+
* Long-running server: starts the background poll. Call once at app boot.
|
|
50
|
+
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
51
|
+
*/
|
|
52
|
+
init(): Promise<void>;
|
|
53
|
+
/** Serverless / edge: fetch rules once, no background timer. */
|
|
54
|
+
initOnce(): Promise<void>;
|
|
55
|
+
/** Stop background timers. Safe to call repeatedly. */
|
|
56
|
+
destroy(): void;
|
|
57
|
+
get(name: string, user: User): boolean;
|
|
58
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
59
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
60
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
61
|
+
};
|
|
39
62
|
|
|
40
|
-
export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
|
|
63
|
+
export { type ExperimentResult, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type User, _resetShipeasyServerForTests, configureShipeasyServer, flags, getShipeasyServerClient, version };
|