@shipeasy/sdk 1.0.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 +48 -0
- package/dist/client/index.d.ts +48 -0
- package/dist/client/index.js +317 -0
- package/dist/client/index.mjs +291 -0
- package/dist/server/index.d.mts +40 -0
- package/dist/server/index.d.ts +40 -0
- package/dist/server/index.js +272 -0
- package/dist/server/index.mjs +246 -0
- package/package.json +54 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
declare const version = "1.0.0";
|
|
2
|
+
interface User {
|
|
3
|
+
user_id?: string;
|
|
4
|
+
[attr: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface ExperimentResult<P> {
|
|
7
|
+
inExperiment: boolean;
|
|
8
|
+
group: string;
|
|
9
|
+
params: P;
|
|
10
|
+
}
|
|
11
|
+
interface EvalFlagResult {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface EvalExpResult {
|
|
15
|
+
in_experiment: boolean;
|
|
16
|
+
group: string;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
interface EvalResponse {
|
|
20
|
+
flags: Record<string, EvalFlagResult>;
|
|
21
|
+
experiments: Record<string, EvalExpResult>;
|
|
22
|
+
}
|
|
23
|
+
interface FlagsClientBrowserOptions {
|
|
24
|
+
sdkKey: string;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
autoGuardrails?: boolean;
|
|
27
|
+
}
|
|
28
|
+
declare class FlagsClientBrowser {
|
|
29
|
+
private readonly sdkKey;
|
|
30
|
+
private readonly baseUrl;
|
|
31
|
+
private readonly autoGuardrails;
|
|
32
|
+
private evalResult;
|
|
33
|
+
private anonId;
|
|
34
|
+
private userId;
|
|
35
|
+
private buffer;
|
|
36
|
+
private guardrailsInstalled;
|
|
37
|
+
constructor(opts: FlagsClientBrowserOptions);
|
|
38
|
+
identify(user: User): Promise<void>;
|
|
39
|
+
initFromBootstrap(data: EvalResponse): void;
|
|
40
|
+
getFlag(name: string): boolean;
|
|
41
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
42
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
43
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
44
|
+
flush(): Promise<void>;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserOptions, type User, version };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
declare const version = "1.0.0";
|
|
2
|
+
interface User {
|
|
3
|
+
user_id?: string;
|
|
4
|
+
[attr: string]: unknown;
|
|
5
|
+
}
|
|
6
|
+
interface ExperimentResult<P> {
|
|
7
|
+
inExperiment: boolean;
|
|
8
|
+
group: string;
|
|
9
|
+
params: P;
|
|
10
|
+
}
|
|
11
|
+
interface EvalFlagResult {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
interface EvalExpResult {
|
|
15
|
+
in_experiment: boolean;
|
|
16
|
+
group: string;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
interface EvalResponse {
|
|
20
|
+
flags: Record<string, EvalFlagResult>;
|
|
21
|
+
experiments: Record<string, EvalExpResult>;
|
|
22
|
+
}
|
|
23
|
+
interface FlagsClientBrowserOptions {
|
|
24
|
+
sdkKey: string;
|
|
25
|
+
baseUrl?: string;
|
|
26
|
+
autoGuardrails?: boolean;
|
|
27
|
+
}
|
|
28
|
+
declare class FlagsClientBrowser {
|
|
29
|
+
private readonly sdkKey;
|
|
30
|
+
private readonly baseUrl;
|
|
31
|
+
private readonly autoGuardrails;
|
|
32
|
+
private evalResult;
|
|
33
|
+
private anonId;
|
|
34
|
+
private userId;
|
|
35
|
+
private buffer;
|
|
36
|
+
private guardrailsInstalled;
|
|
37
|
+
constructor(opts: FlagsClientBrowserOptions);
|
|
38
|
+
identify(user: User): Promise<void>;
|
|
39
|
+
initFromBootstrap(data: EvalResponse): void;
|
|
40
|
+
getFlag(name: string): boolean;
|
|
41
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
42
|
+
getExperiment<P extends Record<string, unknown>>(name: string, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
43
|
+
track(eventName: string, props?: Record<string, unknown>): void;
|
|
44
|
+
flush(): Promise<void>;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { type ExperimentResult, FlagsClientBrowser, type FlagsClientBrowserOptions, type User, version };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client/index.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
FlagsClientBrowser: () => FlagsClientBrowser,
|
|
24
|
+
version: () => version
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(client_exports);
|
|
27
|
+
var version = "1.0.0";
|
|
28
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
29
|
+
var MAX_BUFFER = 100;
|
|
30
|
+
var ANON_ID_KEY = "__se_anon_id";
|
|
31
|
+
var SEEN_KEY = "__se_seen";
|
|
32
|
+
var PENDING_ALIAS_KEY = "__se_pending_alias";
|
|
33
|
+
var EventBuffer = class {
|
|
34
|
+
constructor(collectUrl, sdkKey) {
|
|
35
|
+
this.collectUrl = collectUrl;
|
|
36
|
+
this.sdkKey = sdkKey;
|
|
37
|
+
if (typeof window !== "undefined") {
|
|
38
|
+
this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
39
|
+
window.addEventListener("beforeunload", () => this.flush());
|
|
40
|
+
document.addEventListener("visibilitychange", () => {
|
|
41
|
+
if (document.visibilityState === "hidden") this.flush(true);
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
const stored = sessionStorage.getItem(SEEN_KEY);
|
|
45
|
+
if (stored) this.exposureSeen = new Set(JSON.parse(stored));
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
collectUrl;
|
|
51
|
+
sdkKey;
|
|
52
|
+
queue = [];
|
|
53
|
+
exposureSeen = /* @__PURE__ */ new Set();
|
|
54
|
+
timer = null;
|
|
55
|
+
destroy() {
|
|
56
|
+
if (this.timer !== null) {
|
|
57
|
+
clearInterval(this.timer);
|
|
58
|
+
this.timer = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
pushExposure(experiment, group, userId, anonId) {
|
|
62
|
+
const key = `${userId || anonId}:${experiment}`;
|
|
63
|
+
if (this.exposureSeen.has(key)) return;
|
|
64
|
+
this.exposureSeen.add(key);
|
|
65
|
+
try {
|
|
66
|
+
sessionStorage.setItem(SEEN_KEY, JSON.stringify([...this.exposureSeen]));
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
this.enqueue({
|
|
70
|
+
type: "exposure",
|
|
71
|
+
experiment,
|
|
72
|
+
group,
|
|
73
|
+
user_id: userId,
|
|
74
|
+
anonymous_id: anonId,
|
|
75
|
+
ts: Date.now()
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
pushMetric(eventName, userId, anonId, props) {
|
|
79
|
+
this.enqueue({
|
|
80
|
+
type: "metric",
|
|
81
|
+
event_name: eventName,
|
|
82
|
+
user_id: userId,
|
|
83
|
+
anonymous_id: anonId,
|
|
84
|
+
ts: Date.now(),
|
|
85
|
+
...props ? { properties: props } : {}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async alias(anonymousId, userId) {
|
|
89
|
+
const record = { anonymousId, userId, ts: Date.now() };
|
|
90
|
+
try {
|
|
91
|
+
localStorage.setItem(PENDING_ALIAS_KEY, JSON.stringify(record));
|
|
92
|
+
} catch {
|
|
93
|
+
}
|
|
94
|
+
await this.flushAsync();
|
|
95
|
+
await this._sendAlias(anonymousId, userId);
|
|
96
|
+
try {
|
|
97
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async flushPendingAlias() {
|
|
102
|
+
try {
|
|
103
|
+
const raw = localStorage.getItem(PENDING_ALIAS_KEY);
|
|
104
|
+
if (!raw) return;
|
|
105
|
+
const record = JSON.parse(raw);
|
|
106
|
+
if (Date.now() - record.ts > 7 * 864e5) {
|
|
107
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await this._sendAlias(record.anonymousId, record.userId);
|
|
111
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
112
|
+
} catch {
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async _sendAlias(anonymousId, userId) {
|
|
116
|
+
this.enqueue({ type: "identify", anonymous_id: anonymousId, user_id: userId, ts: Date.now() });
|
|
117
|
+
await this.flushAsync();
|
|
118
|
+
}
|
|
119
|
+
enqueue(ev) {
|
|
120
|
+
this.queue.push(ev);
|
|
121
|
+
if (this.queue.length >= MAX_BUFFER) this.flush();
|
|
122
|
+
}
|
|
123
|
+
flush(useBeacon = false) {
|
|
124
|
+
if (!this.queue.length) return;
|
|
125
|
+
const batch = this.queue.splice(0);
|
|
126
|
+
const body = JSON.stringify({ events: batch });
|
|
127
|
+
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
128
|
+
navigator.sendBeacon(this.collectUrl, new Blob([body], { type: "text/plain" }));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
fetch(this.collectUrl, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
134
|
+
body,
|
|
135
|
+
keepalive: true
|
|
136
|
+
}).catch(() => {
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async flushAsync() {
|
|
140
|
+
if (!this.queue.length) return;
|
|
141
|
+
const batch = this.queue.splice(0);
|
|
142
|
+
const body = JSON.stringify({ events: batch });
|
|
143
|
+
await fetch(this.collectUrl, {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
146
|
+
body
|
|
147
|
+
}).catch(() => {
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
function installAutoGuardrails(buffer, userId, anonId) {
|
|
152
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
153
|
+
let lcp = null;
|
|
154
|
+
let inp = null;
|
|
155
|
+
let clsBad = false;
|
|
156
|
+
let jsError = false;
|
|
157
|
+
let netError = false;
|
|
158
|
+
try {
|
|
159
|
+
const lcpObs = new PerformanceObserver((list) => {
|
|
160
|
+
const entries = list.getEntries();
|
|
161
|
+
if (entries.length)
|
|
162
|
+
lcp = entries[entries.length - 1].startTime;
|
|
163
|
+
});
|
|
164
|
+
lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
try {
|
|
168
|
+
const inpObs = new PerformanceObserver((list) => {
|
|
169
|
+
for (const e of list.getEntries()) {
|
|
170
|
+
const dur = e.duration ?? 0;
|
|
171
|
+
if (inp === null || dur > inp) inp = dur;
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
inpObs.observe({
|
|
175
|
+
type: "event",
|
|
176
|
+
buffered: true,
|
|
177
|
+
durationThreshold: 16
|
|
178
|
+
});
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const clsObs = new PerformanceObserver((list) => {
|
|
183
|
+
for (const e of list.getEntries()) {
|
|
184
|
+
if (e.value > 0.1) clsBad = true;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
clsObs.observe({ type: "layout-shift", buffered: true });
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
const origOnError = window.onerror;
|
|
191
|
+
window.onerror = (...args) => {
|
|
192
|
+
if (!jsError) {
|
|
193
|
+
jsError = true;
|
|
194
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
|
|
195
|
+
}
|
|
196
|
+
if (typeof origOnError === "function") return origOnError(...args);
|
|
197
|
+
return false;
|
|
198
|
+
};
|
|
199
|
+
window.addEventListener("unhandledrejection", () => {
|
|
200
|
+
if (!jsError) {
|
|
201
|
+
jsError = true;
|
|
202
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const origFetch = window.fetch;
|
|
206
|
+
window.fetch = async function(...args) {
|
|
207
|
+
const res = await origFetch.apply(this, args);
|
|
208
|
+
if (!netError && res.status >= 500) {
|
|
209
|
+
netError = true;
|
|
210
|
+
buffer.pushMetric("__auto_network_error", userId, anonId, { value: 1 });
|
|
211
|
+
}
|
|
212
|
+
return res;
|
|
213
|
+
};
|
|
214
|
+
const flush = () => {
|
|
215
|
+
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
216
|
+
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
217
|
+
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
218
|
+
const abandoned = lcp === null ? 1 : 0;
|
|
219
|
+
buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
|
|
220
|
+
buffer.flush(true);
|
|
221
|
+
};
|
|
222
|
+
document.addEventListener("visibilitychange", () => {
|
|
223
|
+
if (document.visibilityState === "hidden") flush();
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
function getOrCreateAnonId() {
|
|
227
|
+
try {
|
|
228
|
+
const stored = localStorage.getItem(ANON_ID_KEY);
|
|
229
|
+
if (stored) return stored;
|
|
230
|
+
} catch {
|
|
231
|
+
}
|
|
232
|
+
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
|
|
233
|
+
try {
|
|
234
|
+
localStorage.setItem(ANON_ID_KEY, id);
|
|
235
|
+
} catch {
|
|
236
|
+
}
|
|
237
|
+
return id;
|
|
238
|
+
}
|
|
239
|
+
var FlagsClientBrowser = class {
|
|
240
|
+
sdkKey;
|
|
241
|
+
baseUrl;
|
|
242
|
+
autoGuardrails;
|
|
243
|
+
evalResult = null;
|
|
244
|
+
anonId;
|
|
245
|
+
userId = "";
|
|
246
|
+
buffer;
|
|
247
|
+
guardrailsInstalled = false;
|
|
248
|
+
constructor(opts) {
|
|
249
|
+
this.sdkKey = opts.sdkKey;
|
|
250
|
+
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
251
|
+
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
252
|
+
this.anonId = getOrCreateAnonId();
|
|
253
|
+
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
254
|
+
void this.buffer.flushPendingAlias();
|
|
255
|
+
}
|
|
256
|
+
async identify(user) {
|
|
257
|
+
const prevUserId = this.userId;
|
|
258
|
+
this.userId = user.user_id ?? "";
|
|
259
|
+
if (this.anonId && this.userId && this.userId !== prevUserId) {
|
|
260
|
+
await this.buffer.alias(this.anonId, this.userId);
|
|
261
|
+
}
|
|
262
|
+
const res = await fetch(`${this.baseUrl}/sdk/evaluate`, {
|
|
263
|
+
method: "POST",
|
|
264
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
265
|
+
body: JSON.stringify({ user })
|
|
266
|
+
});
|
|
267
|
+
if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
|
|
268
|
+
this.evalResult = await res.json();
|
|
269
|
+
if (this.autoGuardrails && !this.guardrailsInstalled) {
|
|
270
|
+
this.guardrailsInstalled = true;
|
|
271
|
+
installAutoGuardrails(this.buffer, this.userId, this.anonId);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
initFromBootstrap(data) {
|
|
275
|
+
this.evalResult = data;
|
|
276
|
+
}
|
|
277
|
+
getFlag(name) {
|
|
278
|
+
return this.evalResult?.flags[name]?.enabled ?? false;
|
|
279
|
+
}
|
|
280
|
+
getConfig(name, decode) {
|
|
281
|
+
void name;
|
|
282
|
+
void decode;
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
getExperiment(name, defaultParams, decode) {
|
|
286
|
+
const notIn = {
|
|
287
|
+
inExperiment: false,
|
|
288
|
+
group: "control",
|
|
289
|
+
params: defaultParams
|
|
290
|
+
};
|
|
291
|
+
const entry = this.evalResult?.experiments[name];
|
|
292
|
+
if (!entry || !entry.in_experiment) return notIn;
|
|
293
|
+
this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
|
|
294
|
+
if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
|
|
295
|
+
try {
|
|
296
|
+
return { inExperiment: true, group: entry.group, params: decode(entry.params) };
|
|
297
|
+
} catch (err) {
|
|
298
|
+
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
299
|
+
return notIn;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
track(eventName, props) {
|
|
303
|
+
this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
|
|
304
|
+
}
|
|
305
|
+
async flush() {
|
|
306
|
+
await this.buffer.flushAsync();
|
|
307
|
+
}
|
|
308
|
+
destroy() {
|
|
309
|
+
this.buffer.flush();
|
|
310
|
+
this.buffer.destroy();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
314
|
+
0 && (module.exports = {
|
|
315
|
+
FlagsClientBrowser,
|
|
316
|
+
version
|
|
317
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// src/client/index.ts
|
|
2
|
+
var version = "1.0.0";
|
|
3
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
4
|
+
var MAX_BUFFER = 100;
|
|
5
|
+
var ANON_ID_KEY = "__se_anon_id";
|
|
6
|
+
var SEEN_KEY = "__se_seen";
|
|
7
|
+
var PENDING_ALIAS_KEY = "__se_pending_alias";
|
|
8
|
+
var EventBuffer = class {
|
|
9
|
+
constructor(collectUrl, sdkKey) {
|
|
10
|
+
this.collectUrl = collectUrl;
|
|
11
|
+
this.sdkKey = sdkKey;
|
|
12
|
+
if (typeof window !== "undefined") {
|
|
13
|
+
this.timer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
14
|
+
window.addEventListener("beforeunload", () => this.flush());
|
|
15
|
+
document.addEventListener("visibilitychange", () => {
|
|
16
|
+
if (document.visibilityState === "hidden") this.flush(true);
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
const stored = sessionStorage.getItem(SEEN_KEY);
|
|
20
|
+
if (stored) this.exposureSeen = new Set(JSON.parse(stored));
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
collectUrl;
|
|
26
|
+
sdkKey;
|
|
27
|
+
queue = [];
|
|
28
|
+
exposureSeen = /* @__PURE__ */ new Set();
|
|
29
|
+
timer = null;
|
|
30
|
+
destroy() {
|
|
31
|
+
if (this.timer !== null) {
|
|
32
|
+
clearInterval(this.timer);
|
|
33
|
+
this.timer = null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
pushExposure(experiment, group, userId, anonId) {
|
|
37
|
+
const key = `${userId || anonId}:${experiment}`;
|
|
38
|
+
if (this.exposureSeen.has(key)) return;
|
|
39
|
+
this.exposureSeen.add(key);
|
|
40
|
+
try {
|
|
41
|
+
sessionStorage.setItem(SEEN_KEY, JSON.stringify([...this.exposureSeen]));
|
|
42
|
+
} catch {
|
|
43
|
+
}
|
|
44
|
+
this.enqueue({
|
|
45
|
+
type: "exposure",
|
|
46
|
+
experiment,
|
|
47
|
+
group,
|
|
48
|
+
user_id: userId,
|
|
49
|
+
anonymous_id: anonId,
|
|
50
|
+
ts: Date.now()
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
pushMetric(eventName, userId, anonId, props) {
|
|
54
|
+
this.enqueue({
|
|
55
|
+
type: "metric",
|
|
56
|
+
event_name: eventName,
|
|
57
|
+
user_id: userId,
|
|
58
|
+
anonymous_id: anonId,
|
|
59
|
+
ts: Date.now(),
|
|
60
|
+
...props ? { properties: props } : {}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async alias(anonymousId, userId) {
|
|
64
|
+
const record = { anonymousId, userId, ts: Date.now() };
|
|
65
|
+
try {
|
|
66
|
+
localStorage.setItem(PENDING_ALIAS_KEY, JSON.stringify(record));
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
await this.flushAsync();
|
|
70
|
+
await this._sendAlias(anonymousId, userId);
|
|
71
|
+
try {
|
|
72
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async flushPendingAlias() {
|
|
77
|
+
try {
|
|
78
|
+
const raw = localStorage.getItem(PENDING_ALIAS_KEY);
|
|
79
|
+
if (!raw) return;
|
|
80
|
+
const record = JSON.parse(raw);
|
|
81
|
+
if (Date.now() - record.ts > 7 * 864e5) {
|
|
82
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await this._sendAlias(record.anonymousId, record.userId);
|
|
86
|
+
localStorage.removeItem(PENDING_ALIAS_KEY);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async _sendAlias(anonymousId, userId) {
|
|
91
|
+
this.enqueue({ type: "identify", anonymous_id: anonymousId, user_id: userId, ts: Date.now() });
|
|
92
|
+
await this.flushAsync();
|
|
93
|
+
}
|
|
94
|
+
enqueue(ev) {
|
|
95
|
+
this.queue.push(ev);
|
|
96
|
+
if (this.queue.length >= MAX_BUFFER) this.flush();
|
|
97
|
+
}
|
|
98
|
+
flush(useBeacon = false) {
|
|
99
|
+
if (!this.queue.length) return;
|
|
100
|
+
const batch = this.queue.splice(0);
|
|
101
|
+
const body = JSON.stringify({ events: batch });
|
|
102
|
+
if (useBeacon && typeof navigator !== "undefined" && navigator.sendBeacon) {
|
|
103
|
+
navigator.sendBeacon(this.collectUrl, new Blob([body], { type: "text/plain" }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
fetch(this.collectUrl, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
109
|
+
body,
|
|
110
|
+
keepalive: true
|
|
111
|
+
}).catch(() => {
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async flushAsync() {
|
|
115
|
+
if (!this.queue.length) return;
|
|
116
|
+
const batch = this.queue.splice(0);
|
|
117
|
+
const body = JSON.stringify({ events: batch });
|
|
118
|
+
await fetch(this.collectUrl, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
121
|
+
body
|
|
122
|
+
}).catch(() => {
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
function installAutoGuardrails(buffer, userId, anonId) {
|
|
127
|
+
if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return;
|
|
128
|
+
let lcp = null;
|
|
129
|
+
let inp = null;
|
|
130
|
+
let clsBad = false;
|
|
131
|
+
let jsError = false;
|
|
132
|
+
let netError = false;
|
|
133
|
+
try {
|
|
134
|
+
const lcpObs = new PerformanceObserver((list) => {
|
|
135
|
+
const entries = list.getEntries();
|
|
136
|
+
if (entries.length)
|
|
137
|
+
lcp = entries[entries.length - 1].startTime;
|
|
138
|
+
});
|
|
139
|
+
lcpObs.observe({ type: "largest-contentful-paint", buffered: true });
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const inpObs = new PerformanceObserver((list) => {
|
|
144
|
+
for (const e of list.getEntries()) {
|
|
145
|
+
const dur = e.duration ?? 0;
|
|
146
|
+
if (inp === null || dur > inp) inp = dur;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
inpObs.observe({
|
|
150
|
+
type: "event",
|
|
151
|
+
buffered: true,
|
|
152
|
+
durationThreshold: 16
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
const clsObs = new PerformanceObserver((list) => {
|
|
158
|
+
for (const e of list.getEntries()) {
|
|
159
|
+
if (e.value > 0.1) clsBad = true;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
clsObs.observe({ type: "layout-shift", buffered: true });
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
const origOnError = window.onerror;
|
|
166
|
+
window.onerror = (...args) => {
|
|
167
|
+
if (!jsError) {
|
|
168
|
+
jsError = true;
|
|
169
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
|
|
170
|
+
}
|
|
171
|
+
if (typeof origOnError === "function") return origOnError(...args);
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
window.addEventListener("unhandledrejection", () => {
|
|
175
|
+
if (!jsError) {
|
|
176
|
+
jsError = true;
|
|
177
|
+
buffer.pushMetric("__auto_js_error", userId, anonId, { value: 1 });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const origFetch = window.fetch;
|
|
181
|
+
window.fetch = async function(...args) {
|
|
182
|
+
const res = await origFetch.apply(this, args);
|
|
183
|
+
if (!netError && res.status >= 500) {
|
|
184
|
+
netError = true;
|
|
185
|
+
buffer.pushMetric("__auto_network_error", userId, anonId, { value: 1 });
|
|
186
|
+
}
|
|
187
|
+
return res;
|
|
188
|
+
};
|
|
189
|
+
const flush = () => {
|
|
190
|
+
if (lcp !== null) buffer.pushMetric("__auto_lcp", userId, anonId, { value: lcp });
|
|
191
|
+
if (inp !== null) buffer.pushMetric("__auto_inp", userId, anonId, { value: inp });
|
|
192
|
+
if (clsBad) buffer.pushMetric("__auto_cls_binary", userId, anonId, { value: 1 });
|
|
193
|
+
const abandoned = lcp === null ? 1 : 0;
|
|
194
|
+
buffer.pushMetric("__auto_abandoned", userId, anonId, { value: abandoned });
|
|
195
|
+
buffer.flush(true);
|
|
196
|
+
};
|
|
197
|
+
document.addEventListener("visibilitychange", () => {
|
|
198
|
+
if (document.visibilityState === "hidden") flush();
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function getOrCreateAnonId() {
|
|
202
|
+
try {
|
|
203
|
+
const stored = localStorage.getItem(ANON_ID_KEY);
|
|
204
|
+
if (stored) return stored;
|
|
205
|
+
} catch {
|
|
206
|
+
}
|
|
207
|
+
const id = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID() : `anon_${Math.random().toString(36).slice(2)}`;
|
|
208
|
+
try {
|
|
209
|
+
localStorage.setItem(ANON_ID_KEY, id);
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
return id;
|
|
213
|
+
}
|
|
214
|
+
var FlagsClientBrowser = class {
|
|
215
|
+
sdkKey;
|
|
216
|
+
baseUrl;
|
|
217
|
+
autoGuardrails;
|
|
218
|
+
evalResult = null;
|
|
219
|
+
anonId;
|
|
220
|
+
userId = "";
|
|
221
|
+
buffer;
|
|
222
|
+
guardrailsInstalled = false;
|
|
223
|
+
constructor(opts) {
|
|
224
|
+
this.sdkKey = opts.sdkKey;
|
|
225
|
+
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
226
|
+
this.autoGuardrails = opts.autoGuardrails !== false;
|
|
227
|
+
this.anonId = getOrCreateAnonId();
|
|
228
|
+
this.buffer = new EventBuffer(`${this.baseUrl}/collect`, this.sdkKey);
|
|
229
|
+
void this.buffer.flushPendingAlias();
|
|
230
|
+
}
|
|
231
|
+
async identify(user) {
|
|
232
|
+
const prevUserId = this.userId;
|
|
233
|
+
this.userId = user.user_id ?? "";
|
|
234
|
+
if (this.anonId && this.userId && this.userId !== prevUserId) {
|
|
235
|
+
await this.buffer.alias(this.anonId, this.userId);
|
|
236
|
+
}
|
|
237
|
+
const res = await fetch(`${this.baseUrl}/sdk/evaluate`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers: { "X-SDK-Key": this.sdkKey, "Content-Type": "application/json" },
|
|
240
|
+
body: JSON.stringify({ user })
|
|
241
|
+
});
|
|
242
|
+
if (!res.ok) throw new Error(`/sdk/evaluate returned ${res.status}`);
|
|
243
|
+
this.evalResult = await res.json();
|
|
244
|
+
if (this.autoGuardrails && !this.guardrailsInstalled) {
|
|
245
|
+
this.guardrailsInstalled = true;
|
|
246
|
+
installAutoGuardrails(this.buffer, this.userId, this.anonId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
initFromBootstrap(data) {
|
|
250
|
+
this.evalResult = data;
|
|
251
|
+
}
|
|
252
|
+
getFlag(name) {
|
|
253
|
+
return this.evalResult?.flags[name]?.enabled ?? false;
|
|
254
|
+
}
|
|
255
|
+
getConfig(name, decode) {
|
|
256
|
+
void name;
|
|
257
|
+
void decode;
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
getExperiment(name, defaultParams, decode) {
|
|
261
|
+
const notIn = {
|
|
262
|
+
inExperiment: false,
|
|
263
|
+
group: "control",
|
|
264
|
+
params: defaultParams
|
|
265
|
+
};
|
|
266
|
+
const entry = this.evalResult?.experiments[name];
|
|
267
|
+
if (!entry || !entry.in_experiment) return notIn;
|
|
268
|
+
this.buffer.pushExposure(name, entry.group, this.userId, this.anonId);
|
|
269
|
+
if (!decode) return { inExperiment: true, group: entry.group, params: entry.params };
|
|
270
|
+
try {
|
|
271
|
+
return { inExperiment: true, group: entry.group, params: decode(entry.params) };
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
274
|
+
return notIn;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
track(eventName, props) {
|
|
278
|
+
this.buffer.pushMetric(eventName, this.userId, this.anonId, props);
|
|
279
|
+
}
|
|
280
|
+
async flush() {
|
|
281
|
+
await this.buffer.flushAsync();
|
|
282
|
+
}
|
|
283
|
+
destroy() {
|
|
284
|
+
this.buffer.flush();
|
|
285
|
+
this.buffer.destroy();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
export {
|
|
289
|
+
FlagsClientBrowser,
|
|
290
|
+
version
|
|
291
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare const version = "1.0.0";
|
|
2
|
+
interface User {
|
|
3
|
+
user_id?: string;
|
|
4
|
+
anonymous_id?: string;
|
|
5
|
+
[attr: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
interface ExperimentResult<P> {
|
|
8
|
+
inExperiment: boolean;
|
|
9
|
+
group: string;
|
|
10
|
+
params: P;
|
|
11
|
+
}
|
|
12
|
+
interface FlagsClientOptions {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
declare class FlagsClient {
|
|
17
|
+
private readonly apiKey;
|
|
18
|
+
private readonly baseUrl;
|
|
19
|
+
private flagsBlob;
|
|
20
|
+
private expsBlob;
|
|
21
|
+
private flagsEtag;
|
|
22
|
+
private expsEtag;
|
|
23
|
+
private pollInterval;
|
|
24
|
+
private timer;
|
|
25
|
+
private initialized;
|
|
26
|
+
constructor(opts: FlagsClientOptions);
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
initOnce(): Promise<void>;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
private startPoll;
|
|
31
|
+
private fetchAll;
|
|
32
|
+
private fetchFlags;
|
|
33
|
+
private fetchExps;
|
|
34
|
+
getFlag(name: string, user: User): boolean;
|
|
35
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
36
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
37
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
declare const version = "1.0.0";
|
|
2
|
+
interface User {
|
|
3
|
+
user_id?: string;
|
|
4
|
+
anonymous_id?: string;
|
|
5
|
+
[attr: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
interface ExperimentResult<P> {
|
|
8
|
+
inExperiment: boolean;
|
|
9
|
+
group: string;
|
|
10
|
+
params: P;
|
|
11
|
+
}
|
|
12
|
+
interface FlagsClientOptions {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
declare class FlagsClient {
|
|
17
|
+
private readonly apiKey;
|
|
18
|
+
private readonly baseUrl;
|
|
19
|
+
private flagsBlob;
|
|
20
|
+
private expsBlob;
|
|
21
|
+
private flagsEtag;
|
|
22
|
+
private expsEtag;
|
|
23
|
+
private pollInterval;
|
|
24
|
+
private timer;
|
|
25
|
+
private initialized;
|
|
26
|
+
constructor(opts: FlagsClientOptions);
|
|
27
|
+
init(): Promise<void>;
|
|
28
|
+
initOnce(): Promise<void>;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
private startPoll;
|
|
31
|
+
private fetchAll;
|
|
32
|
+
private fetchFlags;
|
|
33
|
+
private fetchExps;
|
|
34
|
+
getFlag(name: string, user: User): boolean;
|
|
35
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
36
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
37
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server/index.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
FlagsClient: () => FlagsClient,
|
|
24
|
+
version: () => version
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(server_exports);
|
|
27
|
+
var version = "1.0.0";
|
|
28
|
+
var C1 = 3432918353;
|
|
29
|
+
var C2 = 461845907;
|
|
30
|
+
function murmur3(key) {
|
|
31
|
+
const bytes = new TextEncoder().encode(key);
|
|
32
|
+
const len = bytes.length;
|
|
33
|
+
const nblocks = len >>> 2;
|
|
34
|
+
let h1 = 0;
|
|
35
|
+
for (let i = 0; i < nblocks; i++) {
|
|
36
|
+
const off = i * 4;
|
|
37
|
+
let k12 = bytes[off] | bytes[off + 1] << 8 | bytes[off + 2] << 16 | bytes[off + 3] << 24;
|
|
38
|
+
k12 = Math.imul(k12, C1);
|
|
39
|
+
k12 = k12 << 15 | k12 >>> 17;
|
|
40
|
+
k12 = Math.imul(k12, C2);
|
|
41
|
+
h1 ^= k12;
|
|
42
|
+
h1 = h1 << 13 | h1 >>> 19;
|
|
43
|
+
h1 = Math.imul(h1, 5) + 3864292196;
|
|
44
|
+
h1 |= 0;
|
|
45
|
+
}
|
|
46
|
+
let k1 = 0;
|
|
47
|
+
const tail = nblocks * 4;
|
|
48
|
+
switch (len & 3) {
|
|
49
|
+
case 3:
|
|
50
|
+
k1 ^= bytes[tail + 2] << 16;
|
|
51
|
+
// fallthrough
|
|
52
|
+
case 2:
|
|
53
|
+
k1 ^= bytes[tail + 1] << 8;
|
|
54
|
+
// fallthrough
|
|
55
|
+
case 1:
|
|
56
|
+
k1 ^= bytes[tail];
|
|
57
|
+
k1 = Math.imul(k1, C1);
|
|
58
|
+
k1 = k1 << 15 | k1 >>> 17;
|
|
59
|
+
k1 = Math.imul(k1, C2);
|
|
60
|
+
h1 ^= k1;
|
|
61
|
+
}
|
|
62
|
+
h1 ^= len;
|
|
63
|
+
h1 ^= h1 >>> 16;
|
|
64
|
+
h1 = Math.imul(h1, 2246822507);
|
|
65
|
+
h1 ^= h1 >>> 13;
|
|
66
|
+
h1 = Math.imul(h1, 3266489909);
|
|
67
|
+
h1 ^= h1 >>> 16;
|
|
68
|
+
return h1 >>> 0;
|
|
69
|
+
}
|
|
70
|
+
function isEnabled(v) {
|
|
71
|
+
return v === 1 || v === true;
|
|
72
|
+
}
|
|
73
|
+
function toNum(v) {
|
|
74
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
75
|
+
if (typeof v === "string" && v !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function matchRule(rule, user) {
|
|
79
|
+
const actual = user[rule.attr];
|
|
80
|
+
switch (rule.op) {
|
|
81
|
+
case "eq":
|
|
82
|
+
return actual === rule.value;
|
|
83
|
+
case "neq":
|
|
84
|
+
return actual !== rule.value;
|
|
85
|
+
case "in":
|
|
86
|
+
return Array.isArray(rule.value) && rule.value.includes(actual);
|
|
87
|
+
case "not_in":
|
|
88
|
+
return Array.isArray(rule.value) && !rule.value.includes(actual);
|
|
89
|
+
case "gt":
|
|
90
|
+
case "gte":
|
|
91
|
+
case "lt":
|
|
92
|
+
case "lte": {
|
|
93
|
+
const a = toNum(actual);
|
|
94
|
+
const b = toNum(rule.value);
|
|
95
|
+
if (a === null || b === null) return false;
|
|
96
|
+
if (rule.op === "gt") return a > b;
|
|
97
|
+
if (rule.op === "gte") return a >= b;
|
|
98
|
+
if (rule.op === "lt") return a < b;
|
|
99
|
+
return a <= b;
|
|
100
|
+
}
|
|
101
|
+
case "contains":
|
|
102
|
+
if (typeof actual === "string" && typeof rule.value === "string")
|
|
103
|
+
return actual.includes(rule.value);
|
|
104
|
+
if (Array.isArray(actual)) return actual.includes(rule.value);
|
|
105
|
+
return false;
|
|
106
|
+
case "regex":
|
|
107
|
+
if (typeof actual !== "string" || typeof rule.value !== "string") return false;
|
|
108
|
+
try {
|
|
109
|
+
return new RegExp(rule.value).test(actual);
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function evalGateInternal(gate, user) {
|
|
118
|
+
if (isEnabled(gate.killswitch)) return false;
|
|
119
|
+
if (!isEnabled(gate.enabled)) return false;
|
|
120
|
+
for (const rule of gate.rules ?? []) {
|
|
121
|
+
if (!matchRule(rule, user)) return false;
|
|
122
|
+
}
|
|
123
|
+
const uid = user.user_id ?? user.anonymous_id;
|
|
124
|
+
if (!uid) return false;
|
|
125
|
+
return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
|
|
126
|
+
}
|
|
127
|
+
var FlagsClient = class {
|
|
128
|
+
apiKey;
|
|
129
|
+
baseUrl;
|
|
130
|
+
flagsBlob = null;
|
|
131
|
+
expsBlob = null;
|
|
132
|
+
flagsEtag = null;
|
|
133
|
+
expsEtag = null;
|
|
134
|
+
pollInterval = 30;
|
|
135
|
+
timer = null;
|
|
136
|
+
initialized = false;
|
|
137
|
+
constructor(opts) {
|
|
138
|
+
this.apiKey = opts.apiKey;
|
|
139
|
+
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
140
|
+
}
|
|
141
|
+
async init() {
|
|
142
|
+
await this.fetchAll();
|
|
143
|
+
this.initialized = true;
|
|
144
|
+
this.startPoll();
|
|
145
|
+
}
|
|
146
|
+
async initOnce() {
|
|
147
|
+
if (this.initialized) return;
|
|
148
|
+
await this.fetchAll();
|
|
149
|
+
this.initialized = true;
|
|
150
|
+
}
|
|
151
|
+
destroy() {
|
|
152
|
+
if (this.timer !== null) {
|
|
153
|
+
clearInterval(this.timer);
|
|
154
|
+
this.timer = null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
startPoll() {
|
|
158
|
+
this.timer = setInterval(() => {
|
|
159
|
+
this.fetchAll().catch(
|
|
160
|
+
(err) => console.warn("[shipeasy] background poll failed:", String(err))
|
|
161
|
+
);
|
|
162
|
+
}, this.pollInterval * 1e3);
|
|
163
|
+
}
|
|
164
|
+
async fetchAll() {
|
|
165
|
+
const [interval] = await Promise.all([this.fetchFlags(), this.fetchExps()]);
|
|
166
|
+
if (interval !== null && interval !== this.pollInterval) {
|
|
167
|
+
this.pollInterval = interval;
|
|
168
|
+
if (this.timer !== null) {
|
|
169
|
+
clearInterval(this.timer);
|
|
170
|
+
this.startPoll();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async fetchFlags() {
|
|
175
|
+
const headers = { "X-SDK-Key": this.apiKey };
|
|
176
|
+
if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
|
|
177
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags`, { headers });
|
|
178
|
+
const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
|
|
179
|
+
if (res.status === 304) return interval;
|
|
180
|
+
if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
|
|
181
|
+
const etag = res.headers.get("ETag");
|
|
182
|
+
if (etag) this.flagsEtag = etag;
|
|
183
|
+
this.flagsBlob = await res.json();
|
|
184
|
+
return interval;
|
|
185
|
+
}
|
|
186
|
+
async fetchExps() {
|
|
187
|
+
const headers = { "X-SDK-Key": this.apiKey };
|
|
188
|
+
if (this.expsEtag) headers["If-None-Match"] = this.expsEtag;
|
|
189
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/experiments`, { headers });
|
|
190
|
+
if (res.status === 304) return;
|
|
191
|
+
if (!res.ok) throw new Error(`/sdk/experiments returned ${res.status}`);
|
|
192
|
+
const etag = res.headers.get("ETag");
|
|
193
|
+
if (etag) this.expsEtag = etag;
|
|
194
|
+
this.expsBlob = await res.json();
|
|
195
|
+
}
|
|
196
|
+
getFlag(name, user) {
|
|
197
|
+
const gate = this.flagsBlob?.gates[name];
|
|
198
|
+
if (!gate) return false;
|
|
199
|
+
return evalGateInternal(gate, user);
|
|
200
|
+
}
|
|
201
|
+
getConfig(name, decode) {
|
|
202
|
+
const entry = this.flagsBlob?.configs[name];
|
|
203
|
+
if (!entry) return void 0;
|
|
204
|
+
if (!decode) return entry.value;
|
|
205
|
+
return decode(entry.value);
|
|
206
|
+
}
|
|
207
|
+
getExperiment(name, user, defaultParams, decode) {
|
|
208
|
+
const notIn = {
|
|
209
|
+
inExperiment: false,
|
|
210
|
+
group: "control",
|
|
211
|
+
params: defaultParams
|
|
212
|
+
};
|
|
213
|
+
if (!this.flagsBlob || !this.expsBlob) return notIn;
|
|
214
|
+
const exp = this.expsBlob.experiments[name];
|
|
215
|
+
if (!exp || exp.status !== "running") return notIn;
|
|
216
|
+
if (exp.targetingGate) {
|
|
217
|
+
const gate = this.flagsBlob.gates[exp.targetingGate];
|
|
218
|
+
if (!gate || !evalGateInternal(gate, user)) return notIn;
|
|
219
|
+
}
|
|
220
|
+
const uid = user.user_id ?? user.anonymous_id;
|
|
221
|
+
if (!uid) return notIn;
|
|
222
|
+
const universe = this.expsBlob.universes[exp.universe];
|
|
223
|
+
const holdoutRange = universe?.holdout_range ?? null;
|
|
224
|
+
if (holdoutRange) {
|
|
225
|
+
const seg = murmur3(`${exp.universe}:${uid}`) % 1e4;
|
|
226
|
+
const [lo, hi] = holdoutRange;
|
|
227
|
+
if (seg >= lo && seg <= hi) return notIn;
|
|
228
|
+
}
|
|
229
|
+
if (murmur3(`${exp.salt}:alloc:${uid}`) % 1e4 >= exp.allocationPct) return notIn;
|
|
230
|
+
const groupHash = murmur3(`${exp.salt}:group:${uid}`) % 1e4;
|
|
231
|
+
let cumulative = 0;
|
|
232
|
+
for (let i = 0; i < exp.groups.length; i++) {
|
|
233
|
+
const g = exp.groups[i];
|
|
234
|
+
cumulative += g.weight;
|
|
235
|
+
if (groupHash < cumulative || i === exp.groups.length - 1) {
|
|
236
|
+
if (!decode) {
|
|
237
|
+
return { inExperiment: true, group: g.name, params: g.params };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
return { inExperiment: true, group: g.name, params: decode(g.params) };
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
243
|
+
return notIn;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return notIn;
|
|
248
|
+
}
|
|
249
|
+
track(userId, eventName, props) {
|
|
250
|
+
const body = JSON.stringify({
|
|
251
|
+
events: [
|
|
252
|
+
{
|
|
253
|
+
type: "metric",
|
|
254
|
+
event_name: eventName,
|
|
255
|
+
user_id: userId,
|
|
256
|
+
ts: Date.now(),
|
|
257
|
+
...props !== void 0 ? { properties: props } : {}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
});
|
|
261
|
+
globalThis.fetch(`${this.baseUrl}/collect`, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
|
|
264
|
+
body
|
|
265
|
+
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
269
|
+
0 && (module.exports = {
|
|
270
|
+
FlagsClient,
|
|
271
|
+
version
|
|
272
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
// src/server/index.ts
|
|
2
|
+
var version = "1.0.0";
|
|
3
|
+
var C1 = 3432918353;
|
|
4
|
+
var C2 = 461845907;
|
|
5
|
+
function murmur3(key) {
|
|
6
|
+
const bytes = new TextEncoder().encode(key);
|
|
7
|
+
const len = bytes.length;
|
|
8
|
+
const nblocks = len >>> 2;
|
|
9
|
+
let h1 = 0;
|
|
10
|
+
for (let i = 0; i < nblocks; i++) {
|
|
11
|
+
const off = i * 4;
|
|
12
|
+
let k12 = bytes[off] | bytes[off + 1] << 8 | bytes[off + 2] << 16 | bytes[off + 3] << 24;
|
|
13
|
+
k12 = Math.imul(k12, C1);
|
|
14
|
+
k12 = k12 << 15 | k12 >>> 17;
|
|
15
|
+
k12 = Math.imul(k12, C2);
|
|
16
|
+
h1 ^= k12;
|
|
17
|
+
h1 = h1 << 13 | h1 >>> 19;
|
|
18
|
+
h1 = Math.imul(h1, 5) + 3864292196;
|
|
19
|
+
h1 |= 0;
|
|
20
|
+
}
|
|
21
|
+
let k1 = 0;
|
|
22
|
+
const tail = nblocks * 4;
|
|
23
|
+
switch (len & 3) {
|
|
24
|
+
case 3:
|
|
25
|
+
k1 ^= bytes[tail + 2] << 16;
|
|
26
|
+
// fallthrough
|
|
27
|
+
case 2:
|
|
28
|
+
k1 ^= bytes[tail + 1] << 8;
|
|
29
|
+
// fallthrough
|
|
30
|
+
case 1:
|
|
31
|
+
k1 ^= bytes[tail];
|
|
32
|
+
k1 = Math.imul(k1, C1);
|
|
33
|
+
k1 = k1 << 15 | k1 >>> 17;
|
|
34
|
+
k1 = Math.imul(k1, C2);
|
|
35
|
+
h1 ^= k1;
|
|
36
|
+
}
|
|
37
|
+
h1 ^= len;
|
|
38
|
+
h1 ^= h1 >>> 16;
|
|
39
|
+
h1 = Math.imul(h1, 2246822507);
|
|
40
|
+
h1 ^= h1 >>> 13;
|
|
41
|
+
h1 = Math.imul(h1, 3266489909);
|
|
42
|
+
h1 ^= h1 >>> 16;
|
|
43
|
+
return h1 >>> 0;
|
|
44
|
+
}
|
|
45
|
+
function isEnabled(v) {
|
|
46
|
+
return v === 1 || v === true;
|
|
47
|
+
}
|
|
48
|
+
function toNum(v) {
|
|
49
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
50
|
+
if (typeof v === "string" && v !== "" && Number.isFinite(Number(v))) return Number(v);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function matchRule(rule, user) {
|
|
54
|
+
const actual = user[rule.attr];
|
|
55
|
+
switch (rule.op) {
|
|
56
|
+
case "eq":
|
|
57
|
+
return actual === rule.value;
|
|
58
|
+
case "neq":
|
|
59
|
+
return actual !== rule.value;
|
|
60
|
+
case "in":
|
|
61
|
+
return Array.isArray(rule.value) && rule.value.includes(actual);
|
|
62
|
+
case "not_in":
|
|
63
|
+
return Array.isArray(rule.value) && !rule.value.includes(actual);
|
|
64
|
+
case "gt":
|
|
65
|
+
case "gte":
|
|
66
|
+
case "lt":
|
|
67
|
+
case "lte": {
|
|
68
|
+
const a = toNum(actual);
|
|
69
|
+
const b = toNum(rule.value);
|
|
70
|
+
if (a === null || b === null) return false;
|
|
71
|
+
if (rule.op === "gt") return a > b;
|
|
72
|
+
if (rule.op === "gte") return a >= b;
|
|
73
|
+
if (rule.op === "lt") return a < b;
|
|
74
|
+
return a <= b;
|
|
75
|
+
}
|
|
76
|
+
case "contains":
|
|
77
|
+
if (typeof actual === "string" && typeof rule.value === "string")
|
|
78
|
+
return actual.includes(rule.value);
|
|
79
|
+
if (Array.isArray(actual)) return actual.includes(rule.value);
|
|
80
|
+
return false;
|
|
81
|
+
case "regex":
|
|
82
|
+
if (typeof actual !== "string" || typeof rule.value !== "string") return false;
|
|
83
|
+
try {
|
|
84
|
+
return new RegExp(rule.value).test(actual);
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
default:
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function evalGateInternal(gate, user) {
|
|
93
|
+
if (isEnabled(gate.killswitch)) return false;
|
|
94
|
+
if (!isEnabled(gate.enabled)) return false;
|
|
95
|
+
for (const rule of gate.rules ?? []) {
|
|
96
|
+
if (!matchRule(rule, user)) return false;
|
|
97
|
+
}
|
|
98
|
+
const uid = user.user_id ?? user.anonymous_id;
|
|
99
|
+
if (!uid) return false;
|
|
100
|
+
return murmur3(`${gate.salt}:${uid}`) % 1e4 < gate.rolloutPct;
|
|
101
|
+
}
|
|
102
|
+
var FlagsClient = class {
|
|
103
|
+
apiKey;
|
|
104
|
+
baseUrl;
|
|
105
|
+
flagsBlob = null;
|
|
106
|
+
expsBlob = null;
|
|
107
|
+
flagsEtag = null;
|
|
108
|
+
expsEtag = null;
|
|
109
|
+
pollInterval = 30;
|
|
110
|
+
timer = null;
|
|
111
|
+
initialized = false;
|
|
112
|
+
constructor(opts) {
|
|
113
|
+
this.apiKey = opts.apiKey;
|
|
114
|
+
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
115
|
+
}
|
|
116
|
+
async init() {
|
|
117
|
+
await this.fetchAll();
|
|
118
|
+
this.initialized = true;
|
|
119
|
+
this.startPoll();
|
|
120
|
+
}
|
|
121
|
+
async initOnce() {
|
|
122
|
+
if (this.initialized) return;
|
|
123
|
+
await this.fetchAll();
|
|
124
|
+
this.initialized = true;
|
|
125
|
+
}
|
|
126
|
+
destroy() {
|
|
127
|
+
if (this.timer !== null) {
|
|
128
|
+
clearInterval(this.timer);
|
|
129
|
+
this.timer = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
startPoll() {
|
|
133
|
+
this.timer = setInterval(() => {
|
|
134
|
+
this.fetchAll().catch(
|
|
135
|
+
(err) => console.warn("[shipeasy] background poll failed:", String(err))
|
|
136
|
+
);
|
|
137
|
+
}, this.pollInterval * 1e3);
|
|
138
|
+
}
|
|
139
|
+
async fetchAll() {
|
|
140
|
+
const [interval] = await Promise.all([this.fetchFlags(), this.fetchExps()]);
|
|
141
|
+
if (interval !== null && interval !== this.pollInterval) {
|
|
142
|
+
this.pollInterval = interval;
|
|
143
|
+
if (this.timer !== null) {
|
|
144
|
+
clearInterval(this.timer);
|
|
145
|
+
this.startPoll();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async fetchFlags() {
|
|
150
|
+
const headers = { "X-SDK-Key": this.apiKey };
|
|
151
|
+
if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
|
|
152
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags`, { headers });
|
|
153
|
+
const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
|
|
154
|
+
if (res.status === 304) return interval;
|
|
155
|
+
if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
|
|
156
|
+
const etag = res.headers.get("ETag");
|
|
157
|
+
if (etag) this.flagsEtag = etag;
|
|
158
|
+
this.flagsBlob = await res.json();
|
|
159
|
+
return interval;
|
|
160
|
+
}
|
|
161
|
+
async fetchExps() {
|
|
162
|
+
const headers = { "X-SDK-Key": this.apiKey };
|
|
163
|
+
if (this.expsEtag) headers["If-None-Match"] = this.expsEtag;
|
|
164
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/experiments`, { headers });
|
|
165
|
+
if (res.status === 304) return;
|
|
166
|
+
if (!res.ok) throw new Error(`/sdk/experiments returned ${res.status}`);
|
|
167
|
+
const etag = res.headers.get("ETag");
|
|
168
|
+
if (etag) this.expsEtag = etag;
|
|
169
|
+
this.expsBlob = await res.json();
|
|
170
|
+
}
|
|
171
|
+
getFlag(name, user) {
|
|
172
|
+
const gate = this.flagsBlob?.gates[name];
|
|
173
|
+
if (!gate) return false;
|
|
174
|
+
return evalGateInternal(gate, user);
|
|
175
|
+
}
|
|
176
|
+
getConfig(name, decode) {
|
|
177
|
+
const entry = this.flagsBlob?.configs[name];
|
|
178
|
+
if (!entry) return void 0;
|
|
179
|
+
if (!decode) return entry.value;
|
|
180
|
+
return decode(entry.value);
|
|
181
|
+
}
|
|
182
|
+
getExperiment(name, user, defaultParams, decode) {
|
|
183
|
+
const notIn = {
|
|
184
|
+
inExperiment: false,
|
|
185
|
+
group: "control",
|
|
186
|
+
params: defaultParams
|
|
187
|
+
};
|
|
188
|
+
if (!this.flagsBlob || !this.expsBlob) return notIn;
|
|
189
|
+
const exp = this.expsBlob.experiments[name];
|
|
190
|
+
if (!exp || exp.status !== "running") return notIn;
|
|
191
|
+
if (exp.targetingGate) {
|
|
192
|
+
const gate = this.flagsBlob.gates[exp.targetingGate];
|
|
193
|
+
if (!gate || !evalGateInternal(gate, user)) return notIn;
|
|
194
|
+
}
|
|
195
|
+
const uid = user.user_id ?? user.anonymous_id;
|
|
196
|
+
if (!uid) return notIn;
|
|
197
|
+
const universe = this.expsBlob.universes[exp.universe];
|
|
198
|
+
const holdoutRange = universe?.holdout_range ?? null;
|
|
199
|
+
if (holdoutRange) {
|
|
200
|
+
const seg = murmur3(`${exp.universe}:${uid}`) % 1e4;
|
|
201
|
+
const [lo, hi] = holdoutRange;
|
|
202
|
+
if (seg >= lo && seg <= hi) return notIn;
|
|
203
|
+
}
|
|
204
|
+
if (murmur3(`${exp.salt}:alloc:${uid}`) % 1e4 >= exp.allocationPct) return notIn;
|
|
205
|
+
const groupHash = murmur3(`${exp.salt}:group:${uid}`) % 1e4;
|
|
206
|
+
let cumulative = 0;
|
|
207
|
+
for (let i = 0; i < exp.groups.length; i++) {
|
|
208
|
+
const g = exp.groups[i];
|
|
209
|
+
cumulative += g.weight;
|
|
210
|
+
if (groupHash < cumulative || i === exp.groups.length - 1) {
|
|
211
|
+
if (!decode) {
|
|
212
|
+
return { inExperiment: true, group: g.name, params: g.params };
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
return { inExperiment: true, group: g.name, params: decode(g.params) };
|
|
216
|
+
} catch (err) {
|
|
217
|
+
console.warn(`[shipeasy] getExperiment('${name}') decode failed:`, String(err));
|
|
218
|
+
return notIn;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return notIn;
|
|
223
|
+
}
|
|
224
|
+
track(userId, eventName, props) {
|
|
225
|
+
const body = JSON.stringify({
|
|
226
|
+
events: [
|
|
227
|
+
{
|
|
228
|
+
type: "metric",
|
|
229
|
+
event_name: eventName,
|
|
230
|
+
user_id: userId,
|
|
231
|
+
ts: Date.now(),
|
|
232
|
+
...props !== void 0 ? { properties: props } : {}
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
});
|
|
236
|
+
globalThis.fetch(`${this.baseUrl}/collect`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: { "X-SDK-Key": this.apiKey, "Content-Type": "text/plain" },
|
|
239
|
+
body
|
|
240
|
+
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
export {
|
|
244
|
+
FlagsClient,
|
|
245
|
+
version
|
|
246
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shipeasy/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Feature flag and experimentation SDK",
|
|
5
|
+
"main": "./dist/server/index.js",
|
|
6
|
+
"module": "./dist/server/index.mjs",
|
|
7
|
+
"browser": "./dist/client/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"node": "./dist/server/index.js",
|
|
11
|
+
"browser": "./dist/client/index.js",
|
|
12
|
+
"default": "./dist/server/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./server": {
|
|
15
|
+
"types": "./dist/server/index.d.ts",
|
|
16
|
+
"import": "./dist/server/index.mjs",
|
|
17
|
+
"default": "./dist/server/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./client": {
|
|
20
|
+
"types": "./dist/client/index.d.ts",
|
|
21
|
+
"default": "./dist/client/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./templates/*": "./templates/*.js"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/",
|
|
27
|
+
"templates/"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"type-check": "tsc --noEmit",
|
|
32
|
+
"test": "vitest"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"murmurhash-js": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/murmurhash-js": "^1.0.6",
|
|
39
|
+
"tsup": "^8.3.0",
|
|
40
|
+
"typescript": "^5.7.4",
|
|
41
|
+
"vitest": "^2.1.0"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"zod": "^4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"peerDependenciesMeta": {
|
|
47
|
+
"zod": {
|
|
48
|
+
"optional": true
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
54
|
+
}
|