@flagshark/core 2.2.1 → 2.3.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/config/schema.d.ts +10 -10
- package/dist/output/json.d.ts.map +1 -1
- package/dist/output/json.js +37 -0
- package/dist/output/json.js.map +1 -1
- package/dist/output/text.d.ts.map +1 -1
- package/dist/output/text.js +14 -2
- package/dist/output/text.js.map +1 -1
- package/dist/providers/cache.d.ts +2 -0
- package/dist/providers/cache.d.ts.map +1 -1
- package/dist/providers/cache.js +10 -1
- package/dist/providers/cache.js.map +1 -1
- package/dist/providers/cross-reference.d.ts +36 -13
- package/dist/providers/cross-reference.d.ts.map +1 -1
- package/dist/providers/cross-reference.js +166 -52
- package/dist/providers/cross-reference.js.map +1 -1
- package/dist/providers/index.d.ts +2 -0
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/interface.d.ts +91 -3
- package/dist/providers/interface.d.ts.map +1 -1
- package/dist/providers/launchdarkly/client.d.ts +8 -0
- package/dist/providers/launchdarkly/client.d.ts.map +1 -1
- package/dist/providers/launchdarkly/client.js +84 -2
- package/dist/providers/launchdarkly/client.js.map +1 -1
- package/dist/providers/launchdarkly/definition.d.ts +41 -4
- package/dist/providers/launchdarkly/definition.d.ts.map +1 -1
- package/dist/providers/launchdarkly/definition.js +36 -7
- package/dist/providers/launchdarkly/definition.js.map +1 -1
- package/dist/providers/launchdarkly/types.d.ts +443 -0
- package/dist/providers/launchdarkly/types.d.ts.map +1 -1
- package/dist/providers/launchdarkly/types.js +52 -0
- package/dist/providers/launchdarkly/types.js.map +1 -1
- package/dist/providers/orchestrate.d.ts +38 -5
- package/dist/providers/orchestrate.d.ts.map +1 -1
- package/dist/providers/orchestrate.js +92 -18
- package/dist/providers/orchestrate.js.map +1 -1
- package/dist/scan-repo.d.ts.map +1 -1
- package/dist/scan-repo.js +2 -1
- package/dist/scan-repo.js.map +1 -1
- package/dist/staleness.d.ts +42 -2
- package/dist/staleness.d.ts.map +1 -1
- package/dist/staleness.js +16 -4
- package/dist/staleness.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* emits PlatformSignals based on the platform's view of each flag.
|
|
2
|
+
* Render an env attribution suffix for a multi-env signal description.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 2. archived-in-platform — flag archived (warning)
|
|
8
|
-
* 3. platform-launched — LD says single variation for 7+ days (error)
|
|
9
|
-
* 4. platform-inactive — LD says no eval events for 7+ days (warning)
|
|
10
|
-
* 5. platform-permanent — user marked permanent (control signal)
|
|
11
|
-
* 6. platform-too-old — created > thresholdDays ago (warning)
|
|
4
|
+
* fmtEnvs(['production'], ['production', 'staging']) // 'in production'
|
|
5
|
+
* fmtEnvs(['production', 'staging'], ['production', 'staging']) // 'everywhere'
|
|
12
6
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
7
|
+
* Order of envs in the output follows the order of `all` (i.e. the
|
|
8
|
+
* config-declared order, since that's the order the orchestrator builds
|
|
9
|
+
* the inner perEnv map in).
|
|
10
|
+
*/
|
|
11
|
+
function fmtEnvs(triggered, all) {
|
|
12
|
+
// Both empty would render 'everywhere' without the second guard, which
|
|
13
|
+
// is semantically wrong. Current call sites all gate on triggered.length > 0
|
|
14
|
+
// before invoking; the guard is defensive insurance for future callers.
|
|
15
|
+
if (triggered.length === all.length && triggered.length > 0)
|
|
16
|
+
return 'everywhere';
|
|
17
|
+
const ordered = all.filter((e) => triggered.includes(e));
|
|
18
|
+
return `in ${ordered.join(', ')}`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pure function: joins detected flag keys against per-env platform flag data,
|
|
22
|
+
* emits PlatformSignals based on each platform's view of each flag.
|
|
23
|
+
*
|
|
24
|
+
* Two early-exit signals are sole signals when they fire:
|
|
25
|
+
* - missing-in-platform — flag absent from EVERY env (error)
|
|
26
|
+
* - archived-in-platform — flag archived in the platform (warning)
|
|
27
|
+
*
|
|
28
|
+
* All other signals stack — a single flag can carry several at once:
|
|
29
|
+
* - platform-permanent — user marked the flag permanent (info,
|
|
30
|
+
* control signal; filtered from user output)
|
|
31
|
+
* - platform-too-old — created > thresholdDays ago (warning)
|
|
32
|
+
* - platform-launched — at least one env reports the flag as
|
|
33
|
+
* served-single-variation for 7+ days (error)
|
|
34
|
+
* - platform-inactive — at least one env reports no evaluations in
|
|
35
|
+
* 7+ days, AND no env is platform-launched
|
|
36
|
+
* (warning)
|
|
37
|
+
* - platform-zero-evaluations — at least one env has 0 evaluations over
|
|
38
|
+
* the 30-day window (error)
|
|
39
|
+
* - platform-low-evaluations — at least one env is below the threshold,
|
|
40
|
+
* AND no env is at zero (warning)
|
|
41
|
+
* - platform-untouched-stale — EVERY env's audit log confirmed zero
|
|
42
|
+
* activity within the window (warning, the
|
|
43
|
+
* one strict-all-envs rule)
|
|
44
|
+
*
|
|
45
|
+
* For multi-env scans, signal descriptions carry env attribution
|
|
46
|
+
* ('everywhere' when all envs agree, 'in env1, env2' otherwise) via the
|
|
47
|
+
* fmtEnvs helper above.
|
|
16
48
|
*
|
|
17
49
|
* Does NOT surface platform flags with no code reference — that's a separate
|
|
18
50
|
* "orphan platform flags" feature, out of scope.
|
|
19
51
|
*/
|
|
20
|
-
export function crossReference(detectedFlags,
|
|
21
|
-
const platformByKey = new Map(platformFlags.map((f) => [f.key, f]));
|
|
52
|
+
export function crossReference(detectedFlags, platformFlagsByEnv, platformDisplayName, options = {}) {
|
|
22
53
|
const out = new Map();
|
|
23
54
|
const now = Date.now();
|
|
24
55
|
const thresholdMs = options.thresholdDays != null ? options.thresholdDays * 86_400_000 : null;
|
|
25
56
|
for (const key of detectedFlags.keys()) {
|
|
26
|
-
const
|
|
27
|
-
if (!
|
|
57
|
+
const envMap = platformFlagsByEnv.get(key);
|
|
58
|
+
if (!envMap || envMap.size === 0) {
|
|
28
59
|
out.set(key, [
|
|
29
60
|
{
|
|
30
61
|
type: 'missing-in-platform',
|
|
@@ -34,7 +65,11 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
|
|
|
34
65
|
]);
|
|
35
66
|
continue;
|
|
36
67
|
}
|
|
37
|
-
|
|
68
|
+
// Flag-level fields (archived, permanent, tags, maintainer, createdAt)
|
|
69
|
+
// are identical across envs in LD's data model. Read them from the
|
|
70
|
+
// first env's entry.
|
|
71
|
+
const firstEntry = envMap.values().next().value;
|
|
72
|
+
if (firstEntry.archived) {
|
|
38
73
|
out.set(key, [
|
|
39
74
|
{
|
|
40
75
|
type: 'archived-in-platform',
|
|
@@ -44,24 +79,35 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
|
|
|
44
79
|
]);
|
|
45
80
|
continue;
|
|
46
81
|
}
|
|
47
|
-
// Stack-able signals: platform-permanent (control), platform-too-old,
|
|
48
|
-
// platform-inactive/launched. A single flag can carry multiple.
|
|
49
82
|
const signals = [];
|
|
50
|
-
|
|
83
|
+
const allEnvs = Array.from(envMap.keys());
|
|
84
|
+
// platform-launched: fire-on-any with env attribution. Inactive is
|
|
85
|
+
// handled in a separate block below so we can suppress it when any
|
|
86
|
+
// env is launched (they're contradictory states).
|
|
87
|
+
const launchedEnvs = allEnvs.filter((e) => envMap.get(e).status === 'launched');
|
|
88
|
+
if (launchedEnvs.length > 0) {
|
|
89
|
+
const where = fmtEnvs(launchedEnvs, allEnvs);
|
|
51
90
|
signals.push({
|
|
52
91
|
type: 'platform-launched',
|
|
53
92
|
severity: 'error',
|
|
54
|
-
description: `${platformDisplayName} reports this flag has served one variation for 7+ days — likely ready for removal`,
|
|
93
|
+
description: `${platformDisplayName} reports this flag has served one variation for 7+ days ${where} — likely ready for removal`,
|
|
55
94
|
});
|
|
56
95
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
96
|
+
// platform-inactive: fire-on-any-env, BUT suppress when any env is
|
|
97
|
+
// launched (they're contradictory; launched is the more severe
|
|
98
|
+
// and more actionable signal).
|
|
99
|
+
if (launchedEnvs.length === 0) {
|
|
100
|
+
const inactiveEnvs = allEnvs.filter((e) => envMap.get(e).status === 'inactive');
|
|
101
|
+
if (inactiveEnvs.length > 0) {
|
|
102
|
+
const where = fmtEnvs(inactiveEnvs, allEnvs);
|
|
103
|
+
signals.push({
|
|
104
|
+
type: 'platform-inactive',
|
|
105
|
+
severity: 'warning',
|
|
106
|
+
description: `no evaluations recorded in ${platformDisplayName} ${where} in the last 7+ days`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
63
109
|
}
|
|
64
|
-
if (
|
|
110
|
+
if (firstEntry.permanent) {
|
|
65
111
|
// Control signal: tells staleness.ts to suppress age + low-usage
|
|
66
112
|
// signals. Filtered out of the user-facing StaleFlag.signals array
|
|
67
113
|
// before display. Kill-switches and other intentionally permanent
|
|
@@ -77,11 +123,11 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
|
|
|
77
123
|
// exposed a createdAt timestamp. Permanent flags suppress this too —
|
|
78
124
|
// the user explicitly chose to keep a long-lived flag, so don't
|
|
79
125
|
// contradict them with a too-old warning.
|
|
80
|
-
if (!
|
|
126
|
+
if (!firstEntry.permanent &&
|
|
81
127
|
thresholdMs != null &&
|
|
82
|
-
|
|
83
|
-
now -
|
|
84
|
-
const ageDays = Math.floor((now -
|
|
128
|
+
firstEntry.createdAt &&
|
|
129
|
+
now - firstEntry.createdAt.getTime() > thresholdMs) {
|
|
130
|
+
const ageDays = Math.floor((now - firstEntry.createdAt.getTime()) / 86_400_000);
|
|
85
131
|
signals.push({
|
|
86
132
|
type: 'platform-too-old',
|
|
87
133
|
severity: 'warning',
|
|
@@ -97,38 +143,106 @@ export function crossReference(detectedFlags, platformFlags, platformDisplayName
|
|
|
97
143
|
// `undefined` means the feature isn't available for this account
|
|
98
144
|
// (tier-gated / endpoint 404'd); `null` means available but no
|
|
99
145
|
// window data yet. Both fall through to no signal.
|
|
100
|
-
|
|
101
|
-
|
|
146
|
+
//
|
|
147
|
+
// platform-zero-evaluations: fire-on-any with env attribution.
|
|
148
|
+
// platform-low-evaluations: fire-on-any with env attribution, BUT
|
|
149
|
+
// suppress when any env reports zero (zero is the stronger signal;
|
|
150
|
+
// emitting both creates noise without adding information).
|
|
151
|
+
if (!firstEntry.permanent) {
|
|
152
|
+
const zeroEnvs = [];
|
|
153
|
+
const lowByEnv = [];
|
|
154
|
+
const threshold = options.evaluationThreshold ?? 10;
|
|
155
|
+
for (const env of allEnvs) {
|
|
156
|
+
const evals = envMap.get(env).evaluations30d;
|
|
157
|
+
if (typeof evals !== 'number')
|
|
158
|
+
continue;
|
|
159
|
+
if (evals === 0) {
|
|
160
|
+
zeroEnvs.push(env);
|
|
161
|
+
}
|
|
162
|
+
else if (evals < threshold) {
|
|
163
|
+
lowByEnv.push({ env, count: evals });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (zeroEnvs.length > 0) {
|
|
167
|
+
const where = fmtEnvs(zeroEnvs, allEnvs);
|
|
102
168
|
signals.push({
|
|
103
169
|
type: 'platform-zero-evaluations',
|
|
104
170
|
severity: 'error',
|
|
105
|
-
description:
|
|
171
|
+
description: `${platformDisplayName} reports 0 evaluations ${where} over the last 30 days — code path is unused`,
|
|
106
172
|
});
|
|
107
173
|
}
|
|
108
|
-
else {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
174
|
+
else if (lowByEnv.length > 0) {
|
|
175
|
+
// Pick the env with the LOWEST count for the canonical
|
|
176
|
+
// description; render the env list when multiple envs are low.
|
|
177
|
+
const lowEnvs = lowByEnv.map((e) => e.env);
|
|
178
|
+
const lowest = lowByEnv.reduce((a, b) => (a.count <= b.count ? a : b));
|
|
179
|
+
const where = fmtEnvs(lowEnvs, allEnvs);
|
|
180
|
+
// When multiple envs are low with different counts, "only N" is
|
|
181
|
+
// ambiguous (reads as 'each env has N'). "as few as N" makes the
|
|
182
|
+
// minimum-across-envs framing explicit. With one low env, "only N"
|
|
183
|
+
// is the right phrasing — exactly N evaluations in exactly that env.
|
|
184
|
+
const countPhrase = lowByEnv.length > 1
|
|
185
|
+
? `as few as ${lowest.count}`
|
|
186
|
+
: `only ${lowest.count}`;
|
|
187
|
+
signals.push({
|
|
188
|
+
type: 'platform-low-evaluations',
|
|
189
|
+
severity: 'warning',
|
|
190
|
+
description: `${platformDisplayName} reports ${countPhrase} evaluation${lowest.count === 1 ? '' : 's'} ${where} over the last 30 days (below threshold ${threshold})`,
|
|
191
|
+
});
|
|
117
192
|
}
|
|
118
193
|
}
|
|
119
|
-
// platform-untouched-stale:
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
|
|
194
|
+
// platform-untouched-stale: STRICT all-envs rule.
|
|
195
|
+
// Untouched only counts if EVERY env's audit log was successfully
|
|
196
|
+
// fetched AND showed zero activity (lastTouched === null exactly —
|
|
197
|
+
// not undefined, which means the fetch couldn't confirm). A flag
|
|
198
|
+
// toggled in staging counts as touched; a flag whose staging audit
|
|
199
|
+
// log we couldn't read is "unknown", not "untouched".
|
|
200
|
+
//
|
|
201
|
+
// Unlike most signals, this is NOT suppressed for permanent flags —
|
|
202
|
+
// a kill switch untouched for 3 years should still get a periodic
|
|
203
|
+
// review even if the user intends to keep it permanent.
|
|
204
|
+
const allUntouched = allEnvs.length > 0
|
|
205
|
+
&& allEnvs.every((e) => envMap.get(e).lastTouched === null);
|
|
206
|
+
if (allUntouched) {
|
|
207
|
+
// TODO: '90+ days' is hardcoded to AUDIT_LOG_WINDOW_DAYS in the LD
|
|
208
|
+
// client (packages/core/src/providers/launchdarkly/client.ts). If
|
|
209
|
+
// the window becomes a config knob, plumb the actual value through
|
|
210
|
+
// here so the description doesn't lie.
|
|
126
211
|
signals.push({
|
|
127
212
|
type: 'platform-untouched-stale',
|
|
128
213
|
severity: 'warning',
|
|
129
|
-
description: `no activity in ${platformDisplayName} for 90+ days (audit log)`,
|
|
214
|
+
description: `no activity in ${platformDisplayName} ${fmtEnvs(allEnvs, allEnvs)} for 90+ days (audit log)`,
|
|
130
215
|
});
|
|
131
216
|
}
|
|
217
|
+
// coverage-gap-vs-platform: LD's code-refs feature reports more
|
|
218
|
+
// references for this flag than FlagShark detected. The delta is
|
|
219
|
+
// detector blind spots — code patterns LD recognizes that our
|
|
220
|
+
// language detectors missed. Info severity: doesn't make the flag
|
|
221
|
+
// stale, just surfaces a detection-quality issue for triage.
|
|
222
|
+
//
|
|
223
|
+
// Fires only LD-says-more. Reverse direction (FlagShark finds more)
|
|
224
|
+
// is expected: LD's ld-find-code-refs CLI excludes test files,
|
|
225
|
+
// node_modules, etc. by default — FlagShark scans them.
|
|
226
|
+
//
|
|
227
|
+
// Three states of codeReferences (read from firstEntry — the field
|
|
228
|
+
// is flag-level, identical across envs in LD's data model):
|
|
229
|
+
// - undefined → feature unavailable, no signal
|
|
230
|
+
// - null → LD says 0 (no gap possible — FlagShark count is ≥ 0)
|
|
231
|
+
// - { count } → compare count vs FlagShark's detection count
|
|
232
|
+
if (firstEntry.codeReferences && firstEntry.codeReferences.count > 0) {
|
|
233
|
+
/* v8 ignore next — ?? 0 fallback not exercised by current fixtures */
|
|
234
|
+
const detected = detectedFlags.get(key)?.length ?? 0;
|
|
235
|
+
if (firstEntry.codeReferences.count > detected) {
|
|
236
|
+
const gap = firstEntry.codeReferences.count - detected;
|
|
237
|
+
const platCount = firstEntry.codeReferences.count;
|
|
238
|
+
signals.push({
|
|
239
|
+
type: 'coverage-gap-vs-platform',
|
|
240
|
+
severity: 'info',
|
|
241
|
+
description: `${platformDisplayName} detected ${platCount} reference${platCount === 1 ? '' : 's'} ` +
|
|
242
|
+
`for this flag; FlagShark detected ${detected} (gap: ${gap})`,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
132
246
|
if (signals.length > 0) {
|
|
133
247
|
out.set(key, signals);
|
|
134
248
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cross-reference.js","sourceRoot":"","sources":["../../src/providers/cross-reference.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cross-reference.js","sourceRoot":"","sources":["../../src/providers/cross-reference.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export type { PlatformFlag, PlatformClient, PlatformDefinition, PlatformSignal, } from './interface.js';
|
|
2
2
|
export { platformRegistry, findPlatform } from './registry.js';
|
|
3
3
|
export { crossReference, mergePlatformSignals } from './cross-reference.js';
|
|
4
|
+
export type { PerEnvFlags } from './cross-reference.js';
|
|
5
|
+
export type { PerFlagEnvironmentData } from './orchestrate.js';
|
|
4
6
|
export { computeCacheKey, readCache, writeCache, loadPlatformFlagsCached, type CacheOptions, } from './cache.js';
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,cAAc,GACf,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3E,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAA;AAC9D,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,EACvB,KAAK,YAAY,GAClB,MAAM,YAAY,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAG3E,OAAO,EACL,eAAe,EACf,SAAS,EACT,UAAU,EACV,uBAAuB,GAExB,MAAM,YAAY,CAAA"}
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
import type { ZodType } from 'zod';
|
|
1
|
+
import type { ZodType, ZodTypeDef } from 'zod';
|
|
2
|
+
import type { ScanLogger } from '../scan-repo.js';
|
|
3
|
+
/**
|
|
4
|
+
* One variation definition on a flag. The `value` is the runtime value
|
|
5
|
+
* the variation serves (boolean, string, number, JSON object — anything
|
|
6
|
+
* the SDK supports). The `name` is the user-defined label shown in the
|
|
7
|
+
* LD UI ("on", "off", "treatment", etc.).
|
|
8
|
+
*
|
|
9
|
+
* Cleanup pipelines look up the substitute value by index — see
|
|
10
|
+
* PlatformFlag.fallthroughVariation and offVariation.
|
|
11
|
+
*/
|
|
12
|
+
export type FlagVariation = {
|
|
13
|
+
value: unknown;
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
2
16
|
/** A flag entry as reported by a flag-management platform's API. */
|
|
3
17
|
export interface PlatformFlag {
|
|
4
18
|
key: string;
|
|
@@ -112,6 +126,73 @@ export interface PlatformFlag {
|
|
|
112
126
|
* surfacing.
|
|
113
127
|
*/
|
|
114
128
|
lastTouched?: Date | null;
|
|
129
|
+
/**
|
|
130
|
+
* Variation definitions for this flag — flag-level (identical across envs
|
|
131
|
+
* in LD's data model). Indexed by the same integer the per-env
|
|
132
|
+
* `fallthroughVariation` and `offVariation` fields point at. SaaS-side
|
|
133
|
+
* cleanup pipelines look up the substitute value via these indices
|
|
134
|
+
* instead of guessing from code-side default arguments.
|
|
135
|
+
*
|
|
136
|
+
* Undefined when the platform doesn't expose variation data (other
|
|
137
|
+
* providers without an equivalent concept) or when the flag-list
|
|
138
|
+
* response unexpectedly omits the field.
|
|
139
|
+
*/
|
|
140
|
+
variations?: FlagVariation[];
|
|
141
|
+
/**
|
|
142
|
+
* Whether the flag is enabled in the configured env. When false, LD
|
|
143
|
+
* serves `offVariation` regardless of fallthrough/targeting rules.
|
|
144
|
+
* Per-env field — the LD client populates this from the configured env's
|
|
145
|
+
* data on each per-env fetch the orchestrator runs.
|
|
146
|
+
*
|
|
147
|
+
* Undefined when envData itself is absent (archived flags or unexpected
|
|
148
|
+
* API responses).
|
|
149
|
+
*/
|
|
150
|
+
on?: boolean;
|
|
151
|
+
/**
|
|
152
|
+
* Variation index served by fallthrough when no targeting/rule matches
|
|
153
|
+
* AND `on: true`.
|
|
154
|
+
* - number → 100% rollout to that variation
|
|
155
|
+
* - null → split rollout (`fallthrough.rollout` shape) or fallthrough
|
|
156
|
+
* absent. **Preserve null literally in output** — SaaS uses
|
|
157
|
+
* it to detect "can't substitute" and fail closed.
|
|
158
|
+
*
|
|
159
|
+
* Non-optional: the LD client's `?? null` normalization guarantees this
|
|
160
|
+
* field is always set (either a number or null) on every returned flag.
|
|
161
|
+
*/
|
|
162
|
+
fallthroughVariation: number | null;
|
|
163
|
+
/**
|
|
164
|
+
* Variation index served when `on: false`. Required by LD on every
|
|
165
|
+
* active flag (LD's API rejects flag creation without it). Undefined
|
|
166
|
+
* when the platform omits the field (archived flags or unexpected API
|
|
167
|
+
* responses).
|
|
168
|
+
*/
|
|
169
|
+
offVariation?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Cross-check data from LaunchDarkly's own code-references feature.
|
|
172
|
+
* LD's `ld-find-code-refs` CLI scans repos and reports how many code
|
|
173
|
+
* references exist for each flag. FlagShark cross-checks LD's count
|
|
174
|
+
* against its own detection count to surface detector blind spots —
|
|
175
|
+
* flags LD found 12 references for but FlagShark only found 8 indicate
|
|
176
|
+
* 4 hunks our patterns missed.
|
|
177
|
+
*
|
|
178
|
+
* Three-state (matches the evaluations30d convention):
|
|
179
|
+
* - undefined → feature not available (tier-gated, not configured for
|
|
180
|
+
* this project, or the aux fetch errored). No signal
|
|
181
|
+
* emitted; the LD client logs a single advisory per scan.
|
|
182
|
+
* - null → feature available; LD has zero references for this
|
|
183
|
+
* flag. No signal — zero refs cannot be a "gap"; FlagShark's
|
|
184
|
+
* detection count is necessarily ≥ 0.
|
|
185
|
+
* - { count } → LD found `count` references for this flag (sum of
|
|
186
|
+
* hunkCount across all repos LD scanned).
|
|
187
|
+
*
|
|
188
|
+
* Used by cross-reference to emit `coverage-gap-vs-platform` when
|
|
189
|
+
* count > FlagShark's detection count. LD-says-more direction only —
|
|
190
|
+
* reverse direction is expected: FlagShark scans test files that
|
|
191
|
+
* ld-find-code-refs excludes by default.
|
|
192
|
+
*/
|
|
193
|
+
codeReferences?: {
|
|
194
|
+
count: number;
|
|
195
|
+
} | null;
|
|
115
196
|
}
|
|
116
197
|
/** Runtime client for a configured platform. Returned by PlatformDefinition.createClient. */
|
|
117
198
|
export interface PlatformClient {
|
|
@@ -121,6 +202,7 @@ export interface PlatformClient {
|
|
|
121
202
|
displayName: string;
|
|
122
203
|
listFlags(opts?: {
|
|
123
204
|
signal?: AbortSignal;
|
|
205
|
+
logger?: ScanLogger;
|
|
124
206
|
}): Promise<PlatformFlag[]>;
|
|
125
207
|
}
|
|
126
208
|
/** Registry entry. Each platform implementation exports exactly one of these. */
|
|
@@ -131,7 +213,7 @@ export interface PlatformDefinition<TConfig = unknown> {
|
|
|
131
213
|
/** Env var read for the secret token. User can override via token_env. */
|
|
132
214
|
defaultTokenEnv: string;
|
|
133
215
|
/** Zod schema validating this platform's config block. */
|
|
134
|
-
configSchema: ZodType<TConfig>;
|
|
216
|
+
configSchema: ZodType<TConfig, ZodTypeDef, unknown>;
|
|
135
217
|
/** Factory — validated config + resolved token → runtime client. No IO until listFlags() is called. */
|
|
136
218
|
createClient: (config: TConfig, token: string) => PlatformClient;
|
|
137
219
|
}
|
|
@@ -165,8 +247,14 @@ export interface PlatformSignal {
|
|
|
165
247
|
* because it's based on actual usage, not heuristics.
|
|
166
248
|
* - `platform-low-evaluations` (warning): evaluations below the
|
|
167
249
|
* configured threshold over the window. Default threshold: 10/30d.
|
|
250
|
+
* - `coverage-gap-vs-platform` (info): LD's own code-refs feature reports
|
|
251
|
+
* more references for this flag than FlagShark detected. Surfaces detector
|
|
252
|
+
* blind spots — code patterns LD recognizes that our language detectors
|
|
253
|
+
* missed. Doesn't make the flag stale; informational only. Fires only
|
|
254
|
+
* when LD count > FlagShark count; reverse direction (FlagShark counts
|
|
255
|
+
* more) is expected for projects that test-file-exclude in LD.
|
|
168
256
|
*/
|
|
169
|
-
type: 'missing-in-platform' | 'archived-in-platform' | 'platform-permanent' | 'platform-too-old' | 'platform-inactive' | 'platform-launched' | 'platform-zero-evaluations' | 'platform-low-evaluations' | 'platform-untouched-stale';
|
|
257
|
+
type: 'missing-in-platform' | 'archived-in-platform' | 'platform-permanent' | 'platform-too-old' | 'platform-inactive' | 'platform-launched' | 'platform-zero-evaluations' | 'platform-low-evaluations' | 'platform-untouched-stale' | 'coverage-gap-vs-platform';
|
|
170
258
|
severity: 'error' | 'warning' | 'info';
|
|
171
259
|
description: string;
|
|
172
260
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/providers/interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAA;
|
|
1
|
+
{"version":3,"file":"interface.d.ts","sourceRoot":"","sources":["../../src/providers/interface.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PlatformFlag } from '../interface.js';
|
|
2
|
+
import type { ScanLogger } from '../../scan-repo.js';
|
|
2
3
|
export interface FetchAllFlagsConfig {
|
|
3
4
|
project: string;
|
|
4
5
|
environment: string;
|
|
@@ -8,6 +9,13 @@ export interface FetchAllFlagsOptions {
|
|
|
8
9
|
apiBase?: string;
|
|
9
10
|
fetch?: typeof globalThis.fetch;
|
|
10
11
|
signal?: AbortSignal;
|
|
12
|
+
/**
|
|
13
|
+
* Optional logger for one-line advisory messages emitted by aux
|
|
14
|
+
* fetches (e.g. code-refs not configured). When unset, advisories
|
|
15
|
+
* are silently dropped — useful for tests and direct callers that
|
|
16
|
+
* don't go through the orchestrator.
|
|
17
|
+
*/
|
|
18
|
+
logger?: ScanLogger;
|
|
11
19
|
}
|
|
12
20
|
/**
|
|
13
21
|
* Fetches every flag in `config.project` (active + archived), then enriches
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/launchdarkly/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/providers/launchdarkly/client.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAiB,MAAM,iBAAiB,CAAA;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAgCpD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAC/B,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,EAC3B,IAAI,GAAE,oBAAyB,GAC9B,OAAO,CAAC,YAAY,EAAE,CAAC,CAuJzB"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pLimit from 'p-limit';
|
|
2
|
-
import { AuditLogResponseSchema, EvaluationsResponseSchema, FlagsResponseSchema, FlagStatusesResponseSchema, MembersResponseSchema, } from './types.js';
|
|
2
|
+
import { AuditLogResponseSchema, CodeRefsStatisticsResponseSchema, EvaluationsResponseSchema, FlagsResponseSchema, FlagStatusesResponseSchema, MembersResponseSchema, } from './types.js';
|
|
3
3
|
import { LdApiError } from './errors.js';
|
|
4
4
|
const DEFAULT_API_BASE = 'https://app.launchdarkly.com';
|
|
5
5
|
const LD_API_VERSION = '20240415';
|
|
@@ -79,6 +79,14 @@ export async function fetchAllFlags(config, opts = {}) {
|
|
|
79
79
|
// Resolved below from the /members lookup; left as the opaque id
|
|
80
80
|
// for now so the producer/consumer split stays clean.
|
|
81
81
|
maintainer: item.maintainerId,
|
|
82
|
+
// The cast narrows the inferred type from `VariationSchema.passthrough()`
|
|
83
|
+
// (which infers `Record<string, unknown>` for extra fields) back to the
|
|
84
|
+
// declared `PlatformFlag.variations` shape. The runtime data is
|
|
85
|
+
// identical; the cast is a structural type-narrowing only.
|
|
86
|
+
variations: item.variations,
|
|
87
|
+
on: envData?.on,
|
|
88
|
+
fallthroughVariation: envData?.fallthrough?.variation ?? null,
|
|
89
|
+
offVariation: envData?.offVariation,
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
92
|
path = parsed._links?.next?.href;
|
|
@@ -146,6 +154,29 @@ export async function fetchAllFlags(config, opts = {}) {
|
|
|
146
154
|
flag.lastTouched = lastTouched.get(flag.key) ?? null;
|
|
147
155
|
}
|
|
148
156
|
}
|
|
157
|
+
// Aux 5: LD code-references (cross-check against our own detection).
|
|
158
|
+
// Single project-scoped call; not env-scoped. Surfaces detector blind
|
|
159
|
+
// spots when LD finds more references than FlagShark detected. Opt-in
|
|
160
|
+
// LD feature; when unavailable we log an advisory pointing customers
|
|
161
|
+
// at the setup docs.
|
|
162
|
+
const codeRefs = await fetchCodeReferences(config, apiBase, headers, fetchFn, opts.signal);
|
|
163
|
+
if (codeRefs === null) {
|
|
164
|
+
// Feature unavailable — single advisory line. info-severity (not
|
|
165
|
+
// warn) because this isn't a failure, just a missed-opportunity
|
|
166
|
+
// nudge. The existing logger threading from Task 1 delivers it
|
|
167
|
+
// through the orchestrator into the scan output.
|
|
168
|
+
opts.logger?.info(`LaunchDarkly code-references not available for this project — enable it in LD ` +
|
|
169
|
+
`(Code references → Connect repository) to get coverage-gap diagnostics. ` +
|
|
170
|
+
`See https://launchdarkly.com/docs/home/observability/code-references for setup.`);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
for (const flag of out) {
|
|
174
|
+
if (flag.archived)
|
|
175
|
+
continue;
|
|
176
|
+
const count = codeRefs.get(flag.key) ?? 0;
|
|
177
|
+
flag.codeReferences = count > 0 ? { count } : null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
149
180
|
return out;
|
|
150
181
|
}
|
|
151
182
|
/**
|
|
@@ -217,7 +248,12 @@ function buildFirstPath(project, environment, archived = false) {
|
|
|
217
248
|
env: environment,
|
|
218
249
|
limit: '100',
|
|
219
250
|
offset: '0',
|
|
220
|
-
summary
|
|
251
|
+
// summary=0 returns full flag objects with variations + per-env
|
|
252
|
+
// fallthrough/on/offVariation. summary=1 (the previous default) only
|
|
253
|
+
// returned key/tags/lastModified, which was insufficient for SaaS
|
|
254
|
+
// Piranha to substitute the correct value during cleanup. Payload
|
|
255
|
+
// grows ~1-5KB per flag — well within reasonable for paginated fetch.
|
|
256
|
+
summary: '0',
|
|
221
257
|
});
|
|
222
258
|
if (archived)
|
|
223
259
|
params.set('archived', 'true');
|
|
@@ -368,4 +404,50 @@ async function fetchLastTouchedMap(config, apiBase, headers, fetchFn, signal) {
|
|
|
368
404
|
return null;
|
|
369
405
|
return lastTouched;
|
|
370
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Best-effort fetch of LD's per-flag code-references statistics.
|
|
409
|
+
*
|
|
410
|
+
* Returns:
|
|
411
|
+
* - Map<flagKey, totalHunkCount> on success (possibly empty if no flag
|
|
412
|
+
* has refs yet — e.g. CI ran but matched nothing)
|
|
413
|
+
* - null when:
|
|
414
|
+
* - 401/403/404 (tier-gated / not configured for this project)
|
|
415
|
+
* - 5xx / 429 (transient errors)
|
|
416
|
+
* - network or JSON parse failure
|
|
417
|
+
*
|
|
418
|
+
* Caller distinguishes:
|
|
419
|
+
* - null → feature unavailable; leave codeReferences undefined
|
|
420
|
+
* per flag AND emit the "not configured" advisory
|
|
421
|
+
* - empty Map → feature available but no refs yet; flags get null
|
|
422
|
+
* - populated Map → join by flag key; flags absent get null
|
|
423
|
+
*/
|
|
424
|
+
async function fetchCodeReferences(config, apiBase, headers, fetchFn, signal) {
|
|
425
|
+
try {
|
|
426
|
+
const url = new URL(`/api/v2/code-refs/statistics/${encodeURIComponent(config.project)}`, apiBase);
|
|
427
|
+
const res = await fetchFn(url, { headers, signal });
|
|
428
|
+
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
/* v8 ignore next — non-401/403/404 error status; not exercised by current fixtures. */
|
|
432
|
+
if (!res.ok)
|
|
433
|
+
return null;
|
|
434
|
+
const parsed = CodeRefsStatisticsResponseSchema.parse(await res.json());
|
|
435
|
+
const out = new Map();
|
|
436
|
+
for (const [flagKey, repoEntries] of Object.entries(parsed.flags)) {
|
|
437
|
+
let total = 0;
|
|
438
|
+
for (const entry of repoEntries) {
|
|
439
|
+
if (typeof entry.hunkCount === 'number')
|
|
440
|
+
total += entry.hunkCount;
|
|
441
|
+
}
|
|
442
|
+
out.set(flagKey, total);
|
|
443
|
+
}
|
|
444
|
+
return out;
|
|
445
|
+
/* v8 ignore start — defensive catch for malformed JSON / schema
|
|
446
|
+
drift; not exercised by current fixtures. */
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
/* v8 ignore stop */
|
|
452
|
+
}
|
|
371
453
|
//# sourceMappingURL=client.js.map
|