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