chub-dev 0.1.0 → 0.1.2-beta.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 +55 -0
- package/bin/chub-mcp +2 -0
- package/dist/airtable/docs/database/javascript/DOC.md +1437 -0
- package/dist/airtable/docs/database/python/DOC.md +1735 -0
- package/dist/amplitude/docs/analytics/javascript/DOC.md +1282 -0
- package/dist/amplitude/docs/analytics/python/DOC.md +1199 -0
- package/dist/anthropic/docs/claude-api/javascript/DOC.md +503 -0
- package/dist/anthropic/docs/claude-api/python/DOC.md +389 -0
- package/dist/asana/docs/tasks/DOC.md +1396 -0
- package/dist/assemblyai/docs/transcription/DOC.md +1043 -0
- package/dist/atlassian/docs/confluence/javascript/DOC.md +1347 -0
- package/dist/atlassian/docs/confluence/python/DOC.md +1604 -0
- package/dist/auth0/docs/identity/javascript/DOC.md +968 -0
- package/dist/auth0/docs/identity/python/DOC.md +1199 -0
- package/dist/aws/docs/s3/javascript/DOC.md +1773 -0
- package/dist/aws/docs/s3/python/DOC.md +1807 -0
- package/dist/binance/docs/trading/javascript/DOC.md +1315 -0
- package/dist/binance/docs/trading/python/DOC.md +1454 -0
- package/dist/braintree/docs/gateway/javascript/DOC.md +1278 -0
- package/dist/braintree/docs/gateway/python/DOC.md +1179 -0
- package/dist/chromadb/docs/embeddings-db/javascript/DOC.md +1263 -0
- package/dist/chromadb/docs/embeddings-db/python/DOC.md +1707 -0
- package/dist/clerk/docs/auth/javascript/DOC.md +1220 -0
- package/dist/clerk/docs/auth/python/DOC.md +274 -0
- package/dist/cloudflare/docs/workers/javascript/DOC.md +918 -0
- package/dist/cloudflare/docs/workers/python/DOC.md +994 -0
- package/dist/cockroachdb/docs/distributed-db/DOC.md +1500 -0
- package/dist/cohere/docs/llm/DOC.md +1335 -0
- package/dist/datadog/docs/monitoring/javascript/DOC.md +1740 -0
- package/dist/datadog/docs/monitoring/python/DOC.md +1815 -0
- package/dist/deepgram/docs/speech/javascript/DOC.md +885 -0
- package/dist/deepgram/docs/speech/python/DOC.md +685 -0
- package/dist/deepl/docs/translation/javascript/DOC.md +887 -0
- package/dist/deepl/docs/translation/python/DOC.md +944 -0
- package/dist/deepseek/docs/llm/DOC.md +1220 -0
- package/dist/directus/docs/headless-cms/javascript/DOC.md +1128 -0
- package/dist/directus/docs/headless-cms/python/DOC.md +1276 -0
- package/dist/discord/docs/bot/javascript/DOC.md +1090 -0
- package/dist/discord/docs/bot/python/DOC.md +1130 -0
- package/dist/elasticsearch/docs/search/DOC.md +1634 -0
- package/dist/elevenlabs/docs/text-to-speech/javascript/DOC.md +336 -0
- package/dist/elevenlabs/docs/text-to-speech/python/DOC.md +552 -0
- package/dist/firebase/docs/auth/DOC.md +1015 -0
- package/dist/gemini/docs/genai/javascript/DOC.md +691 -0
- package/dist/gemini/docs/genai/python/DOC.md +555 -0
- package/dist/github/docs/octokit/DOC.md +1560 -0
- package/dist/google/docs/bigquery/javascript/DOC.md +1688 -0
- package/dist/google/docs/bigquery/python/DOC.md +1503 -0
- package/dist/hubspot/docs/crm/javascript/DOC.md +1805 -0
- package/dist/hubspot/docs/crm/python/DOC.md +2033 -0
- package/dist/huggingface/docs/transformers/DOC.md +948 -0
- package/dist/intercom/docs/messaging/javascript/DOC.md +1844 -0
- package/dist/intercom/docs/messaging/python/DOC.md +1797 -0
- package/dist/jira/docs/issues/javascript/DOC.md +1420 -0
- package/dist/jira/docs/issues/python/DOC.md +1492 -0
- package/dist/kafka/docs/streaming/javascript/DOC.md +1671 -0
- package/dist/kafka/docs/streaming/python/DOC.md +1464 -0
- package/dist/landingai-ade/docs/api/DOC.md +620 -0
- package/dist/landingai-ade/docs/sdk/python/DOC.md +489 -0
- package/dist/landingai-ade/docs/sdk/typescript/DOC.md +542 -0
- package/dist/landingai-ade/skills/SKILL.md +489 -0
- package/dist/launchdarkly/docs/feature-flags/javascript/DOC.md +1191 -0
- package/dist/launchdarkly/docs/feature-flags/python/DOC.md +1671 -0
- package/dist/linear/docs/tracker/DOC.md +1554 -0
- package/dist/livekit/docs/realtime/javascript/DOC.md +303 -0
- package/dist/livekit/docs/realtime/python/DOC.md +163 -0
- package/dist/mailchimp/docs/marketing/DOC.md +1420 -0
- package/dist/meilisearch/docs/search/DOC.md +1241 -0
- package/dist/microsoft/docs/onedrive/javascript/DOC.md +1421 -0
- package/dist/microsoft/docs/onedrive/python/DOC.md +1549 -0
- package/dist/mongodb/docs/atlas/DOC.md +2041 -0
- package/dist/notion/docs/workspace-api/javascript/DOC.md +1435 -0
- package/dist/notion/docs/workspace-api/python/DOC.md +1400 -0
- package/dist/okta/docs/identity/javascript/DOC.md +1171 -0
- package/dist/okta/docs/identity/python/DOC.md +1401 -0
- package/dist/openai/docs/chat/javascript/DOC.md +407 -0
- package/dist/openai/docs/chat/python/DOC.md +568 -0
- package/dist/paypal/docs/checkout/DOC.md +278 -0
- package/dist/pinecone/docs/sdk/javascript/DOC.md +984 -0
- package/dist/pinecone/docs/sdk/python/DOC.md +1395 -0
- package/dist/plaid/docs/banking/javascript/DOC.md +1163 -0
- package/dist/plaid/docs/banking/python/DOC.md +1203 -0
- package/dist/playwright-community/skills/login-flows/SKILL.md +108 -0
- package/dist/postmark/docs/transactional-email/DOC.md +1168 -0
- package/dist/prisma/docs/orm/javascript/DOC.md +1419 -0
- package/dist/prisma/docs/orm/python/DOC.md +1317 -0
- package/dist/qdrant/docs/vector-search/javascript/DOC.md +1221 -0
- package/dist/qdrant/docs/vector-search/python/DOC.md +1653 -0
- package/dist/rabbitmq/docs/message-queue/javascript/DOC.md +1193 -0
- package/dist/rabbitmq/docs/message-queue/python/DOC.md +1243 -0
- package/dist/razorpay/docs/payments/javascript/DOC.md +1219 -0
- package/dist/razorpay/docs/payments/python/DOC.md +1330 -0
- package/dist/redis/docs/key-value/javascript/DOC.md +1851 -0
- package/dist/redis/docs/key-value/python/DOC.md +2054 -0
- package/dist/registry.json +2817 -0
- package/dist/replicate/docs/model-hosting/DOC.md +1318 -0
- package/dist/resend/docs/email/DOC.md +1271 -0
- package/dist/salesforce/docs/crm/javascript/DOC.md +1241 -0
- package/dist/salesforce/docs/crm/python/DOC.md +1183 -0
- package/dist/search-index.json +1 -0
- package/dist/sendgrid/docs/email-api/javascript/DOC.md +371 -0
- package/dist/sendgrid/docs/email-api/python/DOC.md +656 -0
- package/dist/sentry/docs/error-tracking/javascript/DOC.md +1073 -0
- package/dist/sentry/docs/error-tracking/python/DOC.md +1309 -0
- package/dist/shopify/docs/storefront/DOC.md +457 -0
- package/dist/slack/docs/workspace/javascript/DOC.md +933 -0
- package/dist/slack/docs/workspace/python/DOC.md +271 -0
- package/dist/square/docs/payments/javascript/DOC.md +1855 -0
- package/dist/square/docs/payments/python/DOC.md +1728 -0
- package/dist/stripe/docs/api/DOC.md +1727 -0
- package/dist/stripe/docs/payments/DOC.md +1726 -0
- package/dist/stytch/docs/auth/javascript/DOC.md +1813 -0
- package/dist/stytch/docs/auth/python/DOC.md +1962 -0
- package/dist/supabase/docs/client/DOC.md +1606 -0
- package/dist/twilio/docs/messaging/python/DOC.md +469 -0
- package/dist/twilio/docs/messaging/typescript/DOC.md +946 -0
- package/dist/vercel/docs/platform/DOC.md +1940 -0
- package/dist/weaviate/docs/vector-db/javascript/DOC.md +1268 -0
- package/dist/weaviate/docs/vector-db/python/DOC.md +1388 -0
- package/dist/zendesk/docs/support/javascript/DOC.md +2150 -0
- package/dist/zendesk/docs/support/python/DOC.md +2297 -0
- package/package.json +22 -6
- package/skills/get-api-docs/SKILL.md +84 -0
- package/src/commands/annotate.js +83 -0
- package/src/commands/build.js +12 -1
- package/src/commands/feedback.js +150 -0
- package/src/commands/get.js +83 -42
- package/src/commands/search.js +7 -0
- package/src/index.js +43 -17
- package/src/lib/analytics.js +90 -0
- package/src/lib/annotations.js +57 -0
- package/src/lib/bm25.js +170 -0
- package/src/lib/cache.js +69 -6
- package/src/lib/config.js +8 -3
- package/src/lib/identity.js +99 -0
- package/src/lib/registry.js +103 -20
- package/src/lib/telemetry.js +86 -0
- package/src/mcp/server.js +177 -0
- package/src/mcp/tools.js +251 -0
|
@@ -0,0 +1,1671 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: feature-flags
|
|
3
|
+
description: "LaunchDarkly Python Server SDK for feature flag management and experimentation"
|
|
4
|
+
metadata:
|
|
5
|
+
languages: "python"
|
|
6
|
+
versions: "9.12.3"
|
|
7
|
+
updated-on: "2026-03-02"
|
|
8
|
+
source: maintainer
|
|
9
|
+
tags: "launchdarkly,feature-flags,toggles,experimentation,rollout"
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# LaunchDarkly Python Server SDK
|
|
13
|
+
|
|
14
|
+
## Golden Rule
|
|
15
|
+
|
|
16
|
+
**Always use `launchdarkly-server-sdk` for server-side Python applications.**
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install launchdarkly-server-sdk
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Do NOT use:
|
|
23
|
+
- `launchdarkly-client` (deprecated, last updated 2019)
|
|
24
|
+
- `launchdarkly-python-client` (unofficial package)
|
|
25
|
+
- Any unofficial or third-party LaunchDarkly packages
|
|
26
|
+
|
|
27
|
+
The official package is `launchdarkly-server-sdk` maintained by LaunchDarkly.
|
|
28
|
+
|
|
29
|
+
Requires Python 3.9 or higher (as of v9.12+).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
pip install launchdarkly-server-sdk
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Optional observability plugin (requires v9.12+):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install launchdarkly-observability
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Environment Variables
|
|
48
|
+
|
|
49
|
+
Set your SDK key as an environment variable:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export LAUNCHDARKLY_SDK_KEY="sdk-key-123abc"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Initialization
|
|
58
|
+
|
|
59
|
+
### Basic Initialization
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import ldclient
|
|
63
|
+
from ldclient.config import Config
|
|
64
|
+
import os
|
|
65
|
+
|
|
66
|
+
ldclient.set_config(Config(os.environ['LAUNCHDARKLY_SDK_KEY']))
|
|
67
|
+
client = ldclient.get()
|
|
68
|
+
|
|
69
|
+
# Wait for initialization
|
|
70
|
+
if client.is_initialized():
|
|
71
|
+
print('LaunchDarkly client initialized')
|
|
72
|
+
else:
|
|
73
|
+
print('LaunchDarkly client failed to initialize')
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### With Timeout
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import ldclient
|
|
80
|
+
from ldclient.config import Config
|
|
81
|
+
import os
|
|
82
|
+
|
|
83
|
+
ldclient.set_config(Config(os.environ['LAUNCHDARKLY_SDK_KEY']))
|
|
84
|
+
client = ldclient.get()
|
|
85
|
+
|
|
86
|
+
# Wait up to 10 seconds for initialization
|
|
87
|
+
if client.wait_for_initialization(10):
|
|
88
|
+
print('Client initialized successfully')
|
|
89
|
+
else:
|
|
90
|
+
print('Client initialization timeout')
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### With Configuration Options
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import ldclient
|
|
97
|
+
from ldclient.config import Config
|
|
98
|
+
import os
|
|
99
|
+
|
|
100
|
+
config = Config(
|
|
101
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
102
|
+
stream=True,
|
|
103
|
+
connect_timeout=10,
|
|
104
|
+
read_timeout=15,
|
|
105
|
+
offline=False,
|
|
106
|
+
all_attributes_private=False,
|
|
107
|
+
private_attributes={'email', 'ssn'},
|
|
108
|
+
flush_interval=5,
|
|
109
|
+
poll_interval=30,
|
|
110
|
+
diagnostic_opt_out=False,
|
|
111
|
+
wrapper_name='my-wrapper',
|
|
112
|
+
wrapper_version='1.0.0'
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
ldclient.set_config(config)
|
|
116
|
+
client = ldclient.get()
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### With Observability Plugin
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import ldclient
|
|
123
|
+
from ldclient.config import Config
|
|
124
|
+
from ldobserve import ObservabilityPlugin
|
|
125
|
+
import os
|
|
126
|
+
|
|
127
|
+
config = Config(
|
|
128
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
129
|
+
plugins=[ObservabilityPlugin()]
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
ldclient.set_config(config)
|
|
133
|
+
client = ldclient.get()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Singleton Pattern
|
|
137
|
+
|
|
138
|
+
**Critical:** The `ldclient` module maintains a singleton. Use `ldclient.get()` to access the shared instance.
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Initialize once at application startup
|
|
142
|
+
import ldclient
|
|
143
|
+
from ldclient.config import Config
|
|
144
|
+
import os
|
|
145
|
+
|
|
146
|
+
def initialize_launchdarkly():
|
|
147
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
148
|
+
ldclient.set_config(config)
|
|
149
|
+
|
|
150
|
+
client = ldclient.get()
|
|
151
|
+
if not client.wait_for_initialization(10):
|
|
152
|
+
raise Exception('LaunchDarkly failed to initialize')
|
|
153
|
+
|
|
154
|
+
return client
|
|
155
|
+
|
|
156
|
+
# Call once at startup
|
|
157
|
+
client = initialize_launchdarkly()
|
|
158
|
+
|
|
159
|
+
# Access anywhere in application
|
|
160
|
+
def get_ld_client():
|
|
161
|
+
return ldclient.get()
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Error Handling During Initialization
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import ldclient
|
|
168
|
+
from ldclient.config import Config
|
|
169
|
+
import os
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
173
|
+
ldclient.set_config(config)
|
|
174
|
+
client = ldclient.get()
|
|
175
|
+
|
|
176
|
+
if client.wait_for_initialization(10):
|
|
177
|
+
print('Client initialized successfully')
|
|
178
|
+
else:
|
|
179
|
+
print('Client initialization timeout - will use default values')
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f'Initialization error: {e}')
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Contexts
|
|
187
|
+
|
|
188
|
+
### Simple User Context
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from ldclient import Context
|
|
192
|
+
|
|
193
|
+
context = Context.builder('user-key-123abc') \
|
|
194
|
+
.name('Sandy Smith') \
|
|
195
|
+
.build()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Context with Custom Attributes
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from ldclient import Context
|
|
202
|
+
|
|
203
|
+
context = Context.builder('user-key-123abc') \
|
|
204
|
+
.name('Sandy Smith') \
|
|
205
|
+
.set('email', 'sandy@example.com') \
|
|
206
|
+
.set('plan', 'premium') \
|
|
207
|
+
.set('betaTester', True) \
|
|
208
|
+
.build()
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Simple Context (Shorthand)
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from ldclient import Context
|
|
215
|
+
|
|
216
|
+
# Just a key (defaults to 'user' kind)
|
|
217
|
+
context = Context.create('user-key-123abc')
|
|
218
|
+
|
|
219
|
+
# With kind specified
|
|
220
|
+
context = Context.create('org-key-456', 'organization')
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Anonymous Context
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from ldclient import Context
|
|
227
|
+
|
|
228
|
+
context = Context.builder('anonymous-123') \
|
|
229
|
+
.anonymous(True) \
|
|
230
|
+
.build()
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Multi-Context (Multiple Kinds)
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
from ldclient import Context
|
|
237
|
+
|
|
238
|
+
user_context = Context.builder('user-key-123') \
|
|
239
|
+
.kind('user') \
|
|
240
|
+
.name('Sandy') \
|
|
241
|
+
.build()
|
|
242
|
+
|
|
243
|
+
org_context = Context.builder('org-key-456') \
|
|
244
|
+
.kind('organization') \
|
|
245
|
+
.name('Acme Corp') \
|
|
246
|
+
.build()
|
|
247
|
+
|
|
248
|
+
device_context = Context.builder('device-key-789') \
|
|
249
|
+
.kind('device') \
|
|
250
|
+
.set('platform', 'iOS') \
|
|
251
|
+
.build()
|
|
252
|
+
|
|
253
|
+
multi_context = Context.create_multi(user_context, org_context, device_context)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Private Attributes
|
|
257
|
+
|
|
258
|
+
Mark specific attributes as private (not sent to LaunchDarkly):
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from ldclient import Context
|
|
262
|
+
|
|
263
|
+
context = Context.builder('user-key-123abc') \
|
|
264
|
+
.name('Sandy Smith') \
|
|
265
|
+
.set('email', 'sandy@example.com') \
|
|
266
|
+
.set('ssn', '123-45-6789') \
|
|
267
|
+
.private('email', 'ssn') \
|
|
268
|
+
.build()
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Context from Dictionary
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
from ldclient import Context
|
|
275
|
+
|
|
276
|
+
context_dict = {
|
|
277
|
+
'kind': 'user',
|
|
278
|
+
'key': 'user-key-123abc',
|
|
279
|
+
'name': 'Sandy Smith',
|
|
280
|
+
'email': 'sandy@example.com'
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
context = Context.from_dict(context_dict)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Flag Evaluation
|
|
289
|
+
|
|
290
|
+
### Boolean Flag
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
import ldclient
|
|
294
|
+
from ldclient import Context
|
|
295
|
+
|
|
296
|
+
client = ldclient.get()
|
|
297
|
+
context = Context.create('user-key-123abc')
|
|
298
|
+
|
|
299
|
+
show_feature = client.variation('flag-key-123abc', context, False)
|
|
300
|
+
|
|
301
|
+
if show_feature:
|
|
302
|
+
print('Feature enabled')
|
|
303
|
+
else:
|
|
304
|
+
print('Feature disabled')
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### String Flag
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
import ldclient
|
|
311
|
+
from ldclient import Context
|
|
312
|
+
|
|
313
|
+
client = ldclient.get()
|
|
314
|
+
context = Context.create('user-key-123abc')
|
|
315
|
+
|
|
316
|
+
theme = client.variation('theme-flag', context, 'light')
|
|
317
|
+
|
|
318
|
+
print(f'User theme: {theme}') # 'dark' or 'light'
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Number Flag
|
|
322
|
+
|
|
323
|
+
```python
|
|
324
|
+
import ldclient
|
|
325
|
+
from ldclient import Context
|
|
326
|
+
|
|
327
|
+
client = ldclient.get()
|
|
328
|
+
context = Context.create('user-key-123abc')
|
|
329
|
+
|
|
330
|
+
max_items = client.variation('max-items', context, 10)
|
|
331
|
+
|
|
332
|
+
print(f'Max items: {max_items}') # e.g., 50
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### JSON Flag
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
import ldclient
|
|
339
|
+
from ldclient import Context
|
|
340
|
+
|
|
341
|
+
client = ldclient.get()
|
|
342
|
+
context = Context.create('user-key-123abc')
|
|
343
|
+
|
|
344
|
+
config = client.variation('config-flag', context, {
|
|
345
|
+
'timeout': 30,
|
|
346
|
+
'retries': 3
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
print(f"Timeout: {config['timeout']}, Retries: {config['retries']}")
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
## Flag Evaluation with Details
|
|
355
|
+
|
|
356
|
+
### Get Evaluation Reason
|
|
357
|
+
|
|
358
|
+
```python
|
|
359
|
+
import ldclient
|
|
360
|
+
from ldclient import Context
|
|
361
|
+
|
|
362
|
+
client = ldclient.get()
|
|
363
|
+
context = Context.create('user-key-123abc')
|
|
364
|
+
|
|
365
|
+
result = client.variation_detail('flag-key-123abc', context, False)
|
|
366
|
+
|
|
367
|
+
print('Value:', result.value)
|
|
368
|
+
print('Variation Index:', result.variation_index)
|
|
369
|
+
print('Reason:', result.reason)
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Reason Object Structure
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
import ldclient
|
|
376
|
+
from ldclient import Context
|
|
377
|
+
|
|
378
|
+
client = ldclient.get()
|
|
379
|
+
context = Context.create('user-key-123abc')
|
|
380
|
+
|
|
381
|
+
result = client.variation_detail('flag-key-123abc', context, False)
|
|
382
|
+
|
|
383
|
+
# result.reason examples:
|
|
384
|
+
# {'kind': 'OFF'}
|
|
385
|
+
# {'kind': 'FALLTHROUGH'}
|
|
386
|
+
# {'kind': 'TARGET_MATCH'}
|
|
387
|
+
# {'kind': 'RULE_MATCH', 'ruleIndex': 0, 'ruleId': 'rule-id'}
|
|
388
|
+
# {'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': 'other-flag'}
|
|
389
|
+
# {'kind': 'ERROR', 'errorKind': 'MALFORMED_FLAG'}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Using Evaluation Details for Debugging
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
import ldclient
|
|
396
|
+
from ldclient import Context
|
|
397
|
+
|
|
398
|
+
client = ldclient.get()
|
|
399
|
+
context = Context.create('user-key-123abc')
|
|
400
|
+
|
|
401
|
+
result = client.variation_detail('experiment-flag', context, 'control')
|
|
402
|
+
|
|
403
|
+
if result.reason['kind'] == 'ERROR':
|
|
404
|
+
print(f"Flag evaluation error: {result.reason['errorKind']}")
|
|
405
|
+
elif result.reason['kind'] == 'RULE_MATCH':
|
|
406
|
+
print(f"Matched rule: {result.reason['ruleIndex']}")
|
|
407
|
+
elif result.reason['kind'] == 'FALLTHROUGH':
|
|
408
|
+
print('Using fallthrough variation')
|
|
409
|
+
|
|
410
|
+
print(f'Serving variation: {result.value}')
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## All Flags State
|
|
416
|
+
|
|
417
|
+
### Get All Flags for a Context
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
import ldclient
|
|
421
|
+
from ldclient import Context
|
|
422
|
+
|
|
423
|
+
client = ldclient.get()
|
|
424
|
+
context = Context.create('user-key-123abc')
|
|
425
|
+
|
|
426
|
+
state = client.all_flags_state(context)
|
|
427
|
+
|
|
428
|
+
if state.valid:
|
|
429
|
+
all_flags = state.to_values_map()
|
|
430
|
+
print('All flags:', all_flags)
|
|
431
|
+
# {'flag-1': True, 'flag-2': 'value', 'flag-3': 42}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### For Client-Side Bootstrapping
|
|
435
|
+
|
|
436
|
+
```python
|
|
437
|
+
import ldclient
|
|
438
|
+
from ldclient import Context
|
|
439
|
+
|
|
440
|
+
client = ldclient.get()
|
|
441
|
+
context = Context.create('user-key-123abc')
|
|
442
|
+
|
|
443
|
+
state = client.all_flags_state(
|
|
444
|
+
context,
|
|
445
|
+
client_side_only=True,
|
|
446
|
+
with_reasons=True,
|
|
447
|
+
details_only_for_tracked_flags=False
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
if state.valid:
|
|
451
|
+
bootstrap_data = state.to_json_dict()
|
|
452
|
+
# Send to client-side
|
|
453
|
+
return {'flags': bootstrap_data}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Get Individual Flag from State
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
import ldclient
|
|
460
|
+
from ldclient import Context
|
|
461
|
+
|
|
462
|
+
client = ldclient.get()
|
|
463
|
+
context = Context.create('user-key-123abc')
|
|
464
|
+
|
|
465
|
+
state = client.all_flags_state(context)
|
|
466
|
+
|
|
467
|
+
flag_value = state.get_flag_value('flag-key-123abc')
|
|
468
|
+
flag_reason = state.get_flag_reason('flag-key-123abc')
|
|
469
|
+
|
|
470
|
+
print(f'Value: {flag_value}')
|
|
471
|
+
print(f'Reason: {flag_reason}')
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Event Tracking
|
|
477
|
+
|
|
478
|
+
### Track Custom Event
|
|
479
|
+
|
|
480
|
+
```python
|
|
481
|
+
import ldclient
|
|
482
|
+
from ldclient import Context
|
|
483
|
+
|
|
484
|
+
client = ldclient.get()
|
|
485
|
+
context = Context.create('user-key-123abc')
|
|
486
|
+
|
|
487
|
+
client.track('button-clicked', context)
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### Track Event with Data
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
import ldclient
|
|
494
|
+
from ldclient import Context
|
|
495
|
+
|
|
496
|
+
client = ldclient.get()
|
|
497
|
+
context = Context.create('user-key-123abc')
|
|
498
|
+
|
|
499
|
+
client.track('purchase-completed', context, data={
|
|
500
|
+
'item_id': 'item-123',
|
|
501
|
+
'price': 29.99,
|
|
502
|
+
'currency': 'USD'
|
|
503
|
+
})
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Track Event with Numeric Metric
|
|
507
|
+
|
|
508
|
+
```python
|
|
509
|
+
import ldclient
|
|
510
|
+
from ldclient import Context
|
|
511
|
+
|
|
512
|
+
client = ldclient.get()
|
|
513
|
+
context = Context.create('user-key-123abc')
|
|
514
|
+
|
|
515
|
+
client.track('purchase-completed', context, data={
|
|
516
|
+
'item_id': 'item-123'
|
|
517
|
+
}, metric_value=29.99)
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
### Identify Context
|
|
521
|
+
|
|
522
|
+
Send context attributes to LaunchDarkly for targeting:
|
|
523
|
+
|
|
524
|
+
```python
|
|
525
|
+
import ldclient
|
|
526
|
+
from ldclient import Context
|
|
527
|
+
|
|
528
|
+
client = ldclient.get()
|
|
529
|
+
|
|
530
|
+
context = Context.builder('user-key-123abc') \
|
|
531
|
+
.name('Sandy Smith') \
|
|
532
|
+
.set('email', 'sandy@example.com') \
|
|
533
|
+
.build()
|
|
534
|
+
|
|
535
|
+
client.identify(context)
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### Flush Events
|
|
539
|
+
|
|
540
|
+
Force immediate delivery of pending events:
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
import ldclient
|
|
544
|
+
|
|
545
|
+
client = ldclient.get()
|
|
546
|
+
client.flush()
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Auto-Flush on Shutdown
|
|
550
|
+
|
|
551
|
+
```python
|
|
552
|
+
import ldclient
|
|
553
|
+
import signal
|
|
554
|
+
import sys
|
|
555
|
+
|
|
556
|
+
def shutdown_handler(signum, frame):
|
|
557
|
+
print('Shutting down...')
|
|
558
|
+
client = ldclient.get()
|
|
559
|
+
client.flush()
|
|
560
|
+
client.close()
|
|
561
|
+
sys.exit(0)
|
|
562
|
+
|
|
563
|
+
signal.signal(signal.SIGTERM, shutdown_handler)
|
|
564
|
+
signal.signal(signal.SIGINT, shutdown_handler)
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Data Modes
|
|
570
|
+
|
|
571
|
+
### Streaming Mode (Default)
|
|
572
|
+
|
|
573
|
+
```python
|
|
574
|
+
from ldclient.config import Config
|
|
575
|
+
import ldclient
|
|
576
|
+
import os
|
|
577
|
+
|
|
578
|
+
config = Config(
|
|
579
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
580
|
+
stream=True
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
ldclient.set_config(config)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
### Polling Mode
|
|
587
|
+
|
|
588
|
+
```python
|
|
589
|
+
from ldclient.config import Config
|
|
590
|
+
import ldclient
|
|
591
|
+
import os
|
|
592
|
+
|
|
593
|
+
config = Config(
|
|
594
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
595
|
+
stream=False,
|
|
596
|
+
poll_interval=60 # seconds
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
ldclient.set_config(config)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Offline Mode
|
|
603
|
+
|
|
604
|
+
```python
|
|
605
|
+
from ldclient.config import Config
|
|
606
|
+
import ldclient
|
|
607
|
+
import os
|
|
608
|
+
|
|
609
|
+
config = Config(
|
|
610
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
611
|
+
offline=True
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
ldclient.set_config(config)
|
|
615
|
+
client = ldclient.get()
|
|
616
|
+
|
|
617
|
+
# All flags return default values
|
|
618
|
+
result = client.variation('flag-key', context, False)
|
|
619
|
+
# Will always return False (the default)
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
---
|
|
623
|
+
|
|
624
|
+
## HTTP Configuration
|
|
625
|
+
|
|
626
|
+
### Configure HTTP Timeouts
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
from ldclient.config import Config, HTTPConfig
|
|
630
|
+
import ldclient
|
|
631
|
+
import os
|
|
632
|
+
|
|
633
|
+
http_config = HTTPConfig(
|
|
634
|
+
connect_timeout=10,
|
|
635
|
+
read_timeout=15
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
config = Config(
|
|
639
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
640
|
+
http=http_config
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
ldclient.set_config(config)
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### Configure HTTP Proxy
|
|
647
|
+
|
|
648
|
+
```python
|
|
649
|
+
from ldclient.config import Config, HTTPConfig
|
|
650
|
+
import ldclient
|
|
651
|
+
import os
|
|
652
|
+
|
|
653
|
+
http_config = HTTPConfig(
|
|
654
|
+
http_proxy='http://proxy.example.com:8080'
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
config = Config(
|
|
658
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
659
|
+
http=http_config
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
ldclient.set_config(config)
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
### Environment Variable Proxy
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
import os
|
|
669
|
+
|
|
670
|
+
os.environ['HTTPS_PROXY'] = 'https://proxy.example.com:8080'
|
|
671
|
+
|
|
672
|
+
# SDK will automatically use the proxy
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Custom CA Certificates
|
|
676
|
+
|
|
677
|
+
```python
|
|
678
|
+
from ldclient.config import Config, HTTPConfig
|
|
679
|
+
import ldclient
|
|
680
|
+
import os
|
|
681
|
+
|
|
682
|
+
http_config = HTTPConfig(
|
|
683
|
+
ca_certs='/path/to/ca-bundle.crt'
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
config = Config(
|
|
687
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
688
|
+
http=http_config
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
ldclient.set_config(config)
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### Disable SSL Verification (Not Recommended)
|
|
695
|
+
|
|
696
|
+
```python
|
|
697
|
+
from ldclient.config import Config, HTTPConfig
|
|
698
|
+
import ldclient
|
|
699
|
+
import os
|
|
700
|
+
|
|
701
|
+
http_config = HTTPConfig(
|
|
702
|
+
disable_ssl_verification=True
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
config = Config(
|
|
706
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
707
|
+
http=http_config
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
ldclient.set_config(config)
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Relay Proxy
|
|
716
|
+
|
|
717
|
+
### Using LaunchDarkly Relay Proxy
|
|
718
|
+
|
|
719
|
+
```python
|
|
720
|
+
from ldclient.config import Config
|
|
721
|
+
import ldclient
|
|
722
|
+
import os
|
|
723
|
+
|
|
724
|
+
config = Config(
|
|
725
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
726
|
+
base_uri='http://relay-proxy.example.com',
|
|
727
|
+
stream_uri='http://relay-proxy.example.com',
|
|
728
|
+
events_uri='http://relay-proxy.example.com'
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
ldclient.set_config(config)
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
---
|
|
735
|
+
|
|
736
|
+
## Feature Stores
|
|
737
|
+
|
|
738
|
+
### Redis Feature Store
|
|
739
|
+
|
|
740
|
+
```python
|
|
741
|
+
from ldclient.config import Config
|
|
742
|
+
from ldclient.integrations import Redis
|
|
743
|
+
import ldclient
|
|
744
|
+
import os
|
|
745
|
+
|
|
746
|
+
redis_store = Redis.new_feature_store(
|
|
747
|
+
url='redis://localhost:6379',
|
|
748
|
+
prefix='ld',
|
|
749
|
+
max_connections=16,
|
|
750
|
+
expiration=30
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
config = Config(
|
|
754
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
755
|
+
feature_store=redis_store
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
ldclient.set_config(config)
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### DynamoDB Feature Store
|
|
762
|
+
|
|
763
|
+
```python
|
|
764
|
+
from ldclient.config import Config
|
|
765
|
+
from ldclient.integrations import DynamoDB
|
|
766
|
+
import ldclient
|
|
767
|
+
import os
|
|
768
|
+
|
|
769
|
+
dynamodb_store = DynamoDB.new_feature_store(
|
|
770
|
+
'feature-flags-table',
|
|
771
|
+
prefix='ld',
|
|
772
|
+
expiration=30
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
config = Config(
|
|
776
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
777
|
+
feature_store=dynamodb_store
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
ldclient.set_config(config)
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### Consul Feature Store
|
|
784
|
+
|
|
785
|
+
```python
|
|
786
|
+
from ldclient.config import Config
|
|
787
|
+
from ldclient.integrations import Consul
|
|
788
|
+
import ldclient
|
|
789
|
+
import os
|
|
790
|
+
|
|
791
|
+
consul_store = Consul.new_feature_store(
|
|
792
|
+
host='localhost',
|
|
793
|
+
port=8500,
|
|
794
|
+
prefix='ld',
|
|
795
|
+
expiration=30
|
|
796
|
+
)
|
|
797
|
+
|
|
798
|
+
config = Config(
|
|
799
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
800
|
+
feature_store=consul_store
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
ldclient.set_config(config)
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
## File-Based Data Source
|
|
809
|
+
|
|
810
|
+
### Load Flags from File
|
|
811
|
+
|
|
812
|
+
```python
|
|
813
|
+
from ldclient.config import Config
|
|
814
|
+
from ldclient.integrations import Files
|
|
815
|
+
import ldclient
|
|
816
|
+
import os
|
|
817
|
+
|
|
818
|
+
file_source = Files.new_data_source(
|
|
819
|
+
paths=['./flags.json'],
|
|
820
|
+
auto_update=True,
|
|
821
|
+
poll_interval=1
|
|
822
|
+
)
|
|
823
|
+
|
|
824
|
+
config = Config(
|
|
825
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
826
|
+
update_processor_class=file_source
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
ldclient.set_config(config)
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### flags.json Format
|
|
833
|
+
|
|
834
|
+
```json
|
|
835
|
+
{
|
|
836
|
+
"flags": {
|
|
837
|
+
"flag-key-123abc": {
|
|
838
|
+
"key": "flag-key-123abc",
|
|
839
|
+
"on": true,
|
|
840
|
+
"variations": [true, false],
|
|
841
|
+
"fallthrough": {
|
|
842
|
+
"variation": 0
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
"segments": {}
|
|
847
|
+
}
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
---
|
|
851
|
+
|
|
852
|
+
## Big Segments
|
|
853
|
+
|
|
854
|
+
### Configure Big Segments Store
|
|
855
|
+
|
|
856
|
+
```python
|
|
857
|
+
from ldclient.config import Config, BigSegmentsConfig
|
|
858
|
+
from ldclient.integrations import Redis
|
|
859
|
+
import ldclient
|
|
860
|
+
import os
|
|
861
|
+
|
|
862
|
+
redis_store = Redis.new_big_segment_store(
|
|
863
|
+
url='redis://localhost:6379',
|
|
864
|
+
prefix='big-segments'
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
big_segments = BigSegmentsConfig(
|
|
868
|
+
store=redis_store,
|
|
869
|
+
context_cache_size=1000,
|
|
870
|
+
context_cache_time=5,
|
|
871
|
+
status_poll_interval=5,
|
|
872
|
+
stale_after=120
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
config = Config(
|
|
876
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
877
|
+
big_segments=big_segments
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
ldclient.set_config(config)
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### Check Big Segments Status
|
|
884
|
+
|
|
885
|
+
```python
|
|
886
|
+
import ldclient
|
|
887
|
+
from ldclient import Context
|
|
888
|
+
|
|
889
|
+
client = ldclient.get()
|
|
890
|
+
context = Context.create('user-key-123abc')
|
|
891
|
+
|
|
892
|
+
result = client.variation_detail('segment-flag', context, False)
|
|
893
|
+
|
|
894
|
+
if result.reason.get('bigSegmentsStatus') == 'STALE':
|
|
895
|
+
print('Big segments data is stale')
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
---
|
|
899
|
+
|
|
900
|
+
## Hooks
|
|
901
|
+
|
|
902
|
+
### Create a Hook
|
|
903
|
+
|
|
904
|
+
```python
|
|
905
|
+
from ldclient.hook import Hook, Metadata
|
|
906
|
+
|
|
907
|
+
class LoggingHook(Hook):
|
|
908
|
+
@property
|
|
909
|
+
def metadata(self):
|
|
910
|
+
return Metadata(name='logging-hook')
|
|
911
|
+
|
|
912
|
+
def before_evaluation(self, series_context, data):
|
|
913
|
+
print(f'Evaluating flag: {series_context.flag_key}')
|
|
914
|
+
return data
|
|
915
|
+
|
|
916
|
+
def after_evaluation(self, series_context, data, detail):
|
|
917
|
+
print(f'Flag result: {detail.value}')
|
|
918
|
+
return data
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### Register Hook via Configuration
|
|
922
|
+
|
|
923
|
+
```python
|
|
924
|
+
from ldclient.config import Config
|
|
925
|
+
import ldclient
|
|
926
|
+
import os
|
|
927
|
+
|
|
928
|
+
config = Config(
|
|
929
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
930
|
+
hooks=[LoggingHook()]
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
ldclient.set_config(config)
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
### Add Hook at Runtime
|
|
937
|
+
|
|
938
|
+
```python
|
|
939
|
+
import ldclient
|
|
940
|
+
|
|
941
|
+
client = ldclient.get()
|
|
942
|
+
hook = LoggingHook()
|
|
943
|
+
client.add_hook(hook)
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## Migrations
|
|
949
|
+
|
|
950
|
+
### Create Migration
|
|
951
|
+
|
|
952
|
+
```python
|
|
953
|
+
import ldclient
|
|
954
|
+
from ldclient.migrations import MigratorBuilder, Stage, ExecutionOrder
|
|
955
|
+
|
|
956
|
+
client = ldclient.get()
|
|
957
|
+
|
|
958
|
+
def old_read(payload):
|
|
959
|
+
return old_database.read(payload['id'])
|
|
960
|
+
|
|
961
|
+
def new_read(payload):
|
|
962
|
+
return new_database.read(payload['id'])
|
|
963
|
+
|
|
964
|
+
def old_write(payload):
|
|
965
|
+
old_database.write(payload['id'], payload['data'])
|
|
966
|
+
|
|
967
|
+
def new_write(payload):
|
|
968
|
+
new_database.write(payload['id'], payload['data'])
|
|
969
|
+
|
|
970
|
+
def check_consistency(old_value, new_value):
|
|
971
|
+
return old_value == new_value
|
|
972
|
+
|
|
973
|
+
migrator = MigratorBuilder(client) \
|
|
974
|
+
.read(old_read, new_read, check_consistency) \
|
|
975
|
+
.write(old_write, new_write) \
|
|
976
|
+
.read_execution_order(ExecutionOrder.PARALLEL) \
|
|
977
|
+
.track_latency(True) \
|
|
978
|
+
.track_errors(True) \
|
|
979
|
+
.build()
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### Execute Migration Read
|
|
983
|
+
|
|
984
|
+
```python
|
|
985
|
+
import ldclient
|
|
986
|
+
from ldclient import Context
|
|
987
|
+
from ldclient.migrations import Stage
|
|
988
|
+
|
|
989
|
+
client = ldclient.get()
|
|
990
|
+
context = Context.create('user-key-123abc')
|
|
991
|
+
|
|
992
|
+
stage_str = client.variation('migration-flag', context, 'off')
|
|
993
|
+
stage = Stage.from_str(stage_str)
|
|
994
|
+
|
|
995
|
+
result = migrator.read(
|
|
996
|
+
'migration-flag',
|
|
997
|
+
context,
|
|
998
|
+
stage,
|
|
999
|
+
payload={'id': 'record-123'}
|
|
1000
|
+
)
|
|
1001
|
+
|
|
1002
|
+
if result.is_success():
|
|
1003
|
+
print('Data:', result.value)
|
|
1004
|
+
else:
|
|
1005
|
+
print('Migration read failed')
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
### Execute Migration Write
|
|
1009
|
+
|
|
1010
|
+
```python
|
|
1011
|
+
import ldclient
|
|
1012
|
+
from ldclient import Context
|
|
1013
|
+
from ldclient.migrations import Stage
|
|
1014
|
+
|
|
1015
|
+
client = ldclient.get()
|
|
1016
|
+
context = Context.create('user-key-123abc')
|
|
1017
|
+
|
|
1018
|
+
stage_str = client.variation('migration-flag', context, 'off')
|
|
1019
|
+
stage = Stage.from_str(stage_str)
|
|
1020
|
+
|
|
1021
|
+
result = migrator.write(
|
|
1022
|
+
'migration-flag',
|
|
1023
|
+
context,
|
|
1024
|
+
stage,
|
|
1025
|
+
payload={'id': 'record-123', 'data': {'name': 'Example'}}
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
if result.is_success():
|
|
1029
|
+
print('Write completed')
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
### Migration Stages
|
|
1033
|
+
|
|
1034
|
+
```python
|
|
1035
|
+
from ldclient.migrations import Stage
|
|
1036
|
+
|
|
1037
|
+
# Available stages:
|
|
1038
|
+
Stage.OFF # Use old implementation only
|
|
1039
|
+
Stage.DUALWRITE # Write to both, read from old
|
|
1040
|
+
Stage.SHADOW # Read from both, write to both, use old for responses
|
|
1041
|
+
Stage.LIVE # Read from both, write to both, use new for responses
|
|
1042
|
+
Stage.RAMPDOWN # Write to both, read from new
|
|
1043
|
+
Stage.COMPLETE # Use new implementation only
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
### Execution Order
|
|
1047
|
+
|
|
1048
|
+
```python
|
|
1049
|
+
from ldclient.migrations import ExecutionOrder
|
|
1050
|
+
|
|
1051
|
+
ExecutionOrder.SERIAL # Execute old then new sequentially
|
|
1052
|
+
ExecutionOrder.RANDOM # Randomize execution order
|
|
1053
|
+
ExecutionOrder.PARALLEL # Execute both concurrently
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Worker-Based Servers (uWSGI, Gunicorn)
|
|
1059
|
+
|
|
1060
|
+
### uWSGI Configuration
|
|
1061
|
+
|
|
1062
|
+
```python
|
|
1063
|
+
import ldclient
|
|
1064
|
+
from ldclient.config import Config
|
|
1065
|
+
import os
|
|
1066
|
+
|
|
1067
|
+
# Initialize before forking
|
|
1068
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1069
|
+
ldclient.set_config(config)
|
|
1070
|
+
client = ldclient.get()
|
|
1071
|
+
|
|
1072
|
+
# Reinitialize after fork
|
|
1073
|
+
try:
|
|
1074
|
+
import uwsgidecorators
|
|
1075
|
+
|
|
1076
|
+
@uwsgidecorators.postfork
|
|
1077
|
+
def reinit_ld_after_fork():
|
|
1078
|
+
ldclient.get().postfork()
|
|
1079
|
+
print('LaunchDarkly reinitialized after fork')
|
|
1080
|
+
except ImportError:
|
|
1081
|
+
pass
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
### Gunicorn Configuration
|
|
1085
|
+
|
|
1086
|
+
```python
|
|
1087
|
+
# gunicorn_config.py
|
|
1088
|
+
import ldclient
|
|
1089
|
+
from ldclient.config import Config
|
|
1090
|
+
import os
|
|
1091
|
+
|
|
1092
|
+
def on_starting(server):
|
|
1093
|
+
"""Called just before the master process is initialized."""
|
|
1094
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1095
|
+
ldclient.set_config(config)
|
|
1096
|
+
|
|
1097
|
+
def post_fork(server, worker):
|
|
1098
|
+
"""Called just after a worker has been forked."""
|
|
1099
|
+
ldclient.get().postfork()
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
### Without Decorator
|
|
1103
|
+
|
|
1104
|
+
```python
|
|
1105
|
+
import ldclient
|
|
1106
|
+
from ldclient.config import Config
|
|
1107
|
+
import os
|
|
1108
|
+
|
|
1109
|
+
# After forking in worker process
|
|
1110
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1111
|
+
ldclient.set_config(config)
|
|
1112
|
+
client = ldclient.get()
|
|
1113
|
+
|
|
1114
|
+
# Reinitialize
|
|
1115
|
+
client.postfork()
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
---
|
|
1119
|
+
|
|
1120
|
+
## Application Metadata
|
|
1121
|
+
|
|
1122
|
+
### Set Application Info
|
|
1123
|
+
|
|
1124
|
+
```python
|
|
1125
|
+
from ldclient.config import Config
|
|
1126
|
+
import ldclient
|
|
1127
|
+
import os
|
|
1128
|
+
|
|
1129
|
+
config = Config(
|
|
1130
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1131
|
+
application={
|
|
1132
|
+
'id': 'my-app',
|
|
1133
|
+
'version': '1.2.3',
|
|
1134
|
+
'name': 'My Application',
|
|
1135
|
+
'version_name': 'v1.2.3-beta'
|
|
1136
|
+
}
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
ldclient.set_config(config)
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
---
|
|
1143
|
+
|
|
1144
|
+
## Secure Mode
|
|
1145
|
+
|
|
1146
|
+
### Generate Secure Mode Hash
|
|
1147
|
+
|
|
1148
|
+
```python
|
|
1149
|
+
import hashlib
|
|
1150
|
+
import hmac
|
|
1151
|
+
import os
|
|
1152
|
+
|
|
1153
|
+
def generate_secure_mode_hash(sdk_key, context_key):
|
|
1154
|
+
"""Generate secure mode hash for client-side SDK."""
|
|
1155
|
+
return hmac.new(
|
|
1156
|
+
sdk_key.encode(),
|
|
1157
|
+
context_key.encode(),
|
|
1158
|
+
hashlib.sha256
|
|
1159
|
+
).hexdigest()
|
|
1160
|
+
|
|
1161
|
+
hash_value = generate_secure_mode_hash(
|
|
1162
|
+
os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1163
|
+
'user-key-123abc'
|
|
1164
|
+
)
|
|
1165
|
+
|
|
1166
|
+
# Send hash to client-side
|
|
1167
|
+
return {'hash': hash_value}
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
---
|
|
1171
|
+
|
|
1172
|
+
## Logging
|
|
1173
|
+
|
|
1174
|
+
### Configure Logger
|
|
1175
|
+
|
|
1176
|
+
```python
|
|
1177
|
+
import logging
|
|
1178
|
+
from ldclient.config import Config
|
|
1179
|
+
import ldclient
|
|
1180
|
+
import os
|
|
1181
|
+
|
|
1182
|
+
# Set up logging
|
|
1183
|
+
logging.basicConfig(level=logging.INFO)
|
|
1184
|
+
logger = logging.getLogger('ldclient')
|
|
1185
|
+
logger.setLevel(logging.DEBUG)
|
|
1186
|
+
|
|
1187
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1188
|
+
ldclient.set_config(config)
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### Custom Log Level
|
|
1192
|
+
|
|
1193
|
+
```python
|
|
1194
|
+
import logging
|
|
1195
|
+
from ldclient.config import Config
|
|
1196
|
+
import ldclient
|
|
1197
|
+
import os
|
|
1198
|
+
|
|
1199
|
+
# Create custom logger
|
|
1200
|
+
ld_logger = logging.getLogger('ldclient')
|
|
1201
|
+
ld_logger.setLevel(logging.WARNING)
|
|
1202
|
+
|
|
1203
|
+
handler = logging.StreamHandler()
|
|
1204
|
+
handler.setLevel(logging.WARNING)
|
|
1205
|
+
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
1206
|
+
handler.setFormatter(formatter)
|
|
1207
|
+
ld_logger.addHandler(handler)
|
|
1208
|
+
|
|
1209
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1210
|
+
ldclient.set_config(config)
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
---
|
|
1214
|
+
|
|
1215
|
+
## Private Attributes Configuration
|
|
1216
|
+
|
|
1217
|
+
### Global Private Attributes
|
|
1218
|
+
|
|
1219
|
+
```python
|
|
1220
|
+
from ldclient.config import Config
|
|
1221
|
+
import ldclient
|
|
1222
|
+
import os
|
|
1223
|
+
|
|
1224
|
+
config = Config(
|
|
1225
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1226
|
+
private_attributes={'email', 'ssn', 'address'}
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
ldclient.set_config(config)
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### All Attributes Private
|
|
1233
|
+
|
|
1234
|
+
```python
|
|
1235
|
+
from ldclient.config import Config
|
|
1236
|
+
import ldclient
|
|
1237
|
+
import os
|
|
1238
|
+
|
|
1239
|
+
config = Config(
|
|
1240
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1241
|
+
all_attributes_private=True
|
|
1242
|
+
)
|
|
1243
|
+
|
|
1244
|
+
ldclient.set_config(config)
|
|
1245
|
+
```
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
## Diagnostics
|
|
1250
|
+
|
|
1251
|
+
### Disable Diagnostic Events
|
|
1252
|
+
|
|
1253
|
+
```python
|
|
1254
|
+
from ldclient.config import Config
|
|
1255
|
+
import ldclient
|
|
1256
|
+
import os
|
|
1257
|
+
|
|
1258
|
+
config = Config(
|
|
1259
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1260
|
+
diagnostic_opt_out=True
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
ldclient.set_config(config)
|
|
1264
|
+
```
|
|
1265
|
+
|
|
1266
|
+
---
|
|
1267
|
+
|
|
1268
|
+
## Testing
|
|
1269
|
+
|
|
1270
|
+
### Using File Data Source for Tests
|
|
1271
|
+
|
|
1272
|
+
```python
|
|
1273
|
+
from ldclient.config import Config
|
|
1274
|
+
from ldclient.integrations import Files
|
|
1275
|
+
import ldclient
|
|
1276
|
+
|
|
1277
|
+
# Create test flags file
|
|
1278
|
+
test_flags = {
|
|
1279
|
+
'flags': {
|
|
1280
|
+
'test-flag': {
|
|
1281
|
+
'key': 'test-flag',
|
|
1282
|
+
'on': True,
|
|
1283
|
+
'variations': [True, False],
|
|
1284
|
+
'fallthrough': {'variation': 0}
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
'segments': {}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
# Write to file
|
|
1291
|
+
import json
|
|
1292
|
+
with open('test-flags.json', 'w') as f:
|
|
1293
|
+
json.dump(test_flags, f)
|
|
1294
|
+
|
|
1295
|
+
# Configure client with file source
|
|
1296
|
+
file_source = Files.new_data_source(paths=['test-flags.json'])
|
|
1297
|
+
|
|
1298
|
+
config = Config(
|
|
1299
|
+
sdk_key='test-sdk-key',
|
|
1300
|
+
update_processor_class=file_source,
|
|
1301
|
+
send_events=False
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
ldclient.set_config(config)
|
|
1305
|
+
client = ldclient.get()
|
|
1306
|
+
```
|
|
1307
|
+
|
|
1308
|
+
### Mock Client for Unit Tests
|
|
1309
|
+
|
|
1310
|
+
```python
|
|
1311
|
+
from unittest.mock import Mock, MagicMock
|
|
1312
|
+
from ldclient import EvaluationDetail
|
|
1313
|
+
|
|
1314
|
+
mock_client = Mock()
|
|
1315
|
+
mock_client.variation = Mock(return_value=True)
|
|
1316
|
+
mock_client.variation_detail = Mock(return_value=EvaluationDetail(
|
|
1317
|
+
value=True,
|
|
1318
|
+
variation_index=0,
|
|
1319
|
+
reason={'kind': 'OFF'}
|
|
1320
|
+
))
|
|
1321
|
+
mock_client.track = Mock()
|
|
1322
|
+
mock_client.identify = Mock()
|
|
1323
|
+
mock_client.flush = Mock()
|
|
1324
|
+
mock_client.close = Mock()
|
|
1325
|
+
```
|
|
1326
|
+
|
|
1327
|
+
---
|
|
1328
|
+
|
|
1329
|
+
## Shutdown and Cleanup
|
|
1330
|
+
|
|
1331
|
+
### Graceful Shutdown
|
|
1332
|
+
|
|
1333
|
+
```python
|
|
1334
|
+
import ldclient
|
|
1335
|
+
|
|
1336
|
+
def shutdown():
|
|
1337
|
+
print('Flushing events...')
|
|
1338
|
+
client = ldclient.get()
|
|
1339
|
+
client.flush()
|
|
1340
|
+
|
|
1341
|
+
print('Closing client...')
|
|
1342
|
+
client.close()
|
|
1343
|
+
|
|
1344
|
+
print('Shutdown complete')
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
### Check Initialization Status
|
|
1348
|
+
|
|
1349
|
+
```python
|
|
1350
|
+
import ldclient
|
|
1351
|
+
|
|
1352
|
+
client = ldclient.get()
|
|
1353
|
+
|
|
1354
|
+
if client.is_initialized():
|
|
1355
|
+
print('Client is ready')
|
|
1356
|
+
else:
|
|
1357
|
+
print('Client is not initialized yet')
|
|
1358
|
+
```
|
|
1359
|
+
|
|
1360
|
+
---
|
|
1361
|
+
|
|
1362
|
+
## Complete Flask Example
|
|
1363
|
+
|
|
1364
|
+
```python
|
|
1365
|
+
from flask import Flask, request, jsonify
|
|
1366
|
+
import ldclient
|
|
1367
|
+
from ldclient.config import Config
|
|
1368
|
+
from ldclient import Context
|
|
1369
|
+
import os
|
|
1370
|
+
import signal
|
|
1371
|
+
import sys
|
|
1372
|
+
|
|
1373
|
+
app = Flask(__name__)
|
|
1374
|
+
|
|
1375
|
+
# Initialize LaunchDarkly
|
|
1376
|
+
def init_launchdarkly():
|
|
1377
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1378
|
+
ldclient.set_config(config)
|
|
1379
|
+
|
|
1380
|
+
client = ldclient.get()
|
|
1381
|
+
if not client.wait_for_initialization(10):
|
|
1382
|
+
print('LaunchDarkly initialization timeout')
|
|
1383
|
+
else:
|
|
1384
|
+
print('LaunchDarkly initialized')
|
|
1385
|
+
|
|
1386
|
+
return client
|
|
1387
|
+
|
|
1388
|
+
# Route using feature flag
|
|
1389
|
+
@app.route('/feature')
|
|
1390
|
+
def feature():
|
|
1391
|
+
client = ldclient.get()
|
|
1392
|
+
|
|
1393
|
+
user_id = request.headers.get('X-User-ID', 'anonymous')
|
|
1394
|
+
user_email = request.headers.get('X-User-Email')
|
|
1395
|
+
|
|
1396
|
+
context = Context.builder(user_id) \
|
|
1397
|
+
.set('email', user_email) \
|
|
1398
|
+
.build()
|
|
1399
|
+
|
|
1400
|
+
show_new_feature = client.variation('new-feature-flag', context, False)
|
|
1401
|
+
|
|
1402
|
+
if show_new_feature:
|
|
1403
|
+
return jsonify({
|
|
1404
|
+
'feature': 'new',
|
|
1405
|
+
'message': 'Welcome to the new feature!'
|
|
1406
|
+
})
|
|
1407
|
+
else:
|
|
1408
|
+
return jsonify({
|
|
1409
|
+
'feature': 'old',
|
|
1410
|
+
'message': 'Using legacy feature'
|
|
1411
|
+
})
|
|
1412
|
+
|
|
1413
|
+
# Track custom event
|
|
1414
|
+
@app.route('/track-event', methods=['POST'])
|
|
1415
|
+
def track_event():
|
|
1416
|
+
client = ldclient.get()
|
|
1417
|
+
|
|
1418
|
+
data = request.get_json()
|
|
1419
|
+
user_id = data.get('userId')
|
|
1420
|
+
|
|
1421
|
+
context = Context.create(user_id)
|
|
1422
|
+
|
|
1423
|
+
client.track('button-clicked', context, data={
|
|
1424
|
+
'button_id': data.get('buttonId')
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
return jsonify({'success': True})
|
|
1428
|
+
|
|
1429
|
+
# Graceful shutdown
|
|
1430
|
+
def shutdown_handler(signum, frame):
|
|
1431
|
+
print('Shutting down...')
|
|
1432
|
+
client = ldclient.get()
|
|
1433
|
+
client.flush()
|
|
1434
|
+
client.close()
|
|
1435
|
+
sys.exit(0)
|
|
1436
|
+
|
|
1437
|
+
signal.signal(signal.SIGTERM, shutdown_handler)
|
|
1438
|
+
signal.signal(signal.SIGINT, shutdown_handler)
|
|
1439
|
+
|
|
1440
|
+
if __name__ == '__main__':
|
|
1441
|
+
init_launchdarkly()
|
|
1442
|
+
app.run(port=5000)
|
|
1443
|
+
```
|
|
1444
|
+
|
|
1445
|
+
---
|
|
1446
|
+
|
|
1447
|
+
## Complete Django Example
|
|
1448
|
+
|
|
1449
|
+
```python
|
|
1450
|
+
# settings.py
|
|
1451
|
+
import ldclient
|
|
1452
|
+
from ldclient.config import Config
|
|
1453
|
+
import os
|
|
1454
|
+
|
|
1455
|
+
# Initialize LaunchDarkly
|
|
1456
|
+
def init_launchdarkly():
|
|
1457
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1458
|
+
ldclient.set_config(config)
|
|
1459
|
+
|
|
1460
|
+
client = ldclient.get()
|
|
1461
|
+
if client.wait_for_initialization(10):
|
|
1462
|
+
print('LaunchDarkly initialized')
|
|
1463
|
+
|
|
1464
|
+
return client
|
|
1465
|
+
|
|
1466
|
+
LAUNCHDARKLY_CLIENT = init_launchdarkly()
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
# views.py
|
|
1470
|
+
from django.http import JsonResponse
|
|
1471
|
+
from ldclient import Context
|
|
1472
|
+
import ldclient
|
|
1473
|
+
|
|
1474
|
+
def feature_view(request):
|
|
1475
|
+
client = ldclient.get()
|
|
1476
|
+
|
|
1477
|
+
user_id = request.META.get('HTTP_X_USER_ID', 'anonymous')
|
|
1478
|
+
user_email = request.META.get('HTTP_X_USER_EMAIL')
|
|
1479
|
+
|
|
1480
|
+
context = Context.builder(user_id) \
|
|
1481
|
+
.set('email', user_email) \
|
|
1482
|
+
.build()
|
|
1483
|
+
|
|
1484
|
+
show_new_feature = client.variation('new-feature-flag', context, False)
|
|
1485
|
+
|
|
1486
|
+
if show_new_feature:
|
|
1487
|
+
return JsonResponse({
|
|
1488
|
+
'feature': 'new',
|
|
1489
|
+
'message': 'Welcome to the new feature!'
|
|
1490
|
+
})
|
|
1491
|
+
else:
|
|
1492
|
+
return JsonResponse({
|
|
1493
|
+
'feature': 'old',
|
|
1494
|
+
'message': 'Using legacy feature'
|
|
1495
|
+
})
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
# apps.py (for cleanup on shutdown)
|
|
1499
|
+
from django.apps import AppConfig
|
|
1500
|
+
import ldclient
|
|
1501
|
+
|
|
1502
|
+
class MyAppConfig(AppConfig):
|
|
1503
|
+
name = 'myapp'
|
|
1504
|
+
|
|
1505
|
+
def ready(self):
|
|
1506
|
+
import signal
|
|
1507
|
+
import sys
|
|
1508
|
+
|
|
1509
|
+
def shutdown_handler(signum, frame):
|
|
1510
|
+
client = ldclient.get()
|
|
1511
|
+
client.flush()
|
|
1512
|
+
client.close()
|
|
1513
|
+
sys.exit(0)
|
|
1514
|
+
|
|
1515
|
+
signal.signal(signal.SIGTERM, shutdown_handler)
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
---
|
|
1519
|
+
|
|
1520
|
+
## Complete FastAPI Example
|
|
1521
|
+
|
|
1522
|
+
```python
|
|
1523
|
+
from fastapi import FastAPI, Header
|
|
1524
|
+
from pydantic import BaseModel
|
|
1525
|
+
import ldclient
|
|
1526
|
+
from ldclient.config import Config
|
|
1527
|
+
from ldclient import Context
|
|
1528
|
+
import os
|
|
1529
|
+
|
|
1530
|
+
app = FastAPI()
|
|
1531
|
+
|
|
1532
|
+
# Initialize LaunchDarkly
|
|
1533
|
+
@app.on_event("startup")
|
|
1534
|
+
async def startup_event():
|
|
1535
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1536
|
+
ldclient.set_config(config)
|
|
1537
|
+
|
|
1538
|
+
client = ldclient.get()
|
|
1539
|
+
if not client.wait_for_initialization(10):
|
|
1540
|
+
print('LaunchDarkly initialization timeout')
|
|
1541
|
+
else:
|
|
1542
|
+
print('LaunchDarkly initialized')
|
|
1543
|
+
|
|
1544
|
+
# Shutdown
|
|
1545
|
+
@app.on_event("shutdown")
|
|
1546
|
+
async def shutdown_event():
|
|
1547
|
+
client = ldclient.get()
|
|
1548
|
+
client.flush()
|
|
1549
|
+
client.close()
|
|
1550
|
+
|
|
1551
|
+
# Route using feature flag
|
|
1552
|
+
@app.get("/feature")
|
|
1553
|
+
async def get_feature(
|
|
1554
|
+
x_user_id: str = Header(default='anonymous'),
|
|
1555
|
+
x_user_email: str = Header(default=None)
|
|
1556
|
+
):
|
|
1557
|
+
client = ldclient.get()
|
|
1558
|
+
|
|
1559
|
+
context = Context.builder(x_user_id) \
|
|
1560
|
+
.set('email', x_user_email) \
|
|
1561
|
+
.build()
|
|
1562
|
+
|
|
1563
|
+
show_new_feature = client.variation('new-feature-flag', context, False)
|
|
1564
|
+
|
|
1565
|
+
if show_new_feature:
|
|
1566
|
+
return {
|
|
1567
|
+
'feature': 'new',
|
|
1568
|
+
'message': 'Welcome to the new feature!'
|
|
1569
|
+
}
|
|
1570
|
+
else:
|
|
1571
|
+
return {
|
|
1572
|
+
'feature': 'old',
|
|
1573
|
+
'message': 'Using legacy feature'
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
# Track custom event
|
|
1577
|
+
class TrackEventRequest(BaseModel):
|
|
1578
|
+
userId: str
|
|
1579
|
+
buttonId: str
|
|
1580
|
+
|
|
1581
|
+
@app.post("/track-event")
|
|
1582
|
+
async def track_event(event: TrackEventRequest):
|
|
1583
|
+
client = ldclient.get()
|
|
1584
|
+
|
|
1585
|
+
context = Context.create(event.userId)
|
|
1586
|
+
|
|
1587
|
+
client.track('button-clicked', context, data={
|
|
1588
|
+
'button_id': event.buttonId
|
|
1589
|
+
})
|
|
1590
|
+
|
|
1591
|
+
return {'success': True}
|
|
1592
|
+
```
|
|
1593
|
+
|
|
1594
|
+
---
|
|
1595
|
+
|
|
1596
|
+
## OpenTelemetry Integration
|
|
1597
|
+
|
|
1598
|
+
### Setup OpenTelemetry with LaunchDarkly
|
|
1599
|
+
|
|
1600
|
+
```python
|
|
1601
|
+
import ldclient
|
|
1602
|
+
from ldclient.config import Config
|
|
1603
|
+
from ldobserve import ObservabilityPlugin
|
|
1604
|
+
from opentelemetry import metrics
|
|
1605
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
1606
|
+
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
|
1607
|
+
from opentelemetry.exporter.prometheus import PrometheusMetricReader
|
|
1608
|
+
import os
|
|
1609
|
+
|
|
1610
|
+
# Set up OpenTelemetry
|
|
1611
|
+
prometheus_reader = PrometheusMetricReader()
|
|
1612
|
+
meter_provider = MeterProvider(metric_readers=[prometheus_reader])
|
|
1613
|
+
metrics.set_meter_provider(meter_provider)
|
|
1614
|
+
|
|
1615
|
+
# Initialize LaunchDarkly with observability
|
|
1616
|
+
config = Config(
|
|
1617
|
+
sdk_key=os.environ['LAUNCHDARKLY_SDK_KEY'],
|
|
1618
|
+
plugins=[ObservabilityPlugin()]
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
ldclient.set_config(config)
|
|
1622
|
+
client = ldclient.get()
|
|
1623
|
+
```
|
|
1624
|
+
|
|
1625
|
+
---
|
|
1626
|
+
|
|
1627
|
+
## Error Handling
|
|
1628
|
+
|
|
1629
|
+
### Handle Initialization Errors
|
|
1630
|
+
|
|
1631
|
+
```python
|
|
1632
|
+
import ldclient
|
|
1633
|
+
from ldclient.config import Config
|
|
1634
|
+
import os
|
|
1635
|
+
|
|
1636
|
+
try:
|
|
1637
|
+
config = Config(os.environ['LAUNCHDARKLY_SDK_KEY'])
|
|
1638
|
+
ldclient.set_config(config)
|
|
1639
|
+
client = ldclient.get()
|
|
1640
|
+
|
|
1641
|
+
if not client.wait_for_initialization(10):
|
|
1642
|
+
print('Initialization timeout - using default values')
|
|
1643
|
+
except Exception as e:
|
|
1644
|
+
print(f'LaunchDarkly initialization error: {e}')
|
|
1645
|
+
```
|
|
1646
|
+
|
|
1647
|
+
### Handle Evaluation Errors
|
|
1648
|
+
|
|
1649
|
+
```python
|
|
1650
|
+
import ldclient
|
|
1651
|
+
from ldclient import Context
|
|
1652
|
+
|
|
1653
|
+
client = ldclient.get()
|
|
1654
|
+
context = Context.create('user-key-123abc')
|
|
1655
|
+
|
|
1656
|
+
result = client.variation_detail('flag-key', context, False)
|
|
1657
|
+
|
|
1658
|
+
if result.reason['kind'] == 'ERROR':
|
|
1659
|
+
error_kind = result.reason['errorKind']
|
|
1660
|
+
|
|
1661
|
+
if error_kind == 'MALFORMED_FLAG':
|
|
1662
|
+
print('Flag configuration is invalid')
|
|
1663
|
+
elif error_kind == 'FLAG_NOT_FOUND':
|
|
1664
|
+
print('Flag does not exist')
|
|
1665
|
+
elif error_kind == 'USER_NOT_SPECIFIED':
|
|
1666
|
+
print('Context is invalid')
|
|
1667
|
+
elif error_kind == 'WRONG_TYPE':
|
|
1668
|
+
print('Flag type mismatch')
|
|
1669
|
+
else:
|
|
1670
|
+
print(f'Unknown error: {error_kind}')
|
|
1671
|
+
```
|