@variantlab/core 0.1.4 → 0.1.6

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.
@@ -0,0 +1,487 @@
1
+ # Value experiments
2
+
3
+ How to use variantlab as a feature-flag / remote-config tool. Value experiments return values, not components.
4
+
5
+ ## Table of contents
6
+
7
+ - [The two experiment types](#the-two-experiment-types)
8
+ - [When to use value experiments](#when-to-use-value-experiments)
9
+ - [Defining a value experiment](#defining-a-value-experiment)
10
+ - [Reading the value](#reading-the-value)
11
+ - [Type inference](#type-inference)
12
+ - [Supported value types](#supported-value-types)
13
+ - [Feature flag patterns](#feature-flag-patterns)
14
+ - [Remote config patterns](#remote-config-patterns)
15
+ - [Gotchas](#gotchas)
16
+
17
+ ---
18
+
19
+ ## The two experiment types
20
+
21
+ variantlab supports two kinds of experiments:
22
+
23
+ | Type | Purpose | Hook | Component |
24
+ |---|---|---|---|
25
+ | `"render"` | Swap components | `useVariant` | `<Variant>` |
26
+ | `"value"` | Return values | `useVariantValue` | `<VariantValue>` |
27
+
28
+ They share the same config format, targeting, rollback, and debug UX. The only difference is the `type` field and whether variants carry a `value`.
29
+
30
+ ---
31
+
32
+ ## When to use value experiments
33
+
34
+ Use `type: "value"` when:
35
+
36
+ - The experiment changes **data**, not structure (copy, colors, numbers, URLs, feature flags)
37
+ - You want to read the value in multiple places without duplicating render logic
38
+ - The variants are primitives, arrays, or JSON objects
39
+ - You want a classic "feature flag" API
40
+
41
+ Use `type: "render"` when:
42
+
43
+ - The experiment changes **components** (different layouts, different UIs)
44
+ - You want exhaustive type checking on variant → component mapping
45
+ - Each variant has its own JSX tree
46
+
47
+ ---
48
+
49
+ ## Defining a value experiment
50
+
51
+ ```json
52
+ {
53
+ "id": "cta-copy",
54
+ "type": "value",
55
+ "default": "buy-now",
56
+ "variants": [
57
+ { "id": "buy-now", "value": "Buy now" },
58
+ { "id": "get-started", "value": "Get started" },
59
+ { "id": "try-free", "value": "Try it free" }
60
+ ]
61
+ }
62
+ ```
63
+
64
+ - `type: "value"` is required (or it will be treated as a render experiment)
65
+ - Each variant MUST have a `value` field
66
+ - `value` can be any JSON-serializable type (string, number, boolean, array, object, null)
67
+ - All variants should have the same value shape (enforced by codegen, not the runtime)
68
+
69
+ ### A boolean feature flag
70
+
71
+ ```json
72
+ {
73
+ "id": "ai-assistant-enabled",
74
+ "type": "value",
75
+ "default": "off",
76
+ "variants": [
77
+ { "id": "off", "value": false },
78
+ { "id": "on", "value": true }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ ### A number
84
+
85
+ ```json
86
+ {
87
+ "id": "max-articles-per-page",
88
+ "type": "value",
89
+ "default": "default",
90
+ "variants": [
91
+ { "id": "default", "value": 10 },
92
+ { "id": "larger", "value": 20 },
93
+ { "id": "smaller", "value": 5 }
94
+ ]
95
+ }
96
+ ```
97
+
98
+ ### A JSON object
99
+
100
+ ```json
101
+ {
102
+ "id": "homepage-layout-config",
103
+ "type": "value",
104
+ "default": "v1",
105
+ "variants": [
106
+ {
107
+ "id": "v1",
108
+ "value": {
109
+ "heroVisible": true,
110
+ "trendingCount": 10,
111
+ "sidebarAds": false
112
+ }
113
+ },
114
+ {
115
+ "id": "v2",
116
+ "value": {
117
+ "heroVisible": false,
118
+ "trendingCount": 20,
119
+ "sidebarAds": true
120
+ }
121
+ }
122
+ ]
123
+ }
124
+ ```
125
+
126
+ ### An array
127
+
128
+ ```json
129
+ {
130
+ "id": "feature-order",
131
+ "type": "value",
132
+ "default": "default",
133
+ "variants": [
134
+ { "id": "default", "value": ["news", "videos", "opinions"] },
135
+ { "id": "reordered", "value": ["videos", "news", "opinions"] }
136
+ ]
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Reading the value
143
+
144
+ ### Hook
145
+
146
+ ```ts
147
+ import { useVariantValue } from "@variantlab/react";
148
+
149
+ const copy = useVariantValue("cta-copy");
150
+ // With codegen: "Buy now" | "Get started" | "Try it free"
151
+ // Without codegen: string
152
+ ```
153
+
154
+ ### Component
155
+
156
+ ```tsx
157
+ import { VariantValue } from "@variantlab/react";
158
+
159
+ <VariantValue experimentId="cta-copy">
160
+ {(copy) => <Button>{copy}</Button>}
161
+ </VariantValue>
162
+ ```
163
+
164
+ Useful for avoiding hook rules in conditional contexts.
165
+
166
+ ### Server-side
167
+
168
+ ```ts
169
+ import { getVariantValueSSR } from "@variantlab/next";
170
+
171
+ const copy = await getVariantValueSSR("cta-copy", context);
172
+ ```
173
+
174
+ ---
175
+
176
+ ## Type inference
177
+
178
+ With codegen active, the return type is inferred from the config:
179
+
180
+ ```json
181
+ {
182
+ "id": "theme",
183
+ "type": "value",
184
+ "default": "light",
185
+ "variants": [
186
+ { "id": "light", "value": "light" },
187
+ { "id": "dark", "value": "dark" },
188
+ { "id": "auto", "value": "auto" }
189
+ ]
190
+ }
191
+ ```
192
+
193
+ ```ts
194
+ const theme = useVariantValue("theme");
195
+ // theme: "light" | "dark" | "auto"
196
+ ```
197
+
198
+ For JSON objects, the type is a union of the object shapes:
199
+
200
+ ```json
201
+ {
202
+ "id": "pricing",
203
+ "type": "value",
204
+ "variants": [
205
+ { "id": "a", "value": { "price": 9.99, "label": "Basic" } },
206
+ { "id": "b", "value": { "price": 14.99, "label": "Pro" } }
207
+ ]
208
+ }
209
+ ```
210
+
211
+ ```ts
212
+ const pricing = useVariantValue("pricing");
213
+ // pricing: { price: 9.99; label: "Basic" } | { price: 14.99; label: "Pro" }
214
+ // or more usefully: { price: number; label: string }
215
+ ```
216
+
217
+ The codegen emits a union by default; users can widen it manually.
218
+
219
+ ### Without codegen
220
+
221
+ ```ts
222
+ const theme = useVariantValue<"light" | "dark" | "auto">("theme");
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Supported value types
228
+
229
+ All JSON-serializable types:
230
+
231
+ - **string**
232
+ - **number**
233
+ - **boolean**
234
+ - **null**
235
+ - **array** of the above
236
+ - **object** of the above (nested)
237
+
238
+ ### Not supported
239
+
240
+ - **Functions** — configs are JSON
241
+ - **Dates** — pass ISO strings instead
242
+ - **Regex** — pass string patterns, compile in the app
243
+ - **undefined** — use `null` or omit the value
244
+ - **Symbols**, **BigInt**, **Maps**, **Sets** — not JSON-compatible
245
+
246
+ If you need a non-serializable value, read a serializable key and map it in the app:
247
+
248
+ ```ts
249
+ const themeName = useVariantValue("theme");
250
+ const themeColors = themeMap[themeName];
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Feature flag patterns
256
+
257
+ variantlab as a feature flag system:
258
+
259
+ ### Boolean flag
260
+
261
+ ```json
262
+ {
263
+ "id": "ai-tab-enabled",
264
+ "type": "value",
265
+ "default": "off",
266
+ "variants": [
267
+ { "id": "off", "value": false },
268
+ { "id": "on", "value": true }
269
+ ]
270
+ }
271
+ ```
272
+
273
+ ```tsx
274
+ const enabled = useVariantValue("ai-tab-enabled");
275
+ return enabled ? <AITab /> : null;
276
+ ```
277
+
278
+ ### Staged rollout
279
+
280
+ ```json
281
+ {
282
+ "id": "new-checkout",
283
+ "type": "value",
284
+ "assignment": "weighted",
285
+ "split": { "off": 90, "on": 10 },
286
+ "default": "off",
287
+ "variants": [
288
+ { "id": "off", "value": false },
289
+ { "id": "on", "value": true }
290
+ ]
291
+ }
292
+ ```
293
+
294
+ Start at 10%, increase the `split` over time:
295
+
296
+ ```json
297
+ "split": { "off": 50, "on": 50 }
298
+ ```
299
+
300
+ ```json
301
+ "split": { "off": 0, "on": 100 }
302
+ ```
303
+
304
+ ### Kill switch
305
+
306
+ Set `enabled: false` at the top level to instantly disable all experiments:
307
+
308
+ ```json
309
+ {
310
+ "version": 1,
311
+ "enabled": false,
312
+ "experiments": [...]
313
+ }
314
+ ```
315
+
316
+ Or archive a specific one:
317
+
318
+ ```json
319
+ { "id": "new-checkout", "status": "archived", ... }
320
+ ```
321
+
322
+ Archived experiments always return their default.
323
+
324
+ ### Targeted beta
325
+
326
+ ```json
327
+ {
328
+ "id": "ai-assistant-beta",
329
+ "type": "value",
330
+ "default": "off",
331
+ "targeting": { "attributes": { "betaOptIn": true } },
332
+ "variants": [
333
+ { "id": "off", "value": false },
334
+ { "id": "on", "value": true }
335
+ ]
336
+ }
337
+ ```
338
+
339
+ Only users with `betaOptIn: true` see the feature.
340
+
341
+ ---
342
+
343
+ ## Remote config patterns
344
+
345
+ variantlab as a remote-config tool:
346
+
347
+ ### Dynamic text
348
+
349
+ ```json
350
+ {
351
+ "id": "welcome-banner-text",
352
+ "type": "value",
353
+ "default": "default",
354
+ "variants": [
355
+ { "id": "default", "value": "Welcome to Drishtikon" },
356
+ { "id": "holiday", "value": "Happy New Year!" }
357
+ ]
358
+ }
359
+ ```
360
+
361
+ ### Dynamic URLs
362
+
363
+ ```json
364
+ {
365
+ "id": "api-endpoint",
366
+ "type": "value",
367
+ "default": "prod",
368
+ "variants": [
369
+ { "id": "prod", "value": "https://api.example.com" },
370
+ { "id": "staging", "value": "https://staging.example.com" }
371
+ ]
372
+ }
373
+ ```
374
+
375
+ ### Dynamic pricing
376
+
377
+ ```json
378
+ {
379
+ "id": "premium-price",
380
+ "type": "value",
381
+ "assignment": "weighted",
382
+ "split": { "low": 25, "mid": 50, "high": 25 },
383
+ "default": "mid",
384
+ "variants": [
385
+ { "id": "low", "value": 4.99 },
386
+ { "id": "mid", "value": 9.99 },
387
+ { "id": "high", "value": 14.99 }
388
+ ]
389
+ }
390
+ ```
391
+
392
+ ### Time-boxed promotions
393
+
394
+ ```json
395
+ {
396
+ "id": "black-friday-discount",
397
+ "type": "value",
398
+ "startDate": "2026-11-24T00:00:00Z",
399
+ "endDate": "2026-12-01T00:00:00Z",
400
+ "default": "off",
401
+ "variants": [
402
+ { "id": "off", "value": 0 },
403
+ { "id": "on", "value": 0.25 }
404
+ ]
405
+ }
406
+ ```
407
+
408
+ ---
409
+
410
+ ## Gotchas
411
+
412
+ ### Value experiments don't render exhaustive
413
+
414
+ If you use `useVariantValue` + a switch statement, you can miss cases. The type system will not catch it unless you explicitly narrow:
415
+
416
+ ```ts
417
+ const theme = useVariantValue("theme");
418
+ if (theme === "light") return <Light />;
419
+ if (theme === "dark") return <Dark />;
420
+ // forgot "auto" — no compile error unless you use exhaustive checks
421
+ ```
422
+
423
+ For exhaustiveness, use `<Variant>` with `type: "render"` instead:
424
+
425
+ ```tsx
426
+ <Variant experimentId="theme">
427
+ {{
428
+ light: <Light />,
429
+ dark: <Dark />,
430
+ auto: <Auto />, // ❌ error if missing
431
+ }}
432
+ </Variant>
433
+ ```
434
+
435
+ ### Mixing types across variants
436
+
437
+ The runtime doesn't enforce that all variants have the same value shape. You can do this:
438
+
439
+ ```json
440
+ "variants": [
441
+ { "id": "a", "value": "string" },
442
+ { "id": "b", "value": 42 },
443
+ { "id": "c", "value": true }
444
+ ]
445
+ ```
446
+
447
+ But it's a bad idea — your consumer code will have to handle `string | number | boolean` everywhere. Codegen will flag this with a linter warning.
448
+
449
+ ### Default value must be in the variants list
450
+
451
+ ```json
452
+ "default": "premium", // ❌ error if "premium" is not a variant ID
453
+ "variants": [
454
+ { "id": "basic", "value": "..." },
455
+ { "id": "pro", "value": "..." }
456
+ ]
457
+ ```
458
+
459
+ The validator catches this at config load time.
460
+
461
+ ### Don't store sensitive data
462
+
463
+ Variant values are visible in the debug overlay, in network requests (if served remotely), and in the app bundle (if embedded). Don't put API keys, tokens, or secrets in variant values. Use environment variables for those.
464
+
465
+ ---
466
+
467
+ ## Comparison with other tools
468
+
469
+ | Tool | Feature flags | Multi-value | JSON values | Codegen |
470
+ |---|:-:|:-:|:-:|:-:|
471
+ | variantlab | ✅ | ✅ | ✅ | ✅ |
472
+ | Firebase Remote Config | ✅ | ⚠️ | ⚠️ | ❌ |
473
+ | LaunchDarkly | ✅ | ✅ | ✅ | ⚠️ enterprise |
474
+ | Statsig | ✅ | ✅ | ✅ | ⚠️ |
475
+ | GrowthBook | ✅ | ✅ | ✅ | ❌ |
476
+ | Unleash | ✅ | ❌ | ❌ | ❌ |
477
+ | ConfigCat | ✅ | ⚠️ | ⚠️ | ❌ |
478
+
479
+ variantlab is the only tool that combines full value-experiment support with local codegen and zero network dependency.
480
+
481
+ ---
482
+
483
+ ## See also
484
+
485
+ - [`killer-features.md`](./killer-features.md#4-value-and-render-experiments) — the differentiator
486
+ - [`multivariate.md`](./multivariate.md) — 3+ variant experiments
487
+ - [`codegen.md`](./codegen.md) — type-safe values