@voltro/plugin-flags 0.33.0 → 0.34.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/CHANGELOG.md +1801 -0
- package/dist/define-D9Eet7wA.js +79 -0
- package/dist/index.d.ts +552 -1
- package/dist/index.js +441 -120
- package/dist/web.d.ts +160 -0
- package/dist/web.js +8 -7
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import { evaluateDescriptor as e, evaluateVariantsDescriptor as t, flagsRpcClientImports as n } from "./rpc.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { i as r, n as i, r as a, t as o } from "./define-D9Eet7wA.js";
|
|
3
|
+
import { FlagDisabled as s } from "./errors.js";
|
|
4
|
+
import { Effect as c } from "effect";
|
|
5
|
+
import { definePlugin as l, pluginInstanceName as u } from "@voltro/protocol";
|
|
6
|
+
import { boolean as d, id as f, integer as p, json as m, queryFor as h, registerRetention as g, retentionTtlMsFromEnv as _, table as v, text as y, timestamp as b } from "@voltro/database";
|
|
6
7
|
//#region src/evaluate.ts
|
|
7
|
-
var
|
|
8
|
+
var x = (e) => typeof e == "boolean" ? { enabled: e } : e, S = (e, t) => {
|
|
8
9
|
let n = 2166136261, r = `${e}:${t}`;
|
|
9
10
|
for (let e = 0; e < r.length; e++) n ^= r.charCodeAt(e), n = Math.imul(n, 16777619);
|
|
10
11
|
return n >>> 0;
|
|
11
|
-
},
|
|
12
|
+
}, C = (e, t) => S(e, t) % 100, ee = (e, t) => S(`${e}:variant`, t) % 1e4, w = (e, t) => {
|
|
12
13
|
if (e.subjectIds && !(t.id != null && e.subjectIds.includes(t.id)) || e.tenantIds && !(t.tenantId != null && e.tenantIds.includes(t.tenantId)) || e.subjectTypes && !(t.type != null && e.subjectTypes.includes(t.type))) return !1;
|
|
13
14
|
if (e.metadata) {
|
|
14
15
|
let n = t.metadata ?? {};
|
|
15
16
|
for (let [t, r] of Object.entries(e.metadata)) if (String(n[t]) !== r) return !1;
|
|
16
17
|
}
|
|
17
18
|
return !0;
|
|
18
|
-
},
|
|
19
|
-
if (e.activateAt != null && t <
|
|
19
|
+
}, T = (e) => typeof e == "number" ? e : Date.parse(e), E = (e, t) => (e.rolloutBy === "tenant" ? t.tenantId : t.id) ?? "anon", D = (e, t) => {
|
|
20
|
+
if (e.activateAt != null && t < T(e.activateAt) || e.deactivateAt != null && t >= T(e.deactivateAt)) return { active: !1 };
|
|
20
21
|
if (e.ramp) {
|
|
21
|
-
let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o =
|
|
22
|
+
let { from: n, to: r, startAt: i, endAt: a } = e.ramp, o = T(i), s = T(a);
|
|
22
23
|
if (t <= o) return {
|
|
23
24
|
active: !0,
|
|
24
25
|
rollout: n
|
|
@@ -34,71 +35,71 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
return { active: !0 };
|
|
37
|
-
},
|
|
38
|
-
let i =
|
|
38
|
+
}, O = (e, t, n, r = Date.now()) => {
|
|
39
|
+
let i = x(t);
|
|
39
40
|
if (i.enabled === !1) return !1;
|
|
40
41
|
let a;
|
|
41
42
|
if (i.schedule) {
|
|
42
|
-
let e =
|
|
43
|
+
let e = D(i.schedule, r);
|
|
43
44
|
if (!e.active) return !1;
|
|
44
45
|
a = e.rollout;
|
|
45
46
|
}
|
|
46
|
-
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) =>
|
|
47
|
+
if (i.targeting && i.targeting.length > 0 && !i.targeting.some((e) => w(e, n))) return !1;
|
|
47
48
|
let o = a ?? i.rollout ?? 100;
|
|
48
49
|
if (o < 100 && o > 0) {
|
|
49
|
-
if (!(
|
|
50
|
+
if (!(C(e, E(i, n)) < o)) return !1;
|
|
50
51
|
} else if (o <= 0) return !1;
|
|
51
|
-
return i.variants && i.variants.length > 0 && i.offVariant != null ?
|
|
52
|
-
},
|
|
52
|
+
return i.variants && i.variants.length > 0 && i.offVariant != null ? k(e, i, n).name !== i.offVariant : !0;
|
|
53
|
+
}, te = (e) => {
|
|
53
54
|
let t = e.reduce((e, t) => e + (t.weight ?? 1), 0) || e.length, n = 0;
|
|
54
55
|
return e.map((r, i) => (n += r.weight ?? 1, i === e.length - 1 ? 1e4 : Math.round(n / t * 1e4)));
|
|
55
|
-
},
|
|
56
|
+
}, k = (e, t, n) => {
|
|
56
57
|
let r = t.variants ?? [];
|
|
57
58
|
if (r.length === 0) throw Error(`flag "${e}" has no variants`);
|
|
58
|
-
let i =
|
|
59
|
+
let i = ee(e, E(t, n)), a = te(r);
|
|
59
60
|
for (let e = 0; e < r.length; e++) if (i < a[e]) return r[e];
|
|
60
61
|
return r[r.length - 1];
|
|
61
|
-
},
|
|
62
|
-
let i =
|
|
62
|
+
}, A = (e, t, n, r = Date.now()) => {
|
|
63
|
+
let i = x(t), a = O(e, t, n, r);
|
|
63
64
|
if (!i.variants || i.variants.length === 0) return {
|
|
64
65
|
name: a ? "on" : "off",
|
|
65
66
|
value: a,
|
|
66
67
|
enabled: a
|
|
67
68
|
};
|
|
68
|
-
let o =
|
|
69
|
+
let o = k(e, i, n);
|
|
69
70
|
return {
|
|
70
71
|
name: o.name,
|
|
71
72
|
value: o.value,
|
|
72
73
|
enabled: a
|
|
73
74
|
};
|
|
74
|
-
},
|
|
75
|
+
}, ne = (e, t, n = Date.now()) => {
|
|
75
76
|
let r = {};
|
|
76
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
77
|
+
for (let [i, a] of Object.entries(e)) r[i] = O(i, a, t, n);
|
|
77
78
|
return r;
|
|
78
|
-
},
|
|
79
|
+
}, re = (e, t, n = Date.now()) => {
|
|
79
80
|
let r = {};
|
|
80
|
-
for (let [i, a] of Object.entries(e)) r[i] =
|
|
81
|
+
for (let [i, a] of Object.entries(e)) r[i] = A(i, a, t, n);
|
|
81
82
|
return r;
|
|
82
|
-
},
|
|
83
|
-
id:
|
|
84
|
-
key:
|
|
85
|
-
enabled:
|
|
86
|
-
rollout:
|
|
87
|
-
rolloutBy:
|
|
88
|
-
targeting:
|
|
89
|
-
variants:
|
|
90
|
-
offVariant:
|
|
91
|
-
schedule:
|
|
92
|
-
description:
|
|
93
|
-
updatedAt:
|
|
94
|
-
}),
|
|
95
|
-
id:
|
|
96
|
-
flag:
|
|
97
|
-
enabled:
|
|
98
|
-
previousEnabled:
|
|
99
|
-
actorId:
|
|
100
|
-
at:
|
|
101
|
-
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]),
|
|
83
|
+
}, j = "_voltro_feature_flag_audit", M = "_voltro_feature_flags", N = v(M, {
|
|
84
|
+
id: f({ prefix: "flag" }),
|
|
85
|
+
key: y().unique(),
|
|
86
|
+
enabled: d().default(!0),
|
|
87
|
+
rollout: p().nullable(),
|
|
88
|
+
rolloutBy: y().nullable(),
|
|
89
|
+
targeting: m().nullable(),
|
|
90
|
+
variants: m().nullable(),
|
|
91
|
+
offVariant: y().nullable(),
|
|
92
|
+
schedule: m().nullable(),
|
|
93
|
+
description: y().nullable(),
|
|
94
|
+
updatedAt: b().default("now").onUpdate("now")
|
|
95
|
+
}), P = v(j, {
|
|
96
|
+
id: f({ prefix: "flagaud" }),
|
|
97
|
+
flag: y(),
|
|
98
|
+
enabled: d(),
|
|
99
|
+
previousEnabled: d().nullable(),
|
|
100
|
+
actorId: y().nullable(),
|
|
101
|
+
at: b().default("now")
|
|
102
|
+
}).index("byFlagAuditFlag", ["flag"]).index("byFlagAuditAt", ["at"]), F = (e, t) => {
|
|
102
103
|
let n = { ...e };
|
|
103
104
|
for (let e of t) {
|
|
104
105
|
let t = {
|
|
@@ -114,7 +115,7 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
114
115
|
n[e.key] = t;
|
|
115
116
|
}
|
|
116
117
|
return n;
|
|
117
|
-
},
|
|
118
|
+
}, I = new class {
|
|
118
119
|
flags = {};
|
|
119
120
|
set(e) {
|
|
120
121
|
this.flags = e;
|
|
@@ -125,19 +126,19 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
125
126
|
get(e) {
|
|
126
127
|
return this.flags[e];
|
|
127
128
|
}
|
|
128
|
-
}(),
|
|
129
|
-
let n =
|
|
129
|
+
}(), L = (e, t) => R(e, t) !== null, R = (e, t) => {
|
|
130
|
+
let n = I.get(e);
|
|
130
131
|
if (n === void 0) return null;
|
|
131
|
-
let r =
|
|
132
|
-
return
|
|
133
|
-
...
|
|
132
|
+
let r = x(n), i = r.enabled !== !1;
|
|
133
|
+
return I.set({
|
|
134
|
+
...I.all(),
|
|
134
135
|
[e]: {
|
|
135
136
|
...r,
|
|
136
137
|
enabled: t
|
|
137
138
|
}
|
|
138
139
|
}), i;
|
|
139
|
-
},
|
|
140
|
-
let n =
|
|
140
|
+
}, z = (e) => Object.entries(e).map(([e, t]) => {
|
|
141
|
+
let n = x(t);
|
|
141
142
|
return {
|
|
142
143
|
key: e,
|
|
143
144
|
enabled: n.enabled !== !1,
|
|
@@ -146,8 +147,8 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
146
147
|
scheduled: n.schedule != null,
|
|
147
148
|
description: n.description
|
|
148
149
|
};
|
|
149
|
-
}),
|
|
150
|
-
let n =
|
|
150
|
+
}), ie = (e, t) => {
|
|
151
|
+
let n = x(t);
|
|
151
152
|
return {
|
|
152
153
|
key: e,
|
|
153
154
|
enabled: n.enabled !== !1,
|
|
@@ -159,8 +160,8 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
159
160
|
schedule: n.schedule ?? null,
|
|
160
161
|
description: n.description ?? null
|
|
161
162
|
};
|
|
162
|
-
},
|
|
163
|
-
loadOverrides: async () => (await e.query(
|
|
163
|
+
}, ae = (e) => ({
|
|
164
|
+
loadOverrides: async () => (await e.query(h(N).descriptor)).map((e) => ({
|
|
164
165
|
key: String(e.key),
|
|
165
166
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
166
167
|
rollout: e.rollout == null ? null : Number(e.rollout),
|
|
@@ -185,17 +186,17 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
185
186
|
description: t.description,
|
|
186
187
|
updatedAt: /* @__PURE__ */ new Date()
|
|
187
188
|
};
|
|
188
|
-
await e.upsert(
|
|
189
|
+
await e.upsert(M, n, { conflictColumns: ["key"] });
|
|
189
190
|
}
|
|
190
|
-
}),
|
|
191
|
+
}), oe = (e) => ({
|
|
191
192
|
flag: String(e.flag),
|
|
192
193
|
enabled: e.enabled === !0 || e.enabled === 1,
|
|
193
194
|
previousEnabled: e.previousEnabled == null ? null : e.previousEnabled === !0 || e.previousEnabled === 1,
|
|
194
195
|
actorId: e.actorId ?? null,
|
|
195
196
|
at: e.at instanceof Date ? e.at : new Date(String(e.at))
|
|
196
|
-
}),
|
|
197
|
+
}), se = (e) => ({
|
|
197
198
|
record: async (t) => {
|
|
198
|
-
await e.insert(
|
|
199
|
+
await e.insert(j, {
|
|
199
200
|
id: `flagaud_${t.at.getTime()}_${Math.random().toString(36).slice(2, 10)}`,
|
|
200
201
|
flag: t.flag,
|
|
201
202
|
enabled: t.enabled,
|
|
@@ -205,69 +206,369 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
205
206
|
});
|
|
206
207
|
},
|
|
207
208
|
list: async (t) => {
|
|
208
|
-
let n =
|
|
209
|
+
let n = h(P).orderBy("at", "desc");
|
|
209
210
|
t?.limit != null && (n = n.limit(t.limit));
|
|
210
|
-
let r = (await e.query(n.descriptor)).map(
|
|
211
|
+
let r = (await e.query(n.descriptor)).map(oe);
|
|
211
212
|
return t?.flag == null ? r : r.filter((e) => e.flag === t.flag);
|
|
212
213
|
}
|
|
213
|
-
}),
|
|
214
|
-
let n =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
214
|
+
}), B = (e, t) => {
|
|
215
|
+
let n = new Map(t.map((e) => [e.name, e])), r = [];
|
|
216
|
+
for (let t of e) {
|
|
217
|
+
let e = t.experiment;
|
|
218
|
+
if (e === void 0) continue;
|
|
219
|
+
let i = n.get(e);
|
|
220
|
+
if (i === void 0) {
|
|
221
|
+
r.push({
|
|
222
|
+
flag: t.key,
|
|
223
|
+
experiment: e,
|
|
224
|
+
problem: `no experiment named ${JSON.stringify(e)} was passed to flagsPlugin({ experiments }). Pass the defineExperiment() definitions the app declares, or drop the \`experiment\` link.`
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
(i.variantFrom === void 0 || i.variantFrom === null) && r.push({
|
|
229
|
+
flag: t.key,
|
|
230
|
+
experiment: e,
|
|
231
|
+
problem: `the experiment assigns its own arms (\`subject\` mode), so it would measure a split this flag never served — the two bucketings are independent hashes. Persist the served arm (\`flagVariant(ctx, ${JSON.stringify(t.key)})\`) on the watched table and declare the experiment with \`variantFrom: <that column>\`.`
|
|
232
|
+
}), typeof i.holdout == "number" && i.holdout > 0 && r.push({
|
|
233
|
+
flag: t.key,
|
|
234
|
+
experiment: e,
|
|
235
|
+
problem: `holdout ${i.holdout} is carved at ASSIGNMENT, and this flag does the assigning — the experiment can never place a row there. Declare the holdout as a flag variant instead.`
|
|
236
|
+
});
|
|
237
|
+
let a = new Set(t.variants.map((e) => e.name)), o = new Set(i.variants.map((e) => e.name));
|
|
238
|
+
if (a.size === 0) r.push({
|
|
239
|
+
flag: t.key,
|
|
240
|
+
experiment: e,
|
|
241
|
+
problem: "the flag declares no variants, so it serves one value to everyone and there is nothing to compare."
|
|
242
|
+
});
|
|
243
|
+
else {
|
|
244
|
+
let n = [...a].filter((e) => !o.has(e)), i = [...o].filter((e) => !a.has(e));
|
|
245
|
+
(n.length > 0 || i.length > 0) && r.push({
|
|
246
|
+
flag: t.key,
|
|
247
|
+
experiment: e,
|
|
248
|
+
problem: `the arms disagree — ${n.length > 0 ? `served by the flag but not declared by the experiment: ${n.join(", ")}` : ""}${n.length > 0 && i.length > 0 ? "; " : ""}${i.length > 0 ? `declared by the experiment but never served: ${i.join(", ")}` : ""}. A row assigned to an arm the experiment does not declare is EXCLUDED, so the mismatch shows up as an empty arm rather than an error.`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return r;
|
|
253
|
+
}, V = (e, t) => {
|
|
254
|
+
let n = B(e, t);
|
|
255
|
+
if (n.length === 0) return;
|
|
256
|
+
let r = n.map((e) => ` · flag "${e.flag}" → experiment "${e.experiment}": ${e.problem}`);
|
|
257
|
+
throw Error(`@voltro/plugin-flags: ${n.length} flag→experiment link problem(s):\n${r.join("\n")}`);
|
|
258
|
+
}, H = 864e5, U = (e) => typeof e == "number" ? e : Date.parse(e), W = (e, t) => {
|
|
259
|
+
let n = x(e);
|
|
260
|
+
if (n.enabled === !1) return {
|
|
261
|
+
shape: "constantOff",
|
|
262
|
+
reason: "enabled: false — resolves false for every caller"
|
|
263
|
+
};
|
|
264
|
+
if (n.schedule) {
|
|
265
|
+
let { activateAt: e, deactivateAt: r } = n.schedule;
|
|
266
|
+
if (r != null && t >= U(r)) return {
|
|
267
|
+
shape: "expired",
|
|
268
|
+
reason: `schedule.deactivateAt (${String(r)}) has passed — the flag can never be on again`
|
|
269
|
+
};
|
|
270
|
+
if (e != null && t < U(e)) return {
|
|
271
|
+
shape: "notYetActive",
|
|
272
|
+
reason: `schedule.activateAt (${String(e)}) is in the future — pending, not dead`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
let r = n.rollout ?? 100;
|
|
276
|
+
if (r <= 0) return {
|
|
277
|
+
shape: "constantOff",
|
|
278
|
+
reason: `rollout: ${r} — nobody is in the rollout`
|
|
279
|
+
};
|
|
280
|
+
let i = (n.targeting?.length ?? 0) > 0, a = (n.variants?.length ?? 0) > 1, o = n.schedule !== void 0 && (n.schedule.ramp !== void 0 || n.schedule.deactivateAt != null && t < U(n.schedule.deactivateAt));
|
|
281
|
+
return r >= 100 && !i && !a && !o ? {
|
|
282
|
+
shape: "constantOn",
|
|
283
|
+
reason: "rollout 100 with no targeting, no variants and no live schedule — resolves true for every caller"
|
|
284
|
+
} : {
|
|
285
|
+
shape: "conditional",
|
|
286
|
+
reason: `still selects between callers (${[
|
|
287
|
+
r < 100 ? `rollout ${r}%` : void 0,
|
|
288
|
+
i ? `${n.targeting?.length} targeting rule(s)` : void 0,
|
|
289
|
+
a ? `${n.variants?.length} variants` : void 0,
|
|
290
|
+
o ? "a live schedule" : void 0
|
|
291
|
+
].filter((e) => e !== void 0).join(", ")})`
|
|
292
|
+
};
|
|
293
|
+
}, G = [
|
|
294
|
+
"Reachability is not decided. \"Not evaluated since <date>\" is a measurement; \"this code path is dead\" is not decidable in general. A seasonal flag, a flag behind a route nobody visited this month, and a flag whose last call site was deleted are indistinguishable from here.",
|
|
295
|
+
"Only SERVER-side reads are counted as use: isFlagEnabled / requireFlag / flagValue and a gatedBy interception. A flag consulted only in the browser via useFlag() / useFlagValue() reads from the bulk set the server already sent, so it never reaches this report as a named read.",
|
|
296
|
+
"A \"bulk\" observation means the flag was INCLUDED in a flags.evaluate / flags.variants response. It does not mean any client read that key — one useFlags() poll evaluates the whole registry.",
|
|
297
|
+
"The window is bounded by retention (see coverage.retentionDays). Nothing older is evidence: a flag last read BEFORE the window has no observation at all and reads \"neverObserved\", which is exactly what a flag declared this morning reads.",
|
|
298
|
+
"\"neverObserved\" is not a removal candidate. A flag declared yesterday and a flag abandoned last year produce the identical row; only a flag that WAS observed and then stopped (\"stale\") is proven unused."
|
|
299
|
+
], K = (e) => {
|
|
300
|
+
let t = e.now ?? Date.now(), n = e.staleAfterDays, r = t - n * H, i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o;
|
|
301
|
+
for (let t of e.usage) {
|
|
302
|
+
let e = t.lastAt.getTime();
|
|
303
|
+
if (!Number.isFinite(e)) continue;
|
|
304
|
+
(o === void 0 || e < o) && (o = e);
|
|
305
|
+
let n = t.source === "bulk" ? a : i, r = n.get(t.flag);
|
|
306
|
+
(r === void 0 || e > r) && n.set(t.flag, e);
|
|
307
|
+
}
|
|
308
|
+
let s = o === void 0 ? 0 : Math.floor((t - o) / H);
|
|
309
|
+
return {
|
|
310
|
+
coverage: {
|
|
311
|
+
tracking: e.tracking,
|
|
312
|
+
observedSince: o === void 0 ? null : new Date(o).toISOString().slice(0, 10),
|
|
313
|
+
observedDays: s,
|
|
314
|
+
retentionDays: e.retentionDays,
|
|
315
|
+
staleAfterDays: n
|
|
316
|
+
},
|
|
317
|
+
findings: Object.entries(e.flags).map(([o, c]) => {
|
|
318
|
+
let { shape: l, reason: u } = W(c, t), d = i.get(o), f = a.get(o), p = [u], m;
|
|
319
|
+
e.tracking ? d !== void 0 && d >= r ? (m = "evaluated", p.push(`last server-side read ${new Date(d).toISOString()}`)) : d === void 0 ? (m = "neverObserved", p.push(s > 0 ? `no server-side read in the ${s}-day observation window` : "no server-side read observed yet (the observation window is empty)"), f !== void 0 && p.push("the flag WAS delivered to a client in a bulk flags.evaluate response — which does not prove it was read")) : (m = "stale", p.push(`last server-side read ${new Date(d).toISOString()}, more than ${n} day(s) ago, over an observation window of ${s} day(s)`)) : (m = "untracked", p.push("evaluation tracking is off — no usage evidence exists for this deployment"));
|
|
320
|
+
let h = l === "constantOn" || l === "constantOff" || l === "expired";
|
|
321
|
+
return h && p.push("the definition alone makes this flag a constant — it selects nothing"), {
|
|
322
|
+
key: o,
|
|
323
|
+
shape: l,
|
|
324
|
+
shapeReason: u,
|
|
325
|
+
usage: m,
|
|
326
|
+
lastTargetedAt: d === void 0 ? null : new Date(d).toISOString(),
|
|
327
|
+
lastBulkAt: f === void 0 ? null : new Date(f).toISOString(),
|
|
328
|
+
liveTargetedEvaluations: e.liveCounts?.get(o) ?? 0,
|
|
329
|
+
removalCandidate: h || m === "stale",
|
|
330
|
+
why: p
|
|
331
|
+
};
|
|
332
|
+
}),
|
|
333
|
+
limits: G
|
|
334
|
+
};
|
|
335
|
+
}, q = "_voltro_feature_flag_usage", J = v(q, {
|
|
336
|
+
id: f({ prefix: "flagusg" }),
|
|
337
|
+
flag: y(),
|
|
338
|
+
day: y(),
|
|
339
|
+
source: y(),
|
|
340
|
+
lastAt: b()
|
|
341
|
+
}).unique([
|
|
342
|
+
"flag",
|
|
343
|
+
"day",
|
|
344
|
+
"source"
|
|
345
|
+
]).index("byFlagUsageLastAt", ["lastAt"]).index("byFlagUsageFlag", ["flag"]), ce = (e) => e.toISOString().slice(0, 10), le = (e) => {
|
|
346
|
+
let t = 2166136261;
|
|
347
|
+
for (let n = 0; n < e.length; n++) t ^= e.charCodeAt(n), t = Math.imul(t, 16777619);
|
|
348
|
+
return (t >>> 0).toString(16).padStart(8, "0");
|
|
349
|
+
}, ue = (e, t, n) => `flagusg_${t.replace(/-/g, "")}_${n === "targeted" ? "t" : "b"}_${le(e)}`, Y = new class {
|
|
350
|
+
on = !1;
|
|
351
|
+
pending = /* @__PURE__ */ new Map();
|
|
352
|
+
liveCounts = /* @__PURE__ */ new Map();
|
|
353
|
+
enable() {
|
|
354
|
+
this.on = !0;
|
|
355
|
+
}
|
|
356
|
+
disable() {
|
|
357
|
+
this.on = !1, this.pending.clear();
|
|
358
|
+
}
|
|
359
|
+
get enabled() {
|
|
360
|
+
return this.on;
|
|
361
|
+
}
|
|
362
|
+
record(e, t, n = Date.now()) {
|
|
363
|
+
if (!this.on) return;
|
|
364
|
+
let r = `${t}\u0000${e}`, i = this.pending.get(r);
|
|
365
|
+
i ? i.lastAt = n : this.pending.set(r, {
|
|
366
|
+
flag: e,
|
|
367
|
+
source: t,
|
|
368
|
+
lastAt: n
|
|
369
|
+
}), t === "targeted" && this.liveCounts.set(e, (this.liveCounts.get(e) ?? 0) + 1);
|
|
370
|
+
}
|
|
371
|
+
drain() {
|
|
372
|
+
let e = [...this.pending.values()];
|
|
373
|
+
return this.pending.clear(), e;
|
|
374
|
+
}
|
|
375
|
+
restore(e) {
|
|
376
|
+
if (this.on) for (let t of e) {
|
|
377
|
+
let e = `${t.source}\u0000${t.flag}`, n = this.pending.get(e);
|
|
378
|
+
(!n || n.lastAt < t.lastAt) && this.pending.set(e, { ...t });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
liveTargetedCounts() {
|
|
382
|
+
return this.liveCounts;
|
|
383
|
+
}
|
|
384
|
+
reset() {
|
|
385
|
+
this.on = !1, this.pending.clear(), this.liveCounts.clear();
|
|
386
|
+
}
|
|
387
|
+
}(), X = (e, t, n) => Y.record(e, t, n), de = (e) => e === "bulk" ? "bulk" : "targeted", fe = (e) => ({
|
|
388
|
+
upsertMany: async (t) => {
|
|
389
|
+
for (let n of t) {
|
|
390
|
+
let t = ce(n.lastAt);
|
|
391
|
+
await e.upsert(q, {
|
|
392
|
+
id: ue(n.flag, t, n.source),
|
|
393
|
+
flag: n.flag,
|
|
394
|
+
day: t,
|
|
395
|
+
source: n.source,
|
|
396
|
+
lastAt: n.lastAt
|
|
397
|
+
}, { conflictColumns: [
|
|
398
|
+
"flag",
|
|
399
|
+
"day",
|
|
400
|
+
"source"
|
|
401
|
+
] });
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
load: async () => (await e.query(h(J).descriptor)).map((e) => ({
|
|
405
|
+
flag: String(e.flag),
|
|
406
|
+
day: String(e.day),
|
|
407
|
+
source: de(e.source),
|
|
408
|
+
lastAt: e.lastAt instanceof Date ? e.lastAt : new Date(String(e.lastAt))
|
|
409
|
+
}))
|
|
410
|
+
}), pe = 3e5, me = (e) => {
|
|
411
|
+
let t = e.intervalMs ?? 3e5, n = !1, r = async () => {
|
|
412
|
+
let t = Y.drain();
|
|
413
|
+
if (t.length !== 0) try {
|
|
414
|
+
await e.store.upsertMany(t.map((e) => ({
|
|
415
|
+
flag: e.flag,
|
|
416
|
+
source: e.source,
|
|
417
|
+
lastAt: new Date(e.lastAt)
|
|
418
|
+
})));
|
|
419
|
+
} catch (n) {
|
|
420
|
+
Y.restore(t), e.log?.warn?.("flag usage flush failed — observations retained for the next pass", {
|
|
421
|
+
pending: t.length,
|
|
422
|
+
cause: n.message
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
}, i = setInterval(() => {
|
|
426
|
+
r();
|
|
427
|
+
}, t);
|
|
428
|
+
return typeof i.unref == "function" && i.unref(), {
|
|
429
|
+
flushNow: r,
|
|
430
|
+
stop: async () => {
|
|
431
|
+
n || (n = !0, clearInterval(i), await r());
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}, he = (e = process.env, t = 90) => {
|
|
435
|
+
let n = _(e.VOLTRO_FLAG_USAGE_TTL_HOURS, 24 * t);
|
|
436
|
+
return g({
|
|
437
|
+
table: q,
|
|
438
|
+
timeColumn: "lastAt",
|
|
439
|
+
source: "plugin",
|
|
440
|
+
ttlMs: n,
|
|
441
|
+
envVar: "VOLTRO_FLAG_USAGE_TTL_HOURS"
|
|
442
|
+
}), n;
|
|
443
|
+
}, Z = (e) => "request" in e && e.request?.subject || "subject" in e && e.subject || {}, Q = (e, t) => {
|
|
444
|
+
let n = I.get(t);
|
|
445
|
+
return X(t, "targeted"), n !== void 0 && O(t, n, Z(e));
|
|
446
|
+
}, ge = (e, t) => Q(e, t) ? c.void : c.fail(new s({ flag: t })), _e = (e, t) => {
|
|
447
|
+
let n = I.get(t.key);
|
|
448
|
+
return X(t.key, "targeted"), n === void 0 ? t.defaultValue : r(t, A(t.key, n, Z(e)));
|
|
449
|
+
}, ve = (e, t) => {
|
|
450
|
+
let n = I.get(t.key);
|
|
451
|
+
return X(t.key, "targeted"), n === void 0 ? null : A(t.key, n, Z(e)).name;
|
|
452
|
+
}, ye = (e, t) => e === t || e.startsWith("/") && e.endsWith("/") && new RegExp(e.slice(1, -1)).test(t), $ = "@voltro/plugin-flags", be = (r = {}) => {
|
|
453
|
+
let a = r.gatedBy ?? {}, o = u({
|
|
454
|
+
base: $,
|
|
455
|
+
alias: r.alias,
|
|
456
|
+
instance: r.name
|
|
457
|
+
}), d = r.typedFlags ?? [], f = /* @__PURE__ */ new Map();
|
|
458
|
+
for (let e of d) {
|
|
459
|
+
if (f.has(e.key)) throw Error(`${$}: two typedFlags declare the key ${JSON.stringify(e.key)}`);
|
|
460
|
+
if (r.flags?.[e.key] !== void 0) throw Error(`${$}: ${JSON.stringify(e.key)} is declared BOTH in \`flags\` and in \`typedFlags\`. One key, one declaration — the typed one would otherwise be silently overwritten by the untyped baseline.`);
|
|
461
|
+
f.set(e.key, e);
|
|
462
|
+
}
|
|
463
|
+
d.some((e) => e.experiment !== void 0) && V(d, r.experiments ?? []);
|
|
464
|
+
let p = { ...r.flags ?? {} };
|
|
465
|
+
for (let e of d) p[e.key] = e.definition;
|
|
466
|
+
let m = r.store === "postgres", h, g, _, v, y = [], b = r.usage ?? {}, x = b.track ?? m;
|
|
467
|
+
if (x && !m) throw Error(`${$}: usage.track requires store: 'postgres' — there is nowhere to record an evaluation on the memory tier, and a dead-flag report that silently observed nothing is exactly the defect it exists to find.`);
|
|
468
|
+
let S = b.staleAfterDays ?? 30, C = b.retentionDays ?? 90;
|
|
469
|
+
if (S > C) throw Error(`${$}: usage.staleAfterDays (${S}) exceeds usage.retentionDays (${C}), so the observation window can never be long enough to prove a flag stale and every verdict would be withheld.`);
|
|
470
|
+
let ee = (e) => {
|
|
471
|
+
let t = F(p, e), n = [];
|
|
472
|
+
for (let [e, r] of f) {
|
|
473
|
+
let a = t[e];
|
|
474
|
+
if (a === void 0) continue;
|
|
475
|
+
let o = i(r, typeof a == "boolean" ? { enabled: a } : a);
|
|
476
|
+
o.ok || (n.push(o.refusal), t[e] = r.definition);
|
|
477
|
+
}
|
|
478
|
+
y = n, I.set(t);
|
|
479
|
+
}, w = async () => {
|
|
480
|
+
h && ee(await h.loadOverrides());
|
|
481
|
+
}, T = (e) => {
|
|
482
|
+
if (a[e] !== void 0) return a[e];
|
|
483
|
+
for (let [t, n] of Object.entries(a)) if (t !== e && ye(t, e)) return n;
|
|
484
|
+
}, E = (e, t) => {
|
|
485
|
+
let n = T(t.tag);
|
|
486
|
+
return n === void 0 || Q({ subject: t.subject }, n) ? e : c.fail(new s({ flag: n }));
|
|
487
|
+
}, D = Object.keys(a).length > 0, O = (e) => {
|
|
488
|
+
for (let t of e) X(t, "bulk");
|
|
489
|
+
}, te = {
|
|
226
490
|
...e,
|
|
227
491
|
description: "Resolve every feature flag for the calling subject.",
|
|
228
|
-
execute: (e, t) =>
|
|
229
|
-
|
|
492
|
+
execute: (e, t) => c.sync(() => {
|
|
493
|
+
let e = I.all();
|
|
494
|
+
return O(Object.keys(e)), ne(e, t.request.subject ?? {});
|
|
495
|
+
})
|
|
496
|
+
}, k = {
|
|
230
497
|
...t,
|
|
231
498
|
description: "Resolve every feature flag to its served variant for the calling subject.",
|
|
232
|
-
execute: (e, t) =>
|
|
233
|
-
|
|
499
|
+
execute: (e, t) => c.sync(() => {
|
|
500
|
+
let e = I.all();
|
|
501
|
+
return O(Object.keys(e)), re(e, t.request.subject ?? {});
|
|
502
|
+
})
|
|
503
|
+
}, A = (e) => ({
|
|
234
504
|
kind: "json",
|
|
235
505
|
data: e
|
|
236
|
-
}),
|
|
506
|
+
}), j = (e, t) => ({
|
|
237
507
|
kind: "json",
|
|
238
508
|
status: e,
|
|
239
509
|
data: { error: t }
|
|
240
|
-
}),
|
|
510
|
+
}), M = async () => {
|
|
511
|
+
let e = [];
|
|
512
|
+
if (x && _) try {
|
|
513
|
+
e = await _.load();
|
|
514
|
+
} catch {
|
|
515
|
+
e = [];
|
|
516
|
+
}
|
|
517
|
+
return K({
|
|
518
|
+
flags: I.all(),
|
|
519
|
+
usage: e,
|
|
520
|
+
tracking: x,
|
|
521
|
+
liveCounts: Y.liveTargetedCounts(),
|
|
522
|
+
staleAfterDays: S,
|
|
523
|
+
retentionDays: C
|
|
524
|
+
});
|
|
525
|
+
}, L = () => {
|
|
526
|
+
let e = {};
|
|
527
|
+
for (let [t, n] of f) e[t] = {
|
|
528
|
+
valueType: n.valueType,
|
|
529
|
+
variants: n.variants.map((e) => e.name),
|
|
530
|
+
experiment: n.experiment ?? null
|
|
531
|
+
};
|
|
532
|
+
return e;
|
|
533
|
+
}, oe = [
|
|
241
534
|
{
|
|
242
535
|
method: "GET",
|
|
243
536
|
path: "/list",
|
|
244
|
-
description: "Every resolved flag (key, enabled, rollout, description).",
|
|
245
|
-
handler: () =>
|
|
537
|
+
description: "Every resolved flag (key, enabled, rollout, description) plus the dead-flag lifecycle report: per-flag shape + usage verdict, the observation coverage window, and the limits of what the report can prove.",
|
|
538
|
+
handler: () => c.promise(async () => {
|
|
539
|
+
let e = await M();
|
|
540
|
+
return A({
|
|
541
|
+
flags: z(I.all()),
|
|
542
|
+
lifecycle: e,
|
|
543
|
+
typed: L(),
|
|
544
|
+
overrideRefusals: y
|
|
545
|
+
});
|
|
546
|
+
})
|
|
246
547
|
},
|
|
247
548
|
{
|
|
248
549
|
method: "POST",
|
|
249
550
|
path: "/toggle",
|
|
250
551
|
description: "Flip a flag's kill-switch on/off (live registry; durable on the postgres tier). Logs an audit row.",
|
|
251
|
-
handler: (e) =>
|
|
552
|
+
handler: (e) => c.gen(function* () {
|
|
252
553
|
let t;
|
|
253
554
|
try {
|
|
254
555
|
t = JSON.parse(e.body || "{}");
|
|
255
556
|
} catch {
|
|
256
|
-
return
|
|
557
|
+
return j(400, "invalid JSON body");
|
|
257
558
|
}
|
|
258
|
-
if (!t.key || typeof t.enabled != "boolean") return
|
|
259
|
-
let n =
|
|
260
|
-
if (n === null) return
|
|
261
|
-
let r =
|
|
262
|
-
return
|
|
559
|
+
if (!t.key || typeof t.enabled != "boolean") return j(400, "key (string) + enabled (boolean) required");
|
|
560
|
+
let n = R(t.key, t.enabled);
|
|
561
|
+
if (n === null) return j(404, `unknown flag "${t.key}"`);
|
|
562
|
+
let r = h ? I.get(t.key) : void 0;
|
|
563
|
+
return h && r !== void 0 && (yield* c.promise(() => h.upsertOverride(ie(t.key, r)))), g && (yield* c.promise(() => g.record({
|
|
263
564
|
flag: t.key,
|
|
264
565
|
enabled: t.enabled,
|
|
265
566
|
previousEnabled: n,
|
|
266
567
|
actorId: typeof t.actor == "string" ? t.actor : null,
|
|
267
568
|
at: /* @__PURE__ */ new Date()
|
|
268
|
-
}))),
|
|
569
|
+
}))), A({
|
|
269
570
|
ok: !0,
|
|
270
|
-
flags: I
|
|
571
|
+
flags: z(I.all())
|
|
271
572
|
});
|
|
272
573
|
})
|
|
273
574
|
},
|
|
@@ -275,16 +576,16 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
275
576
|
method: "GET",
|
|
276
577
|
path: "/audit",
|
|
277
578
|
description: "The kill-switch flip audit trail, newest-first. `?flag=<key>` filters; `?limit=<n>` caps (postgres tier only).",
|
|
278
|
-
handler: (e) =>
|
|
279
|
-
if (!
|
|
579
|
+
handler: (e) => c.gen(function* () {
|
|
580
|
+
if (!g) return A({
|
|
280
581
|
audit: [],
|
|
281
582
|
note: "audit trail requires store: \"postgres\""
|
|
282
583
|
});
|
|
283
|
-
let t = new URL(e.url, "http://local"), n = t.searchParams.get("flag"), r = t.searchParams.get("limit"),
|
|
584
|
+
let t = new URL(e.url, "http://local"), n = t.searchParams.get("flag"), r = t.searchParams.get("limit"), i = r != null && Number.isFinite(Number(r)) ? Number(r) : 100, a = yield* c.promise(() => g.list({
|
|
284
585
|
...n == null ? {} : { flag: n },
|
|
285
|
-
limit:
|
|
586
|
+
limit: i
|
|
286
587
|
}));
|
|
287
|
-
return
|
|
588
|
+
return A({ audit: a.map((e) => ({
|
|
288
589
|
flag: e.flag,
|
|
289
590
|
enabled: e.enabled,
|
|
290
591
|
previousEnabled: e.previousEnabled,
|
|
@@ -294,48 +595,68 @@ var m = (e) => typeof e == "boolean" ? { enabled: e } : e, h = (e, t) => {
|
|
|
294
595
|
})
|
|
295
596
|
}
|
|
296
597
|
];
|
|
297
|
-
return
|
|
298
|
-
name:
|
|
598
|
+
return l({
|
|
599
|
+
name: o,
|
|
600
|
+
baseName: $,
|
|
299
601
|
description: "Feature flags — targeting, % rollout, kill-switch, declarative gating + client evaluation.",
|
|
300
602
|
permissions: [
|
|
301
|
-
...
|
|
603
|
+
...D ? [
|
|
302
604
|
"rpc:intercept:mutation",
|
|
303
605
|
"rpc:intercept:query",
|
|
304
606
|
"rpc:intercept:action"
|
|
305
607
|
] : [],
|
|
306
|
-
...
|
|
608
|
+
...m ? ["store:write", "store:changes:read"] : [],
|
|
307
609
|
"inspect:read",
|
|
308
610
|
"inspect:write"
|
|
309
611
|
],
|
|
310
|
-
routes: [
|
|
612
|
+
routes: [te, k],
|
|
311
613
|
rpcClientDescriptors: n,
|
|
312
|
-
inspectEndpoints:
|
|
614
|
+
inspectEndpoints: oe,
|
|
313
615
|
errorSchemas: [{
|
|
314
|
-
schema:
|
|
616
|
+
schema: s,
|
|
315
617
|
import: {
|
|
316
618
|
module: "@voltro/plugin-flags/errors",
|
|
317
619
|
name: "FlagDisabled"
|
|
318
620
|
}
|
|
319
621
|
}],
|
|
320
|
-
...
|
|
321
|
-
interceptMutation:
|
|
322
|
-
interceptQuery:
|
|
323
|
-
interceptAction:
|
|
622
|
+
...D ? {
|
|
623
|
+
interceptMutation: E,
|
|
624
|
+
interceptQuery: E,
|
|
625
|
+
interceptAction: E
|
|
324
626
|
} : {},
|
|
325
|
-
...
|
|
326
|
-
|
|
327
|
-
|
|
627
|
+
...m ? { extendSchema: { tables: x ? [
|
|
628
|
+
N,
|
|
629
|
+
P,
|
|
630
|
+
J
|
|
631
|
+
] : [N, P] } } : {},
|
|
632
|
+
...m ? { bindDataStore: (e) => {
|
|
633
|
+
h = ae(e), g = se(e), x && (_ = fe(e));
|
|
328
634
|
} } : {},
|
|
329
|
-
...
|
|
330
|
-
onActivate: (e) =>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
store:
|
|
635
|
+
...m ? { onChangeEvent: (e) => e.table === "_voltro_feature_flags" ? c.tryPromise(() => w()) : c.void } : {},
|
|
636
|
+
onActivate: (e) => c.gen(function* () {
|
|
637
|
+
I.set(p), m && h && (yield* c.promise(w));
|
|
638
|
+
let t;
|
|
639
|
+
x && _ && (t = he(process.env, C), Y.enable(), v = me({
|
|
640
|
+
store: _,
|
|
641
|
+
intervalMs: b.flushIntervalMs ?? 3e5,
|
|
642
|
+
log: { warn: (t, n) => e.logger.warn(t, n) }
|
|
643
|
+
}));
|
|
644
|
+
for (let t of y) e.logger.warn("feature flag override REFUSED — the code-declared definition stands", {
|
|
645
|
+
flag: t.key,
|
|
646
|
+
reason: t.reason
|
|
647
|
+
});
|
|
648
|
+
e.logger.info("feature flags active", {
|
|
649
|
+
count: Object.keys(p).length,
|
|
650
|
+
typed: f.size,
|
|
651
|
+
gated: Object.keys(a).length,
|
|
652
|
+
store: r.store ?? "memory",
|
|
653
|
+
usage: x ? `tracking (stale after ${S}d, observations kept ${Math.round((t ?? 0) / 864e5)}d)` : "off — no dead-flag evidence is collected"
|
|
335
654
|
});
|
|
336
655
|
}),
|
|
337
|
-
onDeactivate: () =>
|
|
656
|
+
onDeactivate: () => c.promise(async () => {
|
|
657
|
+
await v?.stop(), v = void 0, Y.disable(), I.set({});
|
|
658
|
+
})
|
|
338
659
|
});
|
|
339
660
|
};
|
|
340
661
|
//#endregion
|
|
341
|
-
export {
|
|
662
|
+
export { pe as DEFAULT_FLAG_USAGE_FLUSH_MS, G as FLAG_LIFECYCLE_LIMITS, q as FLAG_USAGE_TABLE, s as FlagDisabled, V as assertFlagExperimentLinks, B as checkFlagExperimentLinks, W as classifyShape, fe as dataStoreFlagUsageStore, o as defineFlag, z as describeFlags, ne as evaluateAll, re as evaluateAllVariants, O as evaluateFlag, D as evaluateSchedule, A as evaluateVariant, N as featureFlagsTable, P as flagAuditTable, K as flagLifecycleReport, I as flagRegistry, Y as flagUsageBuffer, ue as flagUsageRowId, J as flagUsageTable, _e as flagValue, ve as flagVariant, be as flagsPlugin, R as flipFlagEnabled, i as gateOverride, Q as isFlagEnabled, a as isTypedFlag, F as mergeOverrides, X as recordFlagEvaluation, he as registerFlagUsageRetention, ge as requireFlag, k as resolveVariant, L as setFlagEnabled, me as startFlagUsageFlush, x as toDefinition, r as typedValueOf, ce as utcDay };
|