posthog-flag-toolkit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/adapters/inngest.cjs +20 -0
- package/dist/adapters/inngest.cjs.map +1 -0
- package/dist/adapters/inngest.d.cts +29 -0
- package/dist/adapters/inngest.d.ts +29 -0
- package/dist/adapters/inngest.js +18 -0
- package/dist/adapters/inngest.js.map +1 -0
- package/dist/adapters/slack.cjs +75 -0
- package/dist/adapters/slack.cjs.map +1 -0
- package/dist/adapters/slack.d.cts +27 -0
- package/dist/adapters/slack.d.ts +27 -0
- package/dist/adapters/slack.js +73 -0
- package/dist/adapters/slack.js.map +1 -0
- package/dist/index.cjs +674 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +371 -0
- package/dist/index.d.ts +371 -0
- package/dist/index.js +647 -0
- package/dist/index.js.map +1 -0
- package/package.json +114 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal subset of PostHog's FeatureFlag schema — only the fields we use.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: PostHog's FeatureFlag schema has NO separate `description` field.
|
|
5
|
+
* The `name` field IS the description, kept under that name for backwards
|
|
6
|
+
* compatibility. From the OpenAPI spec:
|
|
7
|
+
*
|
|
8
|
+
* name: { type: string, description: "contains the description for the flag
|
|
9
|
+
* (field name `name` is kept for backwards-compatibility)" }
|
|
10
|
+
*/
|
|
11
|
+
interface PostHogClientConfig {
|
|
12
|
+
apiKey: string;
|
|
13
|
+
projectId: string;
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
interface PostHogFlagGroup {
|
|
17
|
+
variant?: string | null;
|
|
18
|
+
properties?: unknown[];
|
|
19
|
+
rollout_percentage?: number | null;
|
|
20
|
+
}
|
|
21
|
+
interface PostHogFlagFilters {
|
|
22
|
+
groups?: PostHogFlagGroup[];
|
|
23
|
+
}
|
|
24
|
+
interface PostHogFlag {
|
|
25
|
+
id: number;
|
|
26
|
+
key: string;
|
|
27
|
+
/** Description of the flag — PostHog calls this `name` for legacy reasons. */
|
|
28
|
+
name: string | null;
|
|
29
|
+
active: boolean;
|
|
30
|
+
deleted: boolean;
|
|
31
|
+
tags: string[] | null;
|
|
32
|
+
filters: PostHogFlagFilters | null;
|
|
33
|
+
created_at: string;
|
|
34
|
+
updated_at?: string;
|
|
35
|
+
}
|
|
36
|
+
interface PostHogFlagList {
|
|
37
|
+
results: PostHogFlag[];
|
|
38
|
+
next: string | null;
|
|
39
|
+
}
|
|
40
|
+
interface PostHogExperiment {
|
|
41
|
+
id: number;
|
|
42
|
+
name: string;
|
|
43
|
+
feature_flag_key: string;
|
|
44
|
+
start_date: string | null;
|
|
45
|
+
end_date: string | null;
|
|
46
|
+
archived: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface PostHogExperimentList {
|
|
49
|
+
results: PostHogExperiment[];
|
|
50
|
+
}
|
|
51
|
+
interface ActivityEntry {
|
|
52
|
+
created_at: string;
|
|
53
|
+
activity: string;
|
|
54
|
+
detail: {
|
|
55
|
+
changes?: Array<{
|
|
56
|
+
field: string;
|
|
57
|
+
after: unknown;
|
|
58
|
+
before: unknown;
|
|
59
|
+
}>;
|
|
60
|
+
} | null;
|
|
61
|
+
}
|
|
62
|
+
interface ActivityList {
|
|
63
|
+
results: ActivityEntry[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CohortMetrics {
|
|
67
|
+
eventCount: number;
|
|
68
|
+
uniqueUsers: number;
|
|
69
|
+
errorRate: number | null;
|
|
70
|
+
publishSuccessRate: number | null;
|
|
71
|
+
}
|
|
72
|
+
interface EvaluationMetrics {
|
|
73
|
+
treatment: CohortMetrics;
|
|
74
|
+
control: CohortMetrics;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Single HogQL round-trip for per-cohort metrics. Partitions by the
|
|
78
|
+
* `$feature/<flag_key>` property the PostHog SDKs attach to every event.
|
|
79
|
+
*
|
|
80
|
+
* Flag key is validated against a strict regex before interpolation.
|
|
81
|
+
*/
|
|
82
|
+
declare function queryCohortMetrics(params: {
|
|
83
|
+
config: PostHogClientConfig;
|
|
84
|
+
flagKey: string;
|
|
85
|
+
windowStart: Date;
|
|
86
|
+
windowEnd: Date;
|
|
87
|
+
/** Custom publish event name. Defaults to `post/publish.completed`. */
|
|
88
|
+
publishEventName?: string;
|
|
89
|
+
/** Custom publish success property. Defaults to `properties.success`. */
|
|
90
|
+
publishSuccessProp?: string;
|
|
91
|
+
}): Promise<EvaluationMetrics>;
|
|
92
|
+
|
|
93
|
+
interface GuardianThresholds {
|
|
94
|
+
windowMinutes: number;
|
|
95
|
+
minSampleSize: number;
|
|
96
|
+
minUniqueUsers: number;
|
|
97
|
+
errorRateRatioThreshold: number;
|
|
98
|
+
publishSuccessDropThreshold: number;
|
|
99
|
+
cooldownMinutes: number;
|
|
100
|
+
}
|
|
101
|
+
declare const DEFAULT_THRESHOLDS: GuardianThresholds;
|
|
102
|
+
declare function mergeThresholds(overrides?: Partial<GuardianThresholds>): GuardianThresholds;
|
|
103
|
+
|
|
104
|
+
type DecisionKind = "insufficient_data" | "no_regression" | "regression_detected" | "auto_disabled";
|
|
105
|
+
interface Decision {
|
|
106
|
+
kind: DecisionKind;
|
|
107
|
+
reason: string;
|
|
108
|
+
}
|
|
109
|
+
declare function fmtPct(n: number | null | undefined): string;
|
|
110
|
+
declare function detectRegression(metrics: EvaluationMetrics, thresholds: GuardianThresholds): Decision;
|
|
111
|
+
declare function meetsSampleFloor(metrics: EvaluationMetrics, thresholds: GuardianThresholds): boolean;
|
|
112
|
+
|
|
113
|
+
interface Logger {
|
|
114
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
115
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
116
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
117
|
+
}
|
|
118
|
+
declare const consoleLogger: Logger;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Abstraction that decouples orchestration functions from Inngest.
|
|
122
|
+
* Inngest's `step.run()` returns `Promise<Jsonify<T>>` which isn't assignable
|
|
123
|
+
* to `Promise<T>` — so we use `unknown` as the return type to stay compatible.
|
|
124
|
+
*/
|
|
125
|
+
interface StepRunner {
|
|
126
|
+
run<T>(id: string, fn: () => Promise<T>): Promise<any>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Simple implementation that just calls `fn()` directly — no durability,
|
|
130
|
+
* suitable for BullMQ, Vercel cron, plain Node, or testing.
|
|
131
|
+
*/
|
|
132
|
+
declare class SimpleStepRunner implements StepRunner {
|
|
133
|
+
run<T>(_id: string, fn: () => Promise<T>): Promise<T>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Auto-rollback cron logic. Scans flags tagged `guardian` and compares
|
|
138
|
+
* treatment vs control cohorts. Pure function with StepRunner + callbacks.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
interface GuardianEvaluation {
|
|
142
|
+
flag: PostHogFlag;
|
|
143
|
+
metrics: EvaluationMetrics;
|
|
144
|
+
decision: DecisionKind;
|
|
145
|
+
reason: string;
|
|
146
|
+
enforced: boolean;
|
|
147
|
+
}
|
|
148
|
+
interface GuardianCallbacks {
|
|
149
|
+
onRegression?: (result: GuardianEvaluation) => void | Promise<void>;
|
|
150
|
+
onEvaluated?: (result: GuardianEvaluation) => void | Promise<void>;
|
|
151
|
+
onEnforced?: (flag: PostHogFlag, result: GuardianEvaluation) => void | Promise<void>;
|
|
152
|
+
}
|
|
153
|
+
interface FlagOutcome {
|
|
154
|
+
flag_key: string;
|
|
155
|
+
decision: DecisionKind;
|
|
156
|
+
reason: string;
|
|
157
|
+
}
|
|
158
|
+
interface GuardianResult {
|
|
159
|
+
flags_evaluated: number;
|
|
160
|
+
regressions_detected: number;
|
|
161
|
+
auto_disabled: number;
|
|
162
|
+
outcomes: FlagOutcome[];
|
|
163
|
+
}
|
|
164
|
+
interface GuardianOptions {
|
|
165
|
+
posthog: PostHogClientConfig;
|
|
166
|
+
thresholds?: Partial<GuardianThresholds>;
|
|
167
|
+
/** Custom HogQL config for publish event detection */
|
|
168
|
+
publishEventName?: string;
|
|
169
|
+
publishSuccessProp?: string;
|
|
170
|
+
step?: StepRunner;
|
|
171
|
+
dryRun?: boolean;
|
|
172
|
+
callbacks?: GuardianCallbacks;
|
|
173
|
+
logger?: Logger;
|
|
174
|
+
}
|
|
175
|
+
declare function posthogFlagUrl(projectId: string, flagId: number, baseUrl?: string): string;
|
|
176
|
+
declare function runFlagGuardian(options: GuardianOptions): Promise<GuardianResult>;
|
|
177
|
+
|
|
178
|
+
/** Fetch every flag in the project, paginating through PostHog's cursor pagination. */
|
|
179
|
+
declare function fetchAllFlags(config: PostHogClientConfig): Promise<PostHogFlag[]>;
|
|
180
|
+
/**
|
|
181
|
+
* Note: PostHog's `?tags=` filter is substring-based — callers should re-filter
|
|
182
|
+
* for exact tag matches via `hasTag()` to be safe.
|
|
183
|
+
*/
|
|
184
|
+
declare function fetchFlagsByTag(config: PostHogClientConfig, tag: string): Promise<PostHogFlag[]>;
|
|
185
|
+
declare function fetchAllExperiments(config: PostHogClientConfig): Promise<PostHogExperiment[]>;
|
|
186
|
+
/**
|
|
187
|
+
* Find when a specific tag was added to a flag by scanning PostHog's activity log.
|
|
188
|
+
*/
|
|
189
|
+
declare function findTagAddedAt(config: PostHogClientConfig, flagId: number, tag: string): Promise<Date | null>;
|
|
190
|
+
interface FlagPatch {
|
|
191
|
+
name?: string;
|
|
192
|
+
tags?: string[];
|
|
193
|
+
active?: boolean;
|
|
194
|
+
filters?: PostHogFlagFilters;
|
|
195
|
+
}
|
|
196
|
+
/** Pass only the fields you want to change. */
|
|
197
|
+
declare function patchFlag(config: PostHogClientConfig, flagId: number, patch: FlagPatch, options?: {
|
|
198
|
+
dryRun?: boolean;
|
|
199
|
+
}): Promise<void>;
|
|
200
|
+
declare function patchFlagTags(config: PostHogClientConfig, flagId: number, tags: string[], options?: {
|
|
201
|
+
dryRun?: boolean;
|
|
202
|
+
}): Promise<void>;
|
|
203
|
+
interface CreateFlagBody {
|
|
204
|
+
key: string;
|
|
205
|
+
/** Goes into PostHog's `name` field (which is actually the description). */
|
|
206
|
+
name: string;
|
|
207
|
+
tags: string[];
|
|
208
|
+
active?: boolean;
|
|
209
|
+
filters?: PostHogFlagFilters;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Defaults to `active: false, rollout: 0%` if filters is omitted.
|
|
213
|
+
*/
|
|
214
|
+
declare function createFlag(config: PostHogClientConfig, body: CreateFlagBody, options?: {
|
|
215
|
+
dryRun?: boolean;
|
|
216
|
+
}): Promise<PostHogFlag | null>;
|
|
217
|
+
/**
|
|
218
|
+
* Active, not deleted, and every release condition at 100% rollout. Flags with
|
|
219
|
+
* zero groups are considered NOT released.
|
|
220
|
+
*/
|
|
221
|
+
declare function isFullyReleased(flag: PostHogFlag): boolean;
|
|
222
|
+
declare function hasTag(flag: PostHogFlag, tag: string): boolean;
|
|
223
|
+
declare function withTagAdded(flag: PostHogFlag, tag: string): string[];
|
|
224
|
+
|
|
225
|
+
type Lifecycle = "release" | "experiment" | "ops" | "tier";
|
|
226
|
+
interface FlagDefinition<K extends string = string> {
|
|
227
|
+
key: K;
|
|
228
|
+
description: string;
|
|
229
|
+
/** @Slack handle or email — shown in admin digests + orphan reports. */
|
|
230
|
+
owner: string;
|
|
231
|
+
/** Navigation tags applied additively to PostHog (e.g. "studio", "instagram"). */
|
|
232
|
+
tags?: readonly string[];
|
|
233
|
+
/** Opt into guardian monitoring on creation (adds the `guardian` tag). */
|
|
234
|
+
guardian?: boolean;
|
|
235
|
+
sunsetTarget?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
interface RegistryConfig<A extends string> {
|
|
239
|
+
areas: readonly A[];
|
|
240
|
+
extraTags?: readonly string[];
|
|
241
|
+
}
|
|
242
|
+
type FeatureFlagKey<A extends string> = `${Lifecycle}_${A}_${string}`;
|
|
243
|
+
interface Registry<A extends string> {
|
|
244
|
+
/**
|
|
245
|
+
* Define the full registry object. The `satisfies` constraint is applied
|
|
246
|
+
* by the consumer at the call site for compile-time validation.
|
|
247
|
+
*/
|
|
248
|
+
NAMING_REGEX: RegExp;
|
|
249
|
+
/** Typed accessor for consumer code. */
|
|
250
|
+
flag<K extends string>(name: K, registry: Record<K, FlagDefinition>): string;
|
|
251
|
+
/** Get the lifecycle prefix from a flag key. */
|
|
252
|
+
getLifecycle: (key: string) => Lifecycle;
|
|
253
|
+
/** Get the area segment from a flag key. */
|
|
254
|
+
getArea: (key: string) => A;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Factory that produces a registry bound to an app's specific areas.
|
|
258
|
+
*
|
|
259
|
+
* Consumer usage:
|
|
260
|
+
* ```ts
|
|
261
|
+
* const { NAMING_REGEX, flag, getLifecycle, getArea } = createRegistry({
|
|
262
|
+
* areas: ["studio", "ai", "publish"] as const,
|
|
263
|
+
* });
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
declare function createRegistry<A extends string>(config: RegistryConfig<A>): Registry<A>;
|
|
267
|
+
|
|
268
|
+
declare function buildNamingRegex(areas: readonly string[]): RegExp;
|
|
269
|
+
declare function getLifecycle(key: string): Lifecycle;
|
|
270
|
+
declare function getArea<A extends string>(key: string): A;
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Detects feature flags / experiments hitting 100% rollout and treats that
|
|
274
|
+
* as a "feature release." Pure function with StepRunner + callbacks.
|
|
275
|
+
*
|
|
276
|
+
* Idempotency, timestamps, and stale-flag dedupe all live in PostHog tags
|
|
277
|
+
* (no DB tables): `released-detected-v1` marks first detection,
|
|
278
|
+
* `stale-notified-YYYY-MM` is month-scoped so flags re-nag once per month.
|
|
279
|
+
*/
|
|
280
|
+
|
|
281
|
+
interface NewRelease {
|
|
282
|
+
key: string;
|
|
283
|
+
name: string;
|
|
284
|
+
type: "flag" | "experiment";
|
|
285
|
+
}
|
|
286
|
+
interface StaleFlagInfo {
|
|
287
|
+
key: string;
|
|
288
|
+
name: string;
|
|
289
|
+
daysSinceRelease: number;
|
|
290
|
+
}
|
|
291
|
+
interface OrphanFlag {
|
|
292
|
+
key: string;
|
|
293
|
+
}
|
|
294
|
+
interface NamingViolation {
|
|
295
|
+
key: string;
|
|
296
|
+
}
|
|
297
|
+
interface DigestPayload {
|
|
298
|
+
newFlagReleases: NewRelease[];
|
|
299
|
+
newExperimentReleases: NewRelease[];
|
|
300
|
+
staleFlags: StaleFlagInfo[];
|
|
301
|
+
orphans: OrphanFlag[];
|
|
302
|
+
namingViolations: NamingViolation[];
|
|
303
|
+
}
|
|
304
|
+
interface ReleaseTrackerCallbacks {
|
|
305
|
+
onNewRelease?: (flag: PostHogFlag, type: "flag" | "experiment") => void | Promise<void>;
|
|
306
|
+
onStale?: (flag: PostHogFlag, daysSinceRelease: number) => void | Promise<void>;
|
|
307
|
+
onOrphan?: (flag: PostHogFlag) => void | Promise<void>;
|
|
308
|
+
onNamingViolation?: (flag: PostHogFlag) => void | Promise<void>;
|
|
309
|
+
onDigestReady?: (digest: DigestPayload) => void | Promise<void>;
|
|
310
|
+
}
|
|
311
|
+
interface ReleaseTrackerResult {
|
|
312
|
+
flags_checked: number;
|
|
313
|
+
experiments_checked: number;
|
|
314
|
+
new_flag_releases: number;
|
|
315
|
+
new_experiment_releases: number;
|
|
316
|
+
stale_flags_notified: number;
|
|
317
|
+
orphans: number;
|
|
318
|
+
naming_violations: number;
|
|
319
|
+
}
|
|
320
|
+
interface ReleaseTrackerOptions {
|
|
321
|
+
posthog: PostHogClientConfig;
|
|
322
|
+
registry: Record<string, FlagDefinition>;
|
|
323
|
+
namingRegex: RegExp;
|
|
324
|
+
namingExemptTag?: string;
|
|
325
|
+
staleThresholdDays?: number;
|
|
326
|
+
step?: StepRunner;
|
|
327
|
+
dryRun?: boolean;
|
|
328
|
+
callbacks?: ReleaseTrackerCallbacks;
|
|
329
|
+
logger?: Logger;
|
|
330
|
+
}
|
|
331
|
+
declare function runReleaseTracker(options: ReleaseTrackerOptions): Promise<ReleaseTrackerResult>;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* One-way sync from a local registry to PostHog. Pure function with
|
|
335
|
+
* StepRunner + callbacks — no framework dependency.
|
|
336
|
+
*
|
|
337
|
+
* Safety principles:
|
|
338
|
+
* - Never deletes flags from PostHog (orphans = human review only)
|
|
339
|
+
* - Never touches active / rollout / conditions (PostHog owns state)
|
|
340
|
+
* - Tag reconciliation is additive only (union, never subtract)
|
|
341
|
+
* - New flags created in safe state (active: false, 0% rollout)
|
|
342
|
+
* - Idempotent — second run is a no-op if registry == PostHog
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
interface FlagSyncCallbacks {
|
|
346
|
+
onCreate?: (def: FlagDefinition) => void | Promise<void>;
|
|
347
|
+
onOrphan?: (flag: PostHogFlag) => void | Promise<void>;
|
|
348
|
+
onNamingViolation?: (flag: PostHogFlag) => void | Promise<void>;
|
|
349
|
+
onReconcile?: (flag: PostHogFlag, patch: Record<string, unknown>) => void | Promise<void>;
|
|
350
|
+
}
|
|
351
|
+
interface FlagSyncResult {
|
|
352
|
+
registry_size: number;
|
|
353
|
+
posthog_size: number;
|
|
354
|
+
created: number;
|
|
355
|
+
reconciled: number;
|
|
356
|
+
orphans: number;
|
|
357
|
+
naming_violations: number;
|
|
358
|
+
}
|
|
359
|
+
interface FlagSyncOptions {
|
|
360
|
+
posthog: PostHogClientConfig;
|
|
361
|
+
registry: Record<string, FlagDefinition>;
|
|
362
|
+
namingRegex: RegExp;
|
|
363
|
+
namingExemptTag?: string;
|
|
364
|
+
step?: StepRunner;
|
|
365
|
+
dryRun?: boolean;
|
|
366
|
+
callbacks?: FlagSyncCallbacks;
|
|
367
|
+
logger?: Logger;
|
|
368
|
+
}
|
|
369
|
+
declare function runFlagSync(options: FlagSyncOptions): Promise<FlagSyncResult>;
|
|
370
|
+
|
|
371
|
+
export { type ActivityEntry, type ActivityList, type CohortMetrics, type CreateFlagBody, DEFAULT_THRESHOLDS, type Decision, type DecisionKind, type DigestPayload, type EvaluationMetrics, type FeatureFlagKey, type FlagDefinition, type FlagOutcome, type FlagPatch, type FlagSyncCallbacks, type FlagSyncOptions, type FlagSyncResult, type GuardianCallbacks, type GuardianEvaluation, type GuardianOptions, type GuardianResult, type GuardianThresholds, type Lifecycle, type Logger, type NamingViolation, type NewRelease, type OrphanFlag, type PostHogClientConfig, type PostHogExperiment, type PostHogExperimentList, type PostHogFlag, type PostHogFlagFilters, type PostHogFlagGroup, type PostHogFlagList, type Registry, type RegistryConfig, type ReleaseTrackerCallbacks, type ReleaseTrackerOptions, type ReleaseTrackerResult, SimpleStepRunner, type StaleFlagInfo, type StepRunner, buildNamingRegex, consoleLogger, createFlag, createRegistry, detectRegression, fetchAllExperiments, fetchAllFlags, fetchFlagsByTag, findTagAddedAt, fmtPct, getArea, getLifecycle, hasTag, isFullyReleased, meetsSampleFloor, mergeThresholds, patchFlag, patchFlagTags, posthogFlagUrl, queryCohortMetrics, runFlagGuardian, runFlagSync, runReleaseTracker, withTagAdded };
|