omgkit 2.22.11 → 2.24.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/README.md +20 -8
- package/package.json +2 -2
- package/plugin/commands/hooks/run.md +144 -0
- package/plugin/commands/hooks/setup.md +174 -0
- package/plugin/commands/workflow/init.md +143 -0
- package/plugin/commands/workflow/status.md +126 -0
- package/plugin/commands/workflow/trunk-based.md +175 -0
- package/plugin/registry.yaml +17 -4
- package/plugin/skills/devops/dora-metrics/SKILL.md +852 -0
- package/plugin/skills/devops/feature-flags/SKILL.md +559 -0
- package/plugin/skills/devops/git-hooks/SKILL.md +526 -0
- package/plugin/skills/devops/workflow-config/SKILL.md +581 -0
- package/plugin/skills/methodology/stacked-diffs/SKILL.md +568 -0
- package/plugin/skills/testing/chaos-engineering/SKILL.md +732 -0
- package/plugin/workflows/git/trunk-based.md +447 -0
- package/templates/omgkit/workflow-gitflow.yaml +128 -0
- package/templates/omgkit/workflow-github.yaml +100 -0
- package/templates/omgkit/workflow-trunk.yaml +105 -0
- package/templates/omgkit/workflow.yaml +145 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature Flags and Progressive Delivery
|
|
3
|
+
description: The agent implements feature flag systems for trunk-based development, canary releases, and A/B testing. Use when implementing gradual rollouts, kill switches, or experiment-driven development.
|
|
4
|
+
category: devops
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Feature Flags and Progressive Delivery
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Feature flags (also known as feature toggles) are a core BigTech practice that enables:
|
|
12
|
+
|
|
13
|
+
- **Trunk-based development** without breaking production
|
|
14
|
+
- **Canary releases** and gradual rollouts
|
|
15
|
+
- **A/B testing** and experimentation
|
|
16
|
+
- **Kill switches** for instant production rollbacks
|
|
17
|
+
- **User targeting** for beta features
|
|
18
|
+
|
|
19
|
+
Google, Netflix, Meta, and Amazon all use feature flags extensively to achieve continuous deployment with minimal risk.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
| Feature | Description | Use Case |
|
|
24
|
+
|---------|-------------|----------|
|
|
25
|
+
| Boolean Flags | Simple on/off toggles | Feature enable/disable |
|
|
26
|
+
| Multivariate Flags | Multiple values per flag | A/B/n testing |
|
|
27
|
+
| Percentage Rollouts | Gradual user exposure | Canary releases |
|
|
28
|
+
| User Targeting | Rules-based targeting | Beta programs |
|
|
29
|
+
| Kill Switches | Emergency disable | Incident response |
|
|
30
|
+
| Scheduled Flags | Time-based activation | Launch coordination |
|
|
31
|
+
|
|
32
|
+
## Feature Flag Providers
|
|
33
|
+
|
|
34
|
+
### LaunchDarkly (Enterprise)
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// LaunchDarkly SDK Integration
|
|
38
|
+
import * as LaunchDarkly from 'launchdarkly-node-server-sdk';
|
|
39
|
+
|
|
40
|
+
const client = LaunchDarkly.init(process.env.LAUNCHDARKLY_SDK_KEY);
|
|
41
|
+
|
|
42
|
+
async function isFeatureEnabled(flagKey: string, user: LDUser): Promise<boolean> {
|
|
43
|
+
await client.waitForInitialization();
|
|
44
|
+
return client.variation(flagKey, user, false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Usage
|
|
48
|
+
const user = {
|
|
49
|
+
key: 'user-123',
|
|
50
|
+
email: 'user@example.com',
|
|
51
|
+
custom: {
|
|
52
|
+
plan: 'enterprise',
|
|
53
|
+
region: 'us-east'
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (await isFeatureEnabled('new-checkout-flow', user)) {
|
|
58
|
+
// New checkout experience
|
|
59
|
+
} else {
|
|
60
|
+
// Legacy checkout
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Unleash (Open Source)
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// Unleash SDK Integration
|
|
68
|
+
import { initialize, isEnabled } from 'unleash-client';
|
|
69
|
+
|
|
70
|
+
const unleash = initialize({
|
|
71
|
+
url: 'https://unleash.example.com/api',
|
|
72
|
+
appName: 'my-app',
|
|
73
|
+
customHeaders: {
|
|
74
|
+
Authorization: process.env.UNLEASH_API_TOKEN
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Wait for ready
|
|
79
|
+
unleash.on('ready', () => {
|
|
80
|
+
if (isEnabled('new-feature')) {
|
|
81
|
+
console.log('Feature is enabled!');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// With context
|
|
86
|
+
const context = {
|
|
87
|
+
userId: 'user-123',
|
|
88
|
+
sessionId: 'session-abc',
|
|
89
|
+
remoteAddress: '192.168.1.1'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (isEnabled('beta-feature', context)) {
|
|
93
|
+
// Beta experience
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Flagsmith (Open Source)
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// Flagsmith SDK Integration
|
|
101
|
+
import Flagsmith from 'flagsmith-nodejs';
|
|
102
|
+
|
|
103
|
+
const flagsmith = new Flagsmith({
|
|
104
|
+
environmentKey: process.env.FLAGSMITH_ENVIRONMENT_KEY
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get flags for user
|
|
108
|
+
const flags = await flagsmith.getIdentityFlags('user-123', {
|
|
109
|
+
plan: 'premium',
|
|
110
|
+
signup_date: '2024-01-01'
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (flags.isFeatureEnabled('dark_mode')) {
|
|
114
|
+
enableDarkMode();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Get feature value
|
|
118
|
+
const buttonColor = flags.getFeatureValue('button_color');
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Custom Feature Flag Service
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// Custom Feature Flag Implementation
|
|
125
|
+
interface FeatureFlag {
|
|
126
|
+
key: string;
|
|
127
|
+
enabled: boolean;
|
|
128
|
+
rolloutPercentage?: number;
|
|
129
|
+
userTargeting?: TargetingRule[];
|
|
130
|
+
schedule?: {
|
|
131
|
+
startAt?: Date;
|
|
132
|
+
endAt?: Date;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface TargetingRule {
|
|
137
|
+
attribute: string;
|
|
138
|
+
operator: 'eq' | 'neq' | 'contains' | 'in';
|
|
139
|
+
value: string | string[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class FeatureFlagService {
|
|
143
|
+
private flags: Map<string, FeatureFlag> = new Map();
|
|
144
|
+
|
|
145
|
+
async isEnabled(flagKey: string, context: Record<string, any>): Promise<boolean> {
|
|
146
|
+
const flag = this.flags.get(flagKey);
|
|
147
|
+
if (!flag) return false;
|
|
148
|
+
|
|
149
|
+
// Check if globally disabled
|
|
150
|
+
if (!flag.enabled) return false;
|
|
151
|
+
|
|
152
|
+
// Check schedule
|
|
153
|
+
if (flag.schedule) {
|
|
154
|
+
const now = new Date();
|
|
155
|
+
if (flag.schedule.startAt && now < flag.schedule.startAt) return false;
|
|
156
|
+
if (flag.schedule.endAt && now > flag.schedule.endAt) return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Check targeting rules
|
|
160
|
+
if (flag.userTargeting) {
|
|
161
|
+
const matches = this.evaluateTargeting(flag.userTargeting, context);
|
|
162
|
+
if (!matches) return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Check percentage rollout
|
|
166
|
+
if (flag.rolloutPercentage !== undefined) {
|
|
167
|
+
const hash = this.hashUser(context.userId || 'anonymous', flagKey);
|
|
168
|
+
return hash < flag.rolloutPercentage;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private hashUser(userId: string, flagKey: string): number {
|
|
175
|
+
// Consistent hashing for stable rollout
|
|
176
|
+
const combined = `${userId}:${flagKey}`;
|
|
177
|
+
let hash = 0;
|
|
178
|
+
for (let i = 0; i < combined.length; i++) {
|
|
179
|
+
hash = ((hash << 5) - hash) + combined.charCodeAt(i);
|
|
180
|
+
hash = hash & hash;
|
|
181
|
+
}
|
|
182
|
+
return Math.abs(hash % 100);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private evaluateTargeting(rules: TargetingRule[], context: Record<string, any>): boolean {
|
|
186
|
+
return rules.every(rule => {
|
|
187
|
+
const value = context[rule.attribute];
|
|
188
|
+
switch (rule.operator) {
|
|
189
|
+
case 'eq': return value === rule.value;
|
|
190
|
+
case 'neq': return value !== rule.value;
|
|
191
|
+
case 'contains': return String(value).includes(String(rule.value));
|
|
192
|
+
case 'in': return Array.isArray(rule.value) && rule.value.includes(value);
|
|
193
|
+
default: return false;
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Framework Integration
|
|
201
|
+
|
|
202
|
+
### React/Next.js
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
// React Feature Flag Provider
|
|
206
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
207
|
+
|
|
208
|
+
interface FeatureFlagContextType {
|
|
209
|
+
isEnabled: (flag: string) => boolean;
|
|
210
|
+
getValue: (flag: string) => any;
|
|
211
|
+
loading: boolean;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const FeatureFlagContext = createContext<FeatureFlagContextType | null>(null);
|
|
215
|
+
|
|
216
|
+
export function FeatureFlagProvider({ children, userId }: {
|
|
217
|
+
children: React.ReactNode;
|
|
218
|
+
userId: string;
|
|
219
|
+
}) {
|
|
220
|
+
const [flags, setFlags] = useState<Record<string, any>>({});
|
|
221
|
+
const [loading, setLoading] = useState(true);
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
async function loadFlags() {
|
|
225
|
+
const response = await fetch(`/api/flags?userId=${userId}`);
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
setFlags(data);
|
|
228
|
+
setLoading(false);
|
|
229
|
+
}
|
|
230
|
+
loadFlags();
|
|
231
|
+
}, [userId]);
|
|
232
|
+
|
|
233
|
+
const value = {
|
|
234
|
+
isEnabled: (flag: string) => Boolean(flags[flag]?.enabled),
|
|
235
|
+
getValue: (flag: string) => flags[flag]?.value,
|
|
236
|
+
loading
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<FeatureFlagContext.Provider value={value}>
|
|
241
|
+
{children}
|
|
242
|
+
</FeatureFlagContext.Provider>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Custom Hook
|
|
247
|
+
export function useFeatureFlag(flagKey: string) {
|
|
248
|
+
const context = useContext(FeatureFlagContext);
|
|
249
|
+
if (!context) throw new Error('useFeatureFlag must be used within FeatureFlagProvider');
|
|
250
|
+
return {
|
|
251
|
+
enabled: context.isEnabled(flagKey),
|
|
252
|
+
value: context.getValue(flagKey),
|
|
253
|
+
loading: context.loading
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Component Usage
|
|
258
|
+
function NewFeature() {
|
|
259
|
+
const { enabled, loading } = useFeatureFlag('new-dashboard');
|
|
260
|
+
|
|
261
|
+
if (loading) return <Spinner />;
|
|
262
|
+
if (!enabled) return <LegacyDashboard />;
|
|
263
|
+
|
|
264
|
+
return <NewDashboard />;
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Node.js/Express Middleware
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Express Feature Flag Middleware
|
|
272
|
+
import { Request, Response, NextFunction } from 'express';
|
|
273
|
+
|
|
274
|
+
declare global {
|
|
275
|
+
namespace Express {
|
|
276
|
+
interface Request {
|
|
277
|
+
featureFlags: {
|
|
278
|
+
isEnabled: (flag: string) => Promise<boolean>;
|
|
279
|
+
getValue: (flag: string) => Promise<any>;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function featureFlagMiddleware(flagService: FeatureFlagService) {
|
|
286
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
287
|
+
const context = {
|
|
288
|
+
userId: req.user?.id,
|
|
289
|
+
userAgent: req.headers['user-agent'],
|
|
290
|
+
ip: req.ip,
|
|
291
|
+
...req.user
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
req.featureFlags = {
|
|
295
|
+
isEnabled: (flag: string) => flagService.isEnabled(flag, context),
|
|
296
|
+
getValue: (flag: string) => flagService.getValue(flag, context)
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
next();
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Route Usage
|
|
304
|
+
app.get('/api/checkout', async (req, res) => {
|
|
305
|
+
if (await req.featureFlags.isEnabled('new-checkout')) {
|
|
306
|
+
return newCheckoutHandler(req, res);
|
|
307
|
+
}
|
|
308
|
+
return legacyCheckoutHandler(req, res);
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### Python/FastAPI
|
|
313
|
+
|
|
314
|
+
```python
|
|
315
|
+
# FastAPI Feature Flag Integration
|
|
316
|
+
from fastapi import FastAPI, Depends, Request
|
|
317
|
+
from functools import lru_cache
|
|
318
|
+
import httpx
|
|
319
|
+
|
|
320
|
+
app = FastAPI()
|
|
321
|
+
|
|
322
|
+
class FeatureFlagClient:
|
|
323
|
+
def __init__(self, base_url: str, api_key: str):
|
|
324
|
+
self.base_url = base_url
|
|
325
|
+
self.api_key = api_key
|
|
326
|
+
self._cache = {}
|
|
327
|
+
|
|
328
|
+
async def is_enabled(self, flag: str, context: dict = None) -> bool:
|
|
329
|
+
cache_key = f"{flag}:{hash(frozenset(context.items()) if context else '')}"
|
|
330
|
+
if cache_key in self._cache:
|
|
331
|
+
return self._cache[cache_key]
|
|
332
|
+
|
|
333
|
+
async with httpx.AsyncClient() as client:
|
|
334
|
+
response = await client.post(
|
|
335
|
+
f"{self.base_url}/api/flags/{flag}/evaluate",
|
|
336
|
+
json={"context": context or {}},
|
|
337
|
+
headers={"Authorization": f"Bearer {self.api_key}"}
|
|
338
|
+
)
|
|
339
|
+
result = response.json().get("enabled", False)
|
|
340
|
+
self._cache[cache_key] = result
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
@lru_cache()
|
|
344
|
+
def get_flag_client():
|
|
345
|
+
return FeatureFlagClient(
|
|
346
|
+
base_url=os.environ["FLAG_SERVICE_URL"],
|
|
347
|
+
api_key=os.environ["FLAG_API_KEY"]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def get_flags(request: Request, client: FeatureFlagClient = Depends(get_flag_client)):
|
|
351
|
+
return client
|
|
352
|
+
|
|
353
|
+
@app.get("/api/dashboard")
|
|
354
|
+
async def dashboard(flags: FeatureFlagClient = Depends(get_flags)):
|
|
355
|
+
if await flags.is_enabled("new_dashboard", {"user_id": "123"}):
|
|
356
|
+
return {"dashboard": "v2"}
|
|
357
|
+
return {"dashboard": "v1"}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Rollout Strategies
|
|
361
|
+
|
|
362
|
+
### Percentage Rollout (Canary)
|
|
363
|
+
|
|
364
|
+
```yaml
|
|
365
|
+
# Gradual rollout configuration
|
|
366
|
+
feature: new-payment-flow
|
|
367
|
+
rollout:
|
|
368
|
+
- percentage: 1
|
|
369
|
+
duration: 1d
|
|
370
|
+
metrics:
|
|
371
|
+
- error_rate < 0.1%
|
|
372
|
+
- latency_p99 < 500ms
|
|
373
|
+
- percentage: 10
|
|
374
|
+
duration: 2d
|
|
375
|
+
- percentage: 50
|
|
376
|
+
duration: 3d
|
|
377
|
+
- percentage: 100
|
|
378
|
+
duration: stable
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### Ring-based Deployment
|
|
382
|
+
|
|
383
|
+
```typescript
|
|
384
|
+
// Ring-based rollout
|
|
385
|
+
enum DeploymentRing {
|
|
386
|
+
INTERNAL = 0, // Employee testing
|
|
387
|
+
EARLY_ADOPTERS = 1, // Opt-in beta users
|
|
388
|
+
CANARY = 2, // 1% of users
|
|
389
|
+
PRODUCTION = 3 // 100% of users
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function getUserRing(userId: string): Promise<DeploymentRing> {
|
|
393
|
+
const user = await getUser(userId);
|
|
394
|
+
|
|
395
|
+
if (user.isEmployee) return DeploymentRing.INTERNAL;
|
|
396
|
+
if (user.betaOptIn) return DeploymentRing.EARLY_ADOPTERS;
|
|
397
|
+
|
|
398
|
+
// Canary: first 1% based on user ID hash
|
|
399
|
+
const hash = hashUserId(userId);
|
|
400
|
+
if (hash < 1) return DeploymentRing.CANARY;
|
|
401
|
+
|
|
402
|
+
return DeploymentRing.PRODUCTION;
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Best Practices
|
|
407
|
+
|
|
408
|
+
### 1. Flag Lifecycle Management
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// Flag metadata for lifecycle tracking
|
|
412
|
+
interface FlagMetadata {
|
|
413
|
+
key: string;
|
|
414
|
+
owner: string;
|
|
415
|
+
createdAt: Date;
|
|
416
|
+
expectedRemovalDate: Date;
|
|
417
|
+
type: 'release' | 'experiment' | 'ops' | 'permission';
|
|
418
|
+
status: 'active' | 'deprecated' | 'removed';
|
|
419
|
+
jiraTicket?: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Automated cleanup alerts
|
|
423
|
+
async function auditStaleFlags() {
|
|
424
|
+
const flags = await getAllFlags();
|
|
425
|
+
const stale = flags.filter(f =>
|
|
426
|
+
f.status === 'active' &&
|
|
427
|
+
f.expectedRemovalDate < new Date()
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
for (const flag of stale) {
|
|
431
|
+
await notifyOwner(flag.owner, `Flag ${flag.key} is past removal date`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 2. Testing with Feature Flags
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// Test utilities for feature flags
|
|
440
|
+
class TestFeatureFlagService extends FeatureFlagService {
|
|
441
|
+
private overrides: Map<string, boolean> = new Map();
|
|
442
|
+
|
|
443
|
+
override(flagKey: string, enabled: boolean): void {
|
|
444
|
+
this.overrides.set(flagKey, enabled);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
clearOverrides(): void {
|
|
448
|
+
this.overrides.clear();
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async isEnabled(flagKey: string, context: Record<string, any>): Promise<boolean> {
|
|
452
|
+
if (this.overrides.has(flagKey)) {
|
|
453
|
+
return this.overrides.get(flagKey)!;
|
|
454
|
+
}
|
|
455
|
+
return super.isEnabled(flagKey, context);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// In tests
|
|
460
|
+
describe('New Checkout Flow', () => {
|
|
461
|
+
let flagService: TestFeatureFlagService;
|
|
462
|
+
|
|
463
|
+
beforeEach(() => {
|
|
464
|
+
flagService = new TestFeatureFlagService();
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should show new checkout when flag enabled', async () => {
|
|
468
|
+
flagService.override('new-checkout', true);
|
|
469
|
+
const result = await renderCheckout(flagService);
|
|
470
|
+
expect(result).toContain('New Checkout');
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should show legacy checkout when flag disabled', async () => {
|
|
474
|
+
flagService.override('new-checkout', false);
|
|
475
|
+
const result = await renderCheckout(flagService);
|
|
476
|
+
expect(result).toContain('Legacy Checkout');
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### 3. Flag Documentation
|
|
482
|
+
|
|
483
|
+
```typescript
|
|
484
|
+
// Self-documenting flags
|
|
485
|
+
const FLAGS = {
|
|
486
|
+
NEW_CHECKOUT: {
|
|
487
|
+
key: 'new-checkout-flow',
|
|
488
|
+
description: 'Enables the redesigned checkout experience',
|
|
489
|
+
owner: 'payments-team',
|
|
490
|
+
type: 'release',
|
|
491
|
+
defaultValue: false,
|
|
492
|
+
// Link to design doc, metrics dashboard
|
|
493
|
+
references: {
|
|
494
|
+
designDoc: 'https://docs.example.com/new-checkout',
|
|
495
|
+
metrics: 'https://grafana.example.com/d/checkout'
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} as const;
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Anti-Patterns to Avoid
|
|
502
|
+
|
|
503
|
+
| Anti-Pattern | Problem | Solution |
|
|
504
|
+
|--------------|---------|----------|
|
|
505
|
+
| Long-lived flags | Technical debt accumulation | Set removal dates, automate cleanup |
|
|
506
|
+
| Nested flags | Complexity explosion | Flatten flag dependencies |
|
|
507
|
+
| Flag in loops | Performance issues | Evaluate once, cache result |
|
|
508
|
+
| No default values | Runtime errors | Always provide defaults |
|
|
509
|
+
| Missing tests | Untested code paths | Test both flag states |
|
|
510
|
+
| No ownership | Orphaned flags | Assign owners, track lifecycle |
|
|
511
|
+
|
|
512
|
+
## Use Cases
|
|
513
|
+
|
|
514
|
+
### 1. Trunk-Based Development
|
|
515
|
+
|
|
516
|
+
Enable incomplete features in production without exposing to users:
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// Feature under development
|
|
520
|
+
if (await flags.isEnabled('wip-new-editor')) {
|
|
521
|
+
return <NewEditor />; // Only visible to developers
|
|
522
|
+
}
|
|
523
|
+
return <CurrentEditor />;
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 2. Incident Kill Switch
|
|
527
|
+
|
|
528
|
+
Instantly disable problematic features:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// Kill switch for external service
|
|
532
|
+
if (await flags.isEnabled('external-recommendations')) {
|
|
533
|
+
recommendations = await fetchExternalRecommendations();
|
|
534
|
+
} else {
|
|
535
|
+
recommendations = await getFallbackRecommendations();
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### 3. A/B Testing
|
|
540
|
+
|
|
541
|
+
Run experiments with statistical significance:
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
const variant = await flags.getValue('pricing-page-variant');
|
|
545
|
+
// variant: 'control' | 'variant-a' | 'variant-b'
|
|
546
|
+
trackExperiment('pricing-page', variant, userId);
|
|
547
|
+
return <PricingPage variant={variant} />;
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Related Skills
|
|
551
|
+
|
|
552
|
+
- `devops/github-actions` - CI/CD integration
|
|
553
|
+
- `testing/comprehensive-testing` - Testing flag states
|
|
554
|
+
- `devops/observability` - Monitoring rollouts
|
|
555
|
+
- `methodology/finishing-development-branch` - Branch workflow
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
*Think Omega. Build Omega. Be Omega.*
|