@shipeasy/sdk 3.0.0 → 3.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/dist/client/index.d.mts +16 -0
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.js +75 -1
- package/dist/client/index.mjs +73 -1
- package/dist/server/index.d.mts +18 -0
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +85 -3
- package/dist/server/index.mjs +85 -3
- package/package.json +1 -1
package/dist/client/index.d.mts
CHANGED
|
@@ -53,6 +53,15 @@ interface FlagsClientBrowserOptions {
|
|
|
53
53
|
autoGuardrailGroups?: Partial<AutoCollectGroups>;
|
|
54
54
|
/** Which published env to read values from. Defaults to "prod". */
|
|
55
55
|
env?: FlagsClientBrowserEnv;
|
|
56
|
+
/**
|
|
57
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
58
|
+
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
59
|
+
* usage is counted by Cloudflare's native per-path analytics. Pass `true` to
|
|
60
|
+
* disable entirely.
|
|
61
|
+
*/
|
|
62
|
+
disableTelemetry?: boolean;
|
|
63
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
64
|
+
telemetryUrl?: string;
|
|
56
65
|
}
|
|
57
66
|
declare class FlagsClientBrowser {
|
|
58
67
|
private readonly sdkKey;
|
|
@@ -64,6 +73,7 @@ declare class FlagsClientBrowser {
|
|
|
64
73
|
private anonId;
|
|
65
74
|
private userId;
|
|
66
75
|
private buffer;
|
|
76
|
+
private telemetry;
|
|
67
77
|
private guardrailsInstalled;
|
|
68
78
|
private listeners;
|
|
69
79
|
private overrideListenerInstalled;
|
|
@@ -175,6 +185,12 @@ interface ShipeasyClientConfig {
|
|
|
175
185
|
* ```
|
|
176
186
|
*/
|
|
177
187
|
autoCollect?: boolean | Partial<AutoCollectGroups>;
|
|
188
|
+
/**
|
|
189
|
+
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
190
|
+
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
191
|
+
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
192
|
+
*/
|
|
193
|
+
disableTelemetry?: boolean;
|
|
178
194
|
}
|
|
179
195
|
/**
|
|
180
196
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
package/dist/client/index.d.ts
CHANGED
|
@@ -53,6 +53,15 @@ interface FlagsClientBrowserOptions {
|
|
|
53
53
|
autoGuardrailGroups?: Partial<AutoCollectGroups>;
|
|
54
54
|
/** Which published env to read values from. Defaults to "prod". */
|
|
55
55
|
env?: FlagsClientBrowserEnv;
|
|
56
|
+
/**
|
|
57
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
58
|
+
* getExperiment/getKillswitch call fires one fire-and-forget sendBeacon so
|
|
59
|
+
* usage is counted by Cloudflare's native per-path analytics. Pass `true` to
|
|
60
|
+
* disable entirely.
|
|
61
|
+
*/
|
|
62
|
+
disableTelemetry?: boolean;
|
|
63
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
64
|
+
telemetryUrl?: string;
|
|
56
65
|
}
|
|
57
66
|
declare class FlagsClientBrowser {
|
|
58
67
|
private readonly sdkKey;
|
|
@@ -64,6 +73,7 @@ declare class FlagsClientBrowser {
|
|
|
64
73
|
private anonId;
|
|
65
74
|
private userId;
|
|
66
75
|
private buffer;
|
|
76
|
+
private telemetry;
|
|
67
77
|
private guardrailsInstalled;
|
|
68
78
|
private listeners;
|
|
69
79
|
private overrideListenerInstalled;
|
|
@@ -175,6 +185,12 @@ interface ShipeasyClientConfig {
|
|
|
175
185
|
* ```
|
|
176
186
|
*/
|
|
177
187
|
autoCollect?: boolean | Partial<AutoCollectGroups>;
|
|
188
|
+
/**
|
|
189
|
+
* Disable per-evaluation usage telemetry. Telemetry is ON by default — every
|
|
190
|
+
* flag/config/experiment/killswitch read fires one fire-and-forget beacon
|
|
191
|
+
* counted by Cloudflare's native per-path analytics. Pass `true` to opt out.
|
|
192
|
+
*/
|
|
193
|
+
disableTelemetry?: boolean;
|
|
178
194
|
}
|
|
179
195
|
/**
|
|
180
196
|
* Initialise the ShipEasy client SDK and wire up lazy devtools.
|
package/dist/client/index.js
CHANGED
|
@@ -42,6 +42,67 @@ __export(client_exports, {
|
|
|
42
42
|
version: () => version
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(client_exports);
|
|
45
|
+
|
|
46
|
+
// src/telemetry.ts
|
|
47
|
+
async function sha256Hex(input) {
|
|
48
|
+
const buf = new TextEncoder().encode(input);
|
|
49
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
50
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
51
|
+
}
|
|
52
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
53
|
+
var Telemetry = class {
|
|
54
|
+
prefix;
|
|
55
|
+
disabled;
|
|
56
|
+
dedupeMs;
|
|
57
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
58
|
+
// the number of distinct keys the app reads.
|
|
59
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
60
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
61
|
+
// cost is a Map-free microtask, not a hash.
|
|
62
|
+
keyHash;
|
|
63
|
+
constructor(opts) {
|
|
64
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
65
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
66
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
67
|
+
this.prefix = `${endpoint}/t`;
|
|
68
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
72
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
73
|
+
* must never affect the evaluation it measures.
|
|
74
|
+
*/
|
|
75
|
+
emit(feature, resource) {
|
|
76
|
+
if (this.disabled || !this.keyHash) return;
|
|
77
|
+
if (this.dedupeMs > 0) {
|
|
78
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
81
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
82
|
+
this.lastEmit.set(dedupeKey, now);
|
|
83
|
+
}
|
|
84
|
+
void this.keyHash.then((suffix) => {
|
|
85
|
+
if (!suffix) return;
|
|
86
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
function send(url) {
|
|
91
|
+
try {
|
|
92
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
93
|
+
navigator.sendBeacon(url);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const f = globalThis.fetch;
|
|
97
|
+
if (typeof f === "function") {
|
|
98
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/client/index.ts
|
|
45
106
|
var version = "1.0.0";
|
|
46
107
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
47
108
|
var MAX_BUFFER = 100;
|
|
@@ -427,6 +488,7 @@ var FlagsClientBrowser = class {
|
|
|
427
488
|
anonId;
|
|
428
489
|
userId = "";
|
|
429
490
|
buffer;
|
|
491
|
+
telemetry;
|
|
430
492
|
guardrailsInstalled = false;
|
|
431
493
|
listeners = /* @__PURE__ */ new Set();
|
|
432
494
|
overrideListenerInstalled = false;
|
|
@@ -450,6 +512,13 @@ var FlagsClientBrowser = class {
|
|
|
450
512
|
};
|
|
451
513
|
this.anonId = getOrCreateAnonId();
|
|
452
514
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
515
|
+
this.telemetry = new Telemetry({
|
|
516
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
517
|
+
sdkKey: this.sdkKey,
|
|
518
|
+
side: "client",
|
|
519
|
+
env: this.env,
|
|
520
|
+
disabled: opts.disableTelemetry
|
|
521
|
+
});
|
|
453
522
|
void this.buffer.flushPendingAlias();
|
|
454
523
|
}
|
|
455
524
|
async identify(user) {
|
|
@@ -499,12 +568,14 @@ var FlagsClientBrowser = class {
|
|
|
499
568
|
this.evalResult = data;
|
|
500
569
|
}
|
|
501
570
|
getFlag(name) {
|
|
571
|
+
this.telemetry.emit("gate", name);
|
|
502
572
|
if (this.evalResult === null) return false;
|
|
503
573
|
const ov = readGateOverride(name);
|
|
504
574
|
if (ov !== null) return ov;
|
|
505
575
|
return this.evalResult.flags[name] ?? false;
|
|
506
576
|
}
|
|
507
577
|
getConfig(name, decode) {
|
|
578
|
+
this.telemetry.emit("config", name);
|
|
508
579
|
if (this.evalResult === null) return void 0;
|
|
509
580
|
const ov = readConfigOverride(name);
|
|
510
581
|
const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
|
|
@@ -518,6 +589,7 @@ var FlagsClientBrowser = class {
|
|
|
518
589
|
}
|
|
519
590
|
}
|
|
520
591
|
getExperiment(name, defaultParams, decode, variants) {
|
|
592
|
+
this.telemetry.emit("experiment", name);
|
|
521
593
|
const notIn = {
|
|
522
594
|
inExperiment: false,
|
|
523
595
|
group: "control",
|
|
@@ -582,6 +654,7 @@ var FlagsClientBrowser = class {
|
|
|
582
654
|
* the per-switch state. Returns false for unknown killswitches / switches.
|
|
583
655
|
*/
|
|
584
656
|
getKillswitch(name, switchKey) {
|
|
657
|
+
this.telemetry.emit("ks", name);
|
|
585
658
|
if (this.evalResult === null) return false;
|
|
586
659
|
const ks = this.evalResult.killswitches?.[name];
|
|
587
660
|
if (ks === void 0) return false;
|
|
@@ -716,7 +789,8 @@ function shipeasy(opts) {
|
|
|
716
789
|
sdkKey: opts.clientKey,
|
|
717
790
|
baseUrl,
|
|
718
791
|
autoGuardrails: blanket,
|
|
719
|
-
autoGuardrailGroups: groups
|
|
792
|
+
autoGuardrailGroups: groups,
|
|
793
|
+
disableTelemetry: opts.disableTelemetry
|
|
720
794
|
});
|
|
721
795
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
722
796
|
flags.notifyMounted();
|
package/dist/client/index.mjs
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
+
// src/telemetry.ts
|
|
2
|
+
async function sha256Hex(input) {
|
|
3
|
+
const buf = new TextEncoder().encode(input);
|
|
4
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
5
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
8
|
+
var Telemetry = class {
|
|
9
|
+
prefix;
|
|
10
|
+
disabled;
|
|
11
|
+
dedupeMs;
|
|
12
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
13
|
+
// the number of distinct keys the app reads.
|
|
14
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
15
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
16
|
+
// cost is a Map-free microtask, not a hash.
|
|
17
|
+
keyHash;
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
20
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
21
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
22
|
+
this.prefix = `${endpoint}/t`;
|
|
23
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
27
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
28
|
+
* must never affect the evaluation it measures.
|
|
29
|
+
*/
|
|
30
|
+
emit(feature, resource) {
|
|
31
|
+
if (this.disabled || !this.keyHash) return;
|
|
32
|
+
if (this.dedupeMs > 0) {
|
|
33
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
36
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
37
|
+
this.lastEmit.set(dedupeKey, now);
|
|
38
|
+
}
|
|
39
|
+
void this.keyHash.then((suffix) => {
|
|
40
|
+
if (!suffix) return;
|
|
41
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function send(url) {
|
|
46
|
+
try {
|
|
47
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
48
|
+
navigator.sendBeacon(url);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const f = globalThis.fetch;
|
|
52
|
+
if (typeof f === "function") {
|
|
53
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
1
60
|
// src/client/index.ts
|
|
2
61
|
var version = "1.0.0";
|
|
3
62
|
var FLUSH_INTERVAL_MS = 5e3;
|
|
@@ -384,6 +443,7 @@ var FlagsClientBrowser = class {
|
|
|
384
443
|
anonId;
|
|
385
444
|
userId = "";
|
|
386
445
|
buffer;
|
|
446
|
+
telemetry;
|
|
387
447
|
guardrailsInstalled = false;
|
|
388
448
|
listeners = /* @__PURE__ */ new Set();
|
|
389
449
|
overrideListenerInstalled = false;
|
|
@@ -407,6 +467,13 @@ var FlagsClientBrowser = class {
|
|
|
407
467
|
};
|
|
408
468
|
this.anonId = getOrCreateAnonId();
|
|
409
469
|
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
470
|
+
this.telemetry = new Telemetry({
|
|
471
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
472
|
+
sdkKey: this.sdkKey,
|
|
473
|
+
side: "client",
|
|
474
|
+
env: this.env,
|
|
475
|
+
disabled: opts.disableTelemetry
|
|
476
|
+
});
|
|
410
477
|
void this.buffer.flushPendingAlias();
|
|
411
478
|
}
|
|
412
479
|
async identify(user) {
|
|
@@ -456,12 +523,14 @@ var FlagsClientBrowser = class {
|
|
|
456
523
|
this.evalResult = data;
|
|
457
524
|
}
|
|
458
525
|
getFlag(name) {
|
|
526
|
+
this.telemetry.emit("gate", name);
|
|
459
527
|
if (this.evalResult === null) return false;
|
|
460
528
|
const ov = readGateOverride(name);
|
|
461
529
|
if (ov !== null) return ov;
|
|
462
530
|
return this.evalResult.flags[name] ?? false;
|
|
463
531
|
}
|
|
464
532
|
getConfig(name, decode) {
|
|
533
|
+
this.telemetry.emit("config", name);
|
|
465
534
|
if (this.evalResult === null) return void 0;
|
|
466
535
|
const ov = readConfigOverride(name);
|
|
467
536
|
const raw = ov !== void 0 ? ov : this.evalResult.configs?.[name];
|
|
@@ -475,6 +544,7 @@ var FlagsClientBrowser = class {
|
|
|
475
544
|
}
|
|
476
545
|
}
|
|
477
546
|
getExperiment(name, defaultParams, decode, variants) {
|
|
547
|
+
this.telemetry.emit("experiment", name);
|
|
478
548
|
const notIn = {
|
|
479
549
|
inExperiment: false,
|
|
480
550
|
group: "control",
|
|
@@ -539,6 +609,7 @@ var FlagsClientBrowser = class {
|
|
|
539
609
|
* the per-switch state. Returns false for unknown killswitches / switches.
|
|
540
610
|
*/
|
|
541
611
|
getKillswitch(name, switchKey) {
|
|
612
|
+
this.telemetry.emit("ks", name);
|
|
542
613
|
if (this.evalResult === null) return false;
|
|
543
614
|
const ks = this.evalResult.killswitches?.[name];
|
|
544
615
|
if (ks === void 0) return false;
|
|
@@ -673,7 +744,8 @@ function shipeasy(opts) {
|
|
|
673
744
|
sdkKey: opts.clientKey,
|
|
674
745
|
baseUrl,
|
|
675
746
|
autoGuardrails: blanket,
|
|
676
|
-
autoGuardrailGroups: groups
|
|
747
|
+
autoGuardrailGroups: groups,
|
|
748
|
+
disableTelemetry: opts.disableTelemetry
|
|
677
749
|
});
|
|
678
750
|
injectI18nLoader(opts.clientKey, baseUrl, opts.i18nProfile);
|
|
679
751
|
flags.notifyMounted();
|
package/dist/server/index.d.mts
CHANGED
|
@@ -51,11 +51,23 @@ interface FlagsClientOptions {
|
|
|
51
51
|
* for tests; production callers should rely on init()/initOnce().
|
|
52
52
|
*/
|
|
53
53
|
initialBlob?: FlagsBlob;
|
|
54
|
+
/**
|
|
55
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
56
|
+
* getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
|
|
57
|
+
* fire-and-forget beacon counted by Cloudflare's native per-path analytics.
|
|
58
|
+
* Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
|
|
59
|
+
* outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
|
|
60
|
+
* this on hot request paths that evaluate many flags per request.
|
|
61
|
+
*/
|
|
62
|
+
disableTelemetry?: boolean;
|
|
63
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
64
|
+
telemetryUrl?: string;
|
|
54
65
|
}
|
|
55
66
|
declare class FlagsClient {
|
|
56
67
|
private readonly apiKey;
|
|
57
68
|
private readonly baseUrl;
|
|
58
69
|
private readonly env;
|
|
70
|
+
private readonly telemetry;
|
|
59
71
|
private flagsBlob;
|
|
60
72
|
private expsBlob;
|
|
61
73
|
private flagsEtag;
|
|
@@ -147,6 +159,12 @@ interface ShipeasyServerConfig {
|
|
|
147
159
|
user?: User;
|
|
148
160
|
/** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
|
|
149
161
|
i18nDefaultProfile?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Disable per-evaluation usage telemetry. ON by default. On Cloudflare
|
|
164
|
+
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
165
|
+
* that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
|
|
166
|
+
*/
|
|
167
|
+
disableTelemetry?: boolean;
|
|
150
168
|
}
|
|
151
169
|
interface ShipeasyServerHandle {
|
|
152
170
|
flags: Record<string, boolean>;
|
package/dist/server/index.d.ts
CHANGED
|
@@ -51,11 +51,23 @@ interface FlagsClientOptions {
|
|
|
51
51
|
* for tests; production callers should rely on init()/initOnce().
|
|
52
52
|
*/
|
|
53
53
|
initialBlob?: FlagsBlob;
|
|
54
|
+
/**
|
|
55
|
+
* Per-evaluation usage telemetry. ON by default — each getFlag/getConfig/
|
|
56
|
+
* getExperiment/getKillswitch (and the per-key evaluate() loop) fires one
|
|
57
|
+
* fire-and-forget beacon counted by Cloudflare's native per-path analytics.
|
|
58
|
+
* Pass `true` to disable. NOTE: on Cloudflare Workers each beacon is an
|
|
59
|
+
* outbound subrequest (cap 50 free / 1000 paid per invocation), so disable
|
|
60
|
+
* this on hot request paths that evaluate many flags per request.
|
|
61
|
+
*/
|
|
62
|
+
disableTelemetry?: boolean;
|
|
63
|
+
/** Override the telemetry beacon host. Defaults to {@link DEFAULT_TELEMETRY_URL}. */
|
|
64
|
+
telemetryUrl?: string;
|
|
54
65
|
}
|
|
55
66
|
declare class FlagsClient {
|
|
56
67
|
private readonly apiKey;
|
|
57
68
|
private readonly baseUrl;
|
|
58
69
|
private readonly env;
|
|
70
|
+
private readonly telemetry;
|
|
59
71
|
private flagsBlob;
|
|
60
72
|
private expsBlob;
|
|
61
73
|
private flagsEtag;
|
|
@@ -147,6 +159,12 @@ interface ShipeasyServerConfig {
|
|
|
147
159
|
user?: User;
|
|
148
160
|
/** i18n profile to load for SSR translations, e.g. "en:prod". Defaults to "en:prod". */
|
|
149
161
|
i18nDefaultProfile?: string;
|
|
162
|
+
/**
|
|
163
|
+
* Disable per-evaluation usage telemetry. ON by default. On Cloudflare
|
|
164
|
+
* Workers each beacon is an outbound subrequest, so disable on hot SSR paths
|
|
165
|
+
* that evaluate many flags per request. See {@link FlagsClientOptions.disableTelemetry}.
|
|
166
|
+
*/
|
|
167
|
+
disableTelemetry?: boolean;
|
|
150
168
|
}
|
|
151
169
|
interface ShipeasyServerHandle {
|
|
152
170
|
flags: Record<string, boolean>;
|
package/dist/server/index.js
CHANGED
|
@@ -43,6 +43,67 @@ __export(server_exports, {
|
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(server_exports);
|
|
45
45
|
var import_node_async_hooks = require("async_hooks");
|
|
46
|
+
|
|
47
|
+
// src/telemetry.ts
|
|
48
|
+
async function sha256Hex(input) {
|
|
49
|
+
const buf = new TextEncoder().encode(input);
|
|
50
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
51
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
52
|
+
}
|
|
53
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
54
|
+
var Telemetry = class {
|
|
55
|
+
prefix;
|
|
56
|
+
disabled;
|
|
57
|
+
dedupeMs;
|
|
58
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
59
|
+
// the number of distinct keys the app reads.
|
|
60
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
61
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
62
|
+
// cost is a Map-free microtask, not a hash.
|
|
63
|
+
keyHash;
|
|
64
|
+
constructor(opts) {
|
|
65
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
66
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
67
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
68
|
+
this.prefix = `${endpoint}/t`;
|
|
69
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
73
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
74
|
+
* must never affect the evaluation it measures.
|
|
75
|
+
*/
|
|
76
|
+
emit(feature, resource) {
|
|
77
|
+
if (this.disabled || !this.keyHash) return;
|
|
78
|
+
if (this.dedupeMs > 0) {
|
|
79
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
82
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
83
|
+
this.lastEmit.set(dedupeKey, now);
|
|
84
|
+
}
|
|
85
|
+
void this.keyHash.then((suffix) => {
|
|
86
|
+
if (!suffix) return;
|
|
87
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
function send(url) {
|
|
92
|
+
try {
|
|
93
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
94
|
+
navigator.sendBeacon(url);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const f = globalThis.fetch;
|
|
98
|
+
if (typeof f === "function") {
|
|
99
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/server/index.ts
|
|
46
107
|
var version = "1.0.0";
|
|
47
108
|
var C1 = 3432918353;
|
|
48
109
|
var C2 = 461845907;
|
|
@@ -192,6 +253,7 @@ var FlagsClient = class {
|
|
|
192
253
|
apiKey;
|
|
193
254
|
baseUrl;
|
|
194
255
|
env;
|
|
256
|
+
telemetry;
|
|
195
257
|
flagsBlob = null;
|
|
196
258
|
expsBlob = null;
|
|
197
259
|
flagsEtag = null;
|
|
@@ -203,6 +265,13 @@ var FlagsClient = class {
|
|
|
203
265
|
this.apiKey = opts.apiKey;
|
|
204
266
|
this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
|
|
205
267
|
this.env = opts.env ?? "prod";
|
|
268
|
+
this.telemetry = new Telemetry({
|
|
269
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
270
|
+
sdkKey: this.apiKey,
|
|
271
|
+
side: "server",
|
|
272
|
+
env: this.env,
|
|
273
|
+
disabled: opts.disableTelemetry
|
|
274
|
+
});
|
|
206
275
|
if (opts.initialBlob) {
|
|
207
276
|
this.flagsBlob = opts.initialBlob;
|
|
208
277
|
this.initialized = true;
|
|
@@ -264,17 +333,20 @@ var FlagsClient = class {
|
|
|
264
333
|
this.expsBlob = await res.json();
|
|
265
334
|
}
|
|
266
335
|
getFlag(name, user) {
|
|
336
|
+
this.telemetry.emit("gate", name);
|
|
267
337
|
const gate = this.flagsBlob?.gates[name];
|
|
268
338
|
if (!gate) return false;
|
|
269
339
|
return evalGateInternal(gate, user);
|
|
270
340
|
}
|
|
271
341
|
getConfig(name, decode) {
|
|
342
|
+
this.telemetry.emit("config", name);
|
|
272
343
|
const entry = this.flagsBlob?.configs[name];
|
|
273
344
|
if (!entry) return void 0;
|
|
274
345
|
if (!decode) return entry.value;
|
|
275
346
|
return decode(entry.value);
|
|
276
347
|
}
|
|
277
348
|
getExperiment(name, user, defaultParams, decode) {
|
|
349
|
+
this.telemetry.emit("experiment", name);
|
|
278
350
|
const notIn = {
|
|
279
351
|
inExperiment: false,
|
|
280
352
|
group: "control",
|
|
@@ -349,15 +421,18 @@ var FlagsClient = class {
|
|
|
349
421
|
const experiments = {};
|
|
350
422
|
const killswitches = {};
|
|
351
423
|
for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
|
|
424
|
+
this.telemetry.emit("gate", name);
|
|
352
425
|
flags2[name] = evalGateInternal(gate, user);
|
|
353
426
|
}
|
|
354
427
|
for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
|
|
428
|
+
this.telemetry.emit("config", name);
|
|
355
429
|
configs[name] = entry.value;
|
|
356
430
|
}
|
|
357
431
|
for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
|
|
358
432
|
experiments[name] = this.getExperiment(name, user, {});
|
|
359
433
|
}
|
|
360
434
|
for (const [name, ks] of Object.entries(this.flagsBlob?.killswitches ?? {})) {
|
|
435
|
+
this.telemetry.emit("ks", name);
|
|
361
436
|
if (ks.switches && Object.keys(ks.switches).length > 0) {
|
|
362
437
|
const out = {};
|
|
363
438
|
for (const [k, v] of Object.entries(ks.switches)) out[k] = isEnabled(v);
|
|
@@ -377,6 +452,7 @@ var FlagsClient = class {
|
|
|
377
452
|
return { flags: flags2, configs, experiments, killswitches };
|
|
378
453
|
}
|
|
379
454
|
getKillswitch(name, switchKey) {
|
|
455
|
+
this.telemetry.emit("ks", name);
|
|
380
456
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
381
457
|
if (!ks) return false;
|
|
382
458
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
@@ -387,6 +463,7 @@ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
|
387
463
|
var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
|
|
388
464
|
var _i18nALS = new import_node_async_hooks.AsyncLocalStorage();
|
|
389
465
|
var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
|
|
466
|
+
var I18N_CACHE_TTL_MS = 6e4;
|
|
390
467
|
var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
|
|
391
468
|
globalThis[_I18N_SSR_SYM] = () => {
|
|
392
469
|
const fromALS = _i18nALS.getStore();
|
|
@@ -432,14 +509,19 @@ var i18n = {
|
|
|
432
509
|
const existingALS = _i18nALS.getStore();
|
|
433
510
|
if (existingALS && Object.keys(existingALS.strings).length > 0) return;
|
|
434
511
|
const cached = _i18nCache.get(profile);
|
|
435
|
-
if (cached && Object.keys(cached.strings).length > 0) {
|
|
512
|
+
if (cached && Object.keys(cached.strings).length > 0 && Date.now() - cached.fetchedAt < I18N_CACHE_TTL_MS) {
|
|
436
513
|
_i18nALS.enterWith(cached);
|
|
437
514
|
return;
|
|
438
515
|
}
|
|
439
516
|
const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
|
|
440
517
|
const locale = profile.split(":")[0] || "en";
|
|
441
518
|
const store = { strings: labels?.strings ?? {}, locale };
|
|
442
|
-
if (Object.keys(store.strings).length > 0)
|
|
519
|
+
if (Object.keys(store.strings).length > 0) {
|
|
520
|
+
_i18nCache.set(profile, { ...store, fetchedAt: Date.now() });
|
|
521
|
+
} else if (cached && Object.keys(cached.strings).length > 0) {
|
|
522
|
+
_i18nALS.enterWith(cached);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
443
525
|
_i18nALS.enterWith(store);
|
|
444
526
|
},
|
|
445
527
|
/**
|
|
@@ -506,7 +588,7 @@ async function shipeasy(opts) {
|
|
|
506
588
|
);
|
|
507
589
|
}
|
|
508
590
|
const profile = opts.i18nDefaultProfile ?? "en:prod";
|
|
509
|
-
flags.configure({ apiKey: serverKey });
|
|
591
|
+
flags.configure({ apiKey: serverKey, disableTelemetry: opts.disableTelemetry });
|
|
510
592
|
let resolvedUrlOverrides = opts.urlOverrides;
|
|
511
593
|
if (!resolvedUrlOverrides) {
|
|
512
594
|
try {
|
package/dist/server/index.mjs
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
// src/server/index.ts
|
|
2
2
|
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
+
|
|
4
|
+
// src/telemetry.ts
|
|
5
|
+
async function sha256Hex(input) {
|
|
6
|
+
const buf = new TextEncoder().encode(input);
|
|
7
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
8
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
9
|
+
}
|
|
10
|
+
var DEFAULT_TELEMETRY_URL = "https://t.shipeasy.ai";
|
|
11
|
+
var Telemetry = class {
|
|
12
|
+
prefix;
|
|
13
|
+
disabled;
|
|
14
|
+
dedupeMs;
|
|
15
|
+
// Last-emit timestamp per `feature/resource`, for the dedup window. Bounded by
|
|
16
|
+
// the number of distinct keys the app reads.
|
|
17
|
+
lastEmit = /* @__PURE__ */ new Map();
|
|
18
|
+
// Resolved once at construction and reused by every emit(), so the per-eval
|
|
19
|
+
// cost is a Map-free microtask, not a hash.
|
|
20
|
+
keyHash;
|
|
21
|
+
constructor(opts) {
|
|
22
|
+
const endpoint = (opts.endpoint ?? "").replace(/\/$/, "");
|
|
23
|
+
this.disabled = opts.disabled === true || !opts.sdkKey || !endpoint;
|
|
24
|
+
this.dedupeMs = opts.dedupeMs ?? 2e3;
|
|
25
|
+
this.prefix = `${endpoint}/t`;
|
|
26
|
+
this.keyHash = this.disabled ? null : sha256Hex(opts.sdkKey).then((h) => `${h}/${opts.side}/${encodeURIComponent(opts.env)}`).catch(() => "");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Emit a single best-effort usage beacon for one evaluation. Never blocks the
|
|
30
|
+
* caller (the hash is already resolved) and never throws — a failed beacon
|
|
31
|
+
* must never affect the evaluation it measures.
|
|
32
|
+
*/
|
|
33
|
+
emit(feature, resource) {
|
|
34
|
+
if (this.disabled || !this.keyHash) return;
|
|
35
|
+
if (this.dedupeMs > 0) {
|
|
36
|
+
const dedupeKey = `${feature}/${resource}`;
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
const last = this.lastEmit.get(dedupeKey);
|
|
39
|
+
if (last !== void 0 && now - last < this.dedupeMs) return;
|
|
40
|
+
this.lastEmit.set(dedupeKey, now);
|
|
41
|
+
}
|
|
42
|
+
void this.keyHash.then((suffix) => {
|
|
43
|
+
if (!suffix) return;
|
|
44
|
+
send(`${this.prefix}/${suffix}/${feature}/${encodeURIComponent(resource)}`);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function send(url) {
|
|
49
|
+
try {
|
|
50
|
+
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
51
|
+
navigator.sendBeacon(url);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const f = globalThis.fetch;
|
|
55
|
+
if (typeof f === "function") {
|
|
56
|
+
void f(url, { method: "GET", keepalive: true }).catch(() => {
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/server/index.ts
|
|
3
64
|
var version = "1.0.0";
|
|
4
65
|
var C1 = 3432918353;
|
|
5
66
|
var C2 = 461845907;
|
|
@@ -149,6 +210,7 @@ var FlagsClient = class {
|
|
|
149
210
|
apiKey;
|
|
150
211
|
baseUrl;
|
|
151
212
|
env;
|
|
213
|
+
telemetry;
|
|
152
214
|
flagsBlob = null;
|
|
153
215
|
expsBlob = null;
|
|
154
216
|
flagsEtag = null;
|
|
@@ -160,6 +222,13 @@ var FlagsClient = class {
|
|
|
160
222
|
this.apiKey = opts.apiKey;
|
|
161
223
|
this.baseUrl = (opts.baseUrl ?? "https://cdn.shipeasy.ai").replace(/\/$/, "");
|
|
162
224
|
this.env = opts.env ?? "prod";
|
|
225
|
+
this.telemetry = new Telemetry({
|
|
226
|
+
endpoint: opts.telemetryUrl ?? DEFAULT_TELEMETRY_URL,
|
|
227
|
+
sdkKey: this.apiKey,
|
|
228
|
+
side: "server",
|
|
229
|
+
env: this.env,
|
|
230
|
+
disabled: opts.disableTelemetry
|
|
231
|
+
});
|
|
163
232
|
if (opts.initialBlob) {
|
|
164
233
|
this.flagsBlob = opts.initialBlob;
|
|
165
234
|
this.initialized = true;
|
|
@@ -221,17 +290,20 @@ var FlagsClient = class {
|
|
|
221
290
|
this.expsBlob = await res.json();
|
|
222
291
|
}
|
|
223
292
|
getFlag(name, user) {
|
|
293
|
+
this.telemetry.emit("gate", name);
|
|
224
294
|
const gate = this.flagsBlob?.gates[name];
|
|
225
295
|
if (!gate) return false;
|
|
226
296
|
return evalGateInternal(gate, user);
|
|
227
297
|
}
|
|
228
298
|
getConfig(name, decode) {
|
|
299
|
+
this.telemetry.emit("config", name);
|
|
229
300
|
const entry = this.flagsBlob?.configs[name];
|
|
230
301
|
if (!entry) return void 0;
|
|
231
302
|
if (!decode) return entry.value;
|
|
232
303
|
return decode(entry.value);
|
|
233
304
|
}
|
|
234
305
|
getExperiment(name, user, defaultParams, decode) {
|
|
306
|
+
this.telemetry.emit("experiment", name);
|
|
235
307
|
const notIn = {
|
|
236
308
|
inExperiment: false,
|
|
237
309
|
group: "control",
|
|
@@ -306,15 +378,18 @@ var FlagsClient = class {
|
|
|
306
378
|
const experiments = {};
|
|
307
379
|
const killswitches = {};
|
|
308
380
|
for (const [name, gate] of Object.entries(this.flagsBlob?.gates ?? {})) {
|
|
381
|
+
this.telemetry.emit("gate", name);
|
|
309
382
|
flags2[name] = evalGateInternal(gate, user);
|
|
310
383
|
}
|
|
311
384
|
for (const [name, entry] of Object.entries(this.flagsBlob?.configs ?? {})) {
|
|
385
|
+
this.telemetry.emit("config", name);
|
|
312
386
|
configs[name] = entry.value;
|
|
313
387
|
}
|
|
314
388
|
for (const [name] of Object.entries(this.expsBlob?.experiments ?? {})) {
|
|
315
389
|
experiments[name] = this.getExperiment(name, user, {});
|
|
316
390
|
}
|
|
317
391
|
for (const [name, ks] of Object.entries(this.flagsBlob?.killswitches ?? {})) {
|
|
392
|
+
this.telemetry.emit("ks", name);
|
|
318
393
|
if (ks.switches && Object.keys(ks.switches).length > 0) {
|
|
319
394
|
const out = {};
|
|
320
395
|
for (const [k, v] of Object.entries(ks.switches)) out[k] = isEnabled(v);
|
|
@@ -334,6 +409,7 @@ var FlagsClient = class {
|
|
|
334
409
|
return { flags: flags2, configs, experiments, killswitches };
|
|
335
410
|
}
|
|
336
411
|
getKillswitch(name, switchKey) {
|
|
412
|
+
this.telemetry.emit("ks", name);
|
|
337
413
|
const ks = this.flagsBlob?.killswitches?.[name];
|
|
338
414
|
if (!ks) return false;
|
|
339
415
|
if (switchKey === void 0) return isEnabled(ks.killed);
|
|
@@ -344,6 +420,7 @@ var _I18N_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n");
|
|
|
344
420
|
var _EDIT_MODE_SSR_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-edit-mode");
|
|
345
421
|
var _i18nALS = new AsyncLocalStorage();
|
|
346
422
|
var _I18N_CACHE_SYM = /* @__PURE__ */ Symbol.for("@shipeasy/sdk:ssr-i18n-cache");
|
|
423
|
+
var I18N_CACHE_TTL_MS = 6e4;
|
|
347
424
|
var _i18nCache = globalThis[_I18N_CACHE_SYM] ?? (globalThis[_I18N_CACHE_SYM] = /* @__PURE__ */ new Map());
|
|
348
425
|
globalThis[_I18N_SSR_SYM] = () => {
|
|
349
426
|
const fromALS = _i18nALS.getStore();
|
|
@@ -389,14 +466,19 @@ var i18n = {
|
|
|
389
466
|
const existingALS = _i18nALS.getStore();
|
|
390
467
|
if (existingALS && Object.keys(existingALS.strings).length > 0) return;
|
|
391
468
|
const cached = _i18nCache.get(profile);
|
|
392
|
-
if (cached && Object.keys(cached.strings).length > 0) {
|
|
469
|
+
if (cached && Object.keys(cached.strings).length > 0 && Date.now() - cached.fetchedAt < I18N_CACHE_TTL_MS) {
|
|
393
470
|
_i18nALS.enterWith(cached);
|
|
394
471
|
return;
|
|
395
472
|
}
|
|
396
473
|
const labels = await fetchLabelsForSSR({ key, profile, cdnBaseUrl }).catch(() => null);
|
|
397
474
|
const locale = profile.split(":")[0] || "en";
|
|
398
475
|
const store = { strings: labels?.strings ?? {}, locale };
|
|
399
|
-
if (Object.keys(store.strings).length > 0)
|
|
476
|
+
if (Object.keys(store.strings).length > 0) {
|
|
477
|
+
_i18nCache.set(profile, { ...store, fetchedAt: Date.now() });
|
|
478
|
+
} else if (cached && Object.keys(cached.strings).length > 0) {
|
|
479
|
+
_i18nALS.enterWith(cached);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
400
482
|
_i18nALS.enterWith(store);
|
|
401
483
|
},
|
|
402
484
|
/**
|
|
@@ -463,7 +545,7 @@ async function shipeasy(opts) {
|
|
|
463
545
|
);
|
|
464
546
|
}
|
|
465
547
|
const profile = opts.i18nDefaultProfile ?? "en:prod";
|
|
466
|
-
flags.configure({ apiKey: serverKey });
|
|
548
|
+
flags.configure({ apiKey: serverKey, disableTelemetry: opts.disableTelemetry });
|
|
467
549
|
let resolvedUrlOverrides = opts.urlOverrides;
|
|
468
550
|
if (!resolvedUrlOverrides) {
|
|
469
551
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://shipeasy.ai",
|