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