@picked/client 0.0.1-dev.1785287094311 → 0.0.1-dev.1785826066660

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.
Files changed (209) hide show
  1. package/dist/.tsbuildinfo +1 -0
  2. package/dist/api-Djuh2gfe.cjs +1 -0
  3. package/dist/api-d0qKmzPH.js +38 -0
  4. package/dist/cjs/form/index.js +1 -1
  5. package/dist/cjs/index.js +2 -2
  6. package/dist/cjs/plugins/dom-observer/index.js +1 -1
  7. package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
  8. package/dist/cjs/preview/index.js +1 -0
  9. package/dist/cjs/server/index.js +1 -1
  10. package/dist/docs/actions.mdx +38 -18
  11. package/dist/docs/api/authentication.md +75 -48
  12. package/dist/docs/api/authorization-and-scopes.md +63 -47
  13. package/dist/docs/api/error-handling.md +70 -54
  14. package/dist/docs/api/index.md +41 -22
  15. package/dist/docs/api/pagination.md +30 -20
  16. package/dist/docs/api-keys.mdx +11 -6
  17. package/dist/docs/chrome-extension.md +22 -5
  18. package/dist/docs/conditions.mdx +32 -19
  19. package/dist/docs/content/asset-library.mdx +65 -34
  20. package/dist/docs/content/content-entries.mdx +8 -2
  21. package/dist/docs/content/layouts.mdx +45 -29
  22. package/dist/docs/content/pages.mdx +29 -14
  23. package/dist/docs/content/sections.mdx +19 -8
  24. package/dist/docs/delivery-api/experience-content.md +10 -5
  25. package/dist/docs/delivery-api/index.md +19 -12
  26. package/dist/docs/delivery-api/preview-caching-crawling.md +27 -14
  27. package/dist/docs/delivery-api/project-config.md +6 -4
  28. package/dist/docs/experiences.mdx +26 -10
  29. package/dist/docs/features.mdx +93 -22
  30. package/dist/docs/fields.mdx +42 -27
  31. package/dist/docs/flow-builder/building-flows.md +38 -20
  32. package/dist/docs/flow-builder/condition-builder.md +32 -21
  33. package/dist/docs/flow-builder/keyboard-shortcuts.md +18 -18
  34. package/dist/docs/flow-builder/nodes.md +26 -13
  35. package/dist/docs/flow-builder/overview.mdx +28 -15
  36. package/dist/docs/gtm-template.md +35 -26
  37. package/dist/docs/guides/build-a-personalized-page.mdx +36 -18
  38. package/dist/docs/image-optimisation-service.md +33 -17
  39. package/dist/docs/index.md +28 -17
  40. package/dist/docs/mcp.md +47 -39
  41. package/dist/docs/pickedql/data-sources.md +65 -43
  42. package/dist/docs/pickedql/dimensions.md +27 -12
  43. package/dist/docs/pickedql/examples.md +38 -26
  44. package/dist/docs/pickedql/filtering.md +10 -6
  45. package/dist/docs/pickedql/index.md +32 -14
  46. package/dist/docs/pickedql/schema.md +85 -76
  47. package/dist/docs/pickedql/virtual-functions.md +62 -36
  48. package/dist/docs/projects.mdx +14 -7
  49. package/dist/docs/reports.mdx +5 -3
  50. package/dist/docs/sdk/actions.md +33 -17
  51. package/dist/docs/sdk/conditions.md +8 -5
  52. package/dist/docs/sdk/content-security-policy.md +5 -4
  53. package/dist/docs/sdk/experiences.md +29 -11
  54. package/dist/docs/sdk/feature-allocation.md +161 -0
  55. package/dist/docs/sdk/features.md +181 -73
  56. package/dist/docs/sdk/fields.md +66 -42
  57. package/dist/docs/sdk/index.md +25 -10
  58. package/dist/docs/sdk/plugins/create-experience.md +18 -15
  59. package/dist/docs/sdk/plugins/dynamic-experiences.md +43 -17
  60. package/dist/docs/sdk/predicates.md +53 -37
  61. package/dist/docs/sdk/ssr/index.md +25 -8
  62. package/dist/docs/sdk/ssr/rendering-and-hydration.md +37 -16
  63. package/dist/docs/sdk/ssr/serialize.md +38 -20
  64. package/dist/docs/sdk/ssr/slots-and-placements.md +28 -10
  65. package/dist/docs/sdk/steps.md +100 -55
  66. package/dist/docs/steps.mdx +20 -11
  67. package/dist/docs/team-management.md +17 -11
  68. package/dist/docs/vite-plugin.md +26 -19
  69. package/dist/docs/workspace-versioning.mdx +14 -8
  70. package/dist/docs/workspaces.mdx +55 -24
  71. package/dist/{dom-BwmTHeT7.js → dom-BqcToNL0.js} +1 -3
  72. package/dist/{dom-CDrHc_j9.cjs → dom-DlLH3FxV.cjs} +1 -1
  73. package/dist/esm/form/index.js +2 -4
  74. package/dist/esm/index.js +434 -334
  75. package/dist/esm/plugins/dom-observer/index.js +1 -1
  76. package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
  77. package/dist/esm/preview/index.js +2 -0
  78. package/dist/esm/server/index.js +13 -11
  79. package/dist/{experience-instance-DOJShRk5.js → experience-instance-DGk11d8a.js} +3 -3
  80. package/dist/experience-instance-Dej3P1sE.cjs +1 -0
  81. package/dist/inert-script-report-B_bMPRzp.js +18 -0
  82. package/dist/inert-script-report-ijRHPtcY.cjs +1 -0
  83. package/dist/{plugin-C46DEiGE.js → plugin-BVAFWjpT.js} +61 -56
  84. package/dist/plugin-BcCn2llk.cjs +1 -0
  85. package/dist/preview-channel-CSr96XAo.js +98 -0
  86. package/dist/preview-channel-Cx0e6fye.cjs +1 -0
  87. package/dist/protocol-Bb_vpLKE.cjs +1 -0
  88. package/dist/protocol-C5KkkiZc.js +84 -0
  89. package/dist/types/client.d.ts +7 -6
  90. package/dist/types/entities/action/action.d.ts +5 -4
  91. package/dist/types/entities/action/action.d.ts.map +1 -1
  92. package/dist/types/entities/component/component.d.ts +6 -5
  93. package/dist/types/entities/component/component.d.ts.map +1 -1
  94. package/dist/types/entities/condition/condition.d.ts +5 -5
  95. package/dist/types/entities/content/content.d.ts +14 -3
  96. package/dist/types/entities/content/content.d.ts.map +1 -1
  97. package/dist/types/entities/content/reference.d.ts +101 -0
  98. package/dist/types/entities/content/reference.d.ts.map +1 -0
  99. package/dist/types/entities/experience/experience.d.ts +11 -10
  100. package/dist/types/entities/experience/experience.d.ts.map +1 -1
  101. package/dist/types/entities/experience-instance/experience-instance.d.ts +10 -9
  102. package/dist/types/entities/feature/feature.d.ts +3 -2
  103. package/dist/types/entities/feature/feature.d.ts.map +1 -1
  104. package/dist/types/entities/field/field.d.ts +7 -6
  105. package/dist/types/entities/framework/framework.d.ts +7 -6
  106. package/dist/types/entities/framework/framework.d.ts.map +1 -1
  107. package/dist/types/entities/history/history.d.ts +6 -6
  108. package/dist/types/entities/plugin/plugin.d.ts +3 -2
  109. package/dist/types/entities/plugin/plugin.d.ts.map +1 -1
  110. package/dist/types/entities/predicate/predicate.d.ts +4 -3
  111. package/dist/types/entities/predicate/predicate.d.ts.map +1 -1
  112. package/dist/types/entities/response/response-validation.d.ts +3 -2
  113. package/dist/types/entities/response/response.d.ts +12 -11
  114. package/dist/types/entities/session/session.d.ts +2 -1
  115. package/dist/types/entities/step/step.d.ts +6 -5
  116. package/dist/types/exports.d.ts +6 -1
  117. package/dist/types/exports.d.ts.map +1 -1
  118. package/dist/types/form/build-responses.d.ts +2 -1
  119. package/dist/types/form/create-field-evaluator.d.ts +4 -3
  120. package/dist/types/form/create-field-validation.d.ts +5 -4
  121. package/dist/types/form/create-form.d.ts +7 -6
  122. package/dist/types/form/create-submit-pipeline.d.ts +7 -6
  123. package/dist/types/form/get-step-fields.d.ts +3 -2
  124. package/dist/types/index.d.ts +1 -0
  125. package/dist/types/index.d.ts.map +1 -1
  126. package/dist/types/lifecycle/condition-selection.d.ts +14 -4
  127. package/dist/types/lifecycle/lifecycle.d.ts +4 -3
  128. package/dist/types/lifecycle/rules-logic.d.ts +4 -3
  129. package/dist/types/lifecycle/state-snapshot.d.ts +4 -3
  130. package/dist/types/picked.d.ts +102 -68
  131. package/dist/types/picked.d.ts.map +1 -1
  132. package/dist/types/ping.d.ts +1 -1
  133. package/dist/types/ping.d.ts.map +1 -1
  134. package/dist/types/plugins/create-experience/create-experience.d.ts +9 -8
  135. package/dist/types/plugins/create-experience/create-experience.d.ts.map +1 -1
  136. package/dist/types/plugins/create-experience/plugin.d.ts +3 -2
  137. package/dist/types/plugins/create-experience/plugin.d.ts.map +1 -1
  138. package/dist/types/plugins/create-experience/serialization.d.ts.map +1 -1
  139. package/dist/types/plugins/dom-observer/plugin.d.ts +1 -1
  140. package/dist/types/plugins/dynamic-experiences/hash.d.ts +40 -5
  141. package/dist/types/plugins/dynamic-experiences/hash.d.ts.map +1 -1
  142. package/dist/types/plugins/dynamic-experiences/plugin.d.ts +3 -3
  143. package/dist/types/plugins/dynamic-experiences/plugin.d.ts.map +1 -1
  144. package/dist/types/plugins/dynamic-experiences/types.d.ts +24 -5
  145. package/dist/types/plugins/dynamic-experiences/types.d.ts.map +1 -1
  146. package/dist/types/plugins/experience-browser-storage/plugin.d.ts +1 -1
  147. package/dist/types/plugins/experience-browser-storage/plugin.d.ts.map +1 -1
  148. package/dist/types/plugins/history-browser-storage/plugin.d.ts +1 -1
  149. package/dist/types/plugins/history-browser-storage/plugin.d.ts.map +1 -1
  150. package/dist/types/plugins/responses-browser-storage/plugin.d.ts +1 -1
  151. package/dist/types/plugins/responses-browser-storage/plugin.d.ts.map +1 -1
  152. package/dist/types/plugins/responses-indexeddb/plugin.d.ts +1 -1
  153. package/dist/types/plugins/single-page-app/plugin.d.ts +1 -1
  154. package/dist/types/plugins/single-page-app/plugin.d.ts.map +1 -1
  155. package/dist/types/plugins/web-components/plugin.d.ts +2 -1
  156. package/dist/types/plugins/window-object/plugin.d.ts +2 -2
  157. package/dist/types/preview/index.d.ts +18 -0
  158. package/dist/types/preview/index.d.ts.map +1 -0
  159. package/dist/types/preview-channel.d.ts +16 -0
  160. package/dist/types/preview-channel.d.ts.map +1 -0
  161. package/dist/types/preview-receiver/apply-ops.d.ts +32 -0
  162. package/dist/types/preview-receiver/apply-ops.d.ts.map +1 -0
  163. package/dist/types/preview-receiver/index.d.ts +32 -0
  164. package/dist/types/preview-receiver/index.d.ts.map +1 -0
  165. package/dist/types/preview-receiver/protocol.d.ts +69 -0
  166. package/dist/types/preview-receiver/protocol.d.ts.map +1 -0
  167. package/dist/types/server/index.d.ts +3 -3
  168. package/dist/types/server/serialize.d.ts +8 -3
  169. package/dist/types/server/serialize.d.ts.map +1 -1
  170. package/dist/types/utils/content-path.d.ts +37 -0
  171. package/dist/types/utils/content-path.d.ts.map +1 -0
  172. package/dist/types/utils/dom.d.ts +2 -1
  173. package/dist/types/utils/events.d.ts +6 -6
  174. package/dist/types/utils/events.d.ts.map +1 -1
  175. package/dist/types/utils/global-obj.d.ts +1 -1
  176. package/dist/types/utils/global-obj.d.ts.map +1 -1
  177. package/dist/types/utils/id.d.ts.map +1 -1
  178. package/dist/types/utils/inert-script-report.d.ts +40 -0
  179. package/dist/types/utils/inert-script-report.d.ts.map +1 -0
  180. package/dist/types/utils/logger.d.ts +2 -1
  181. package/dist/types/utils/logger.d.ts.map +1 -1
  182. package/dist/types/utils/node-tree/get-next-node-from-action.d.ts +2 -1
  183. package/dist/types/utils/node-tree/get-next-node-from-condition.d.ts +2 -1
  184. package/dist/types/utils/node-tree/get-next-node.d.ts +1 -1
  185. package/dist/types/utils/node-tree/get-next-node.d.ts.map +1 -1
  186. package/dist/types/utils/node-tree/get-origin-node.d.ts +1 -1
  187. package/dist/types/utils/node-tree/get-origin-node.d.ts.map +1 -1
  188. package/dist/types/utils/node-tree/min-max-steps-to-leaf.d.ts +1 -1
  189. package/dist/types/utils/node-tree/min-max-steps-to-leaf.d.ts.map +1 -1
  190. package/dist/types/utils/node-tree/min-max-steps-to-origin.d.ts +1 -1
  191. package/dist/types/utils/node-tree/min-max-steps-to-origin.d.ts.map +1 -1
  192. package/dist/types/utils/node-tree/next-reachable-step-nodes.d.ts +1 -1
  193. package/dist/types/utils/node-tree/next-reachable-step-nodes.d.ts.map +1 -1
  194. package/dist/types/utils/nodes.d.ts +1 -1
  195. package/dist/types/utils/picked-config.d.ts +2 -1
  196. package/dist/types/utils/preview-session.d.ts +12 -0
  197. package/dist/types/utils/preview-session.d.ts.map +1 -0
  198. package/dist/types/utils/rehydrate-scripts.d.ts +6 -0
  199. package/dist/types/utils/rehydrate-scripts.d.ts.map +1 -1
  200. package/dist/types/utils/rules-logic.d.ts +6 -2
  201. package/dist/types/utils/storage.d.ts +1 -1
  202. package/dist/types/utils/storage.d.ts.map +1 -1
  203. package/package.json +35 -24
  204. package/dist/api-BzcdC_7V.cjs +0 -1
  205. package/dist/api-o8w9kJwm.js +0 -40
  206. package/dist/experience-instance-DurC3pE-.cjs +0 -1
  207. package/dist/plugin-nrEJGTuE.cjs +0 -1
  208. package/dist/preview-bridge-CpX1BkSA.cjs +0 -1
  209. package/dist/preview-bridge-D76l-dPy.js +0 -33
@@ -2,13 +2,42 @@
2
2
  layout: docs
3
3
  ---
4
4
 
5
- # Features
5
+ # Feature flags
6
6
 
7
- Define feature flags with a `decide` function that resolves to a bucket value, access results through framework hooks, and invalidate cached decisions by bumping the version.
7
+ A feature flag lets you run an A/B test and roll a change out in stages. Control a
8
+ feature flag from the dashboard, or define it in SDK code when you need your own decision
9
+ rules.
8
10
 
9
- ## Defining features
11
+ ## Where feature flags come from
10
12
 
11
- Define features when creating the Picked client by passing a `features` object. Each feature requires a unique identifier, a versioning key, a human-readable name, and a `decide` function:
13
+ You control most feature flags from the dashboard. The
14
+ [dynamic experiences plugin](/sdk/plugins/dynamic-experiences) reads the options of
15
+ your project at runtime and registers every feature flag for you. You declare no feature flag in
16
+ code.
17
+
18
+ For a feature flag that you control from the dashboard, the SDK makes the definition:
19
+
20
+ | Field | Value |
21
+ | --- | --- |
22
+ | `id` | The dashboard key of the feature flag |
23
+ | `key` | `v{version}`, for example `v3` |
24
+ | `name` | The dashboard name of the feature flag |
25
+ | `enabled` | The dashboard **Enabled** toggle |
26
+ | `weight` | The dashboard **Weight**, 0 to 100 |
27
+ | `storage` | `local`, or `session` in [preview mode](/sdk/feature-allocation#preview-mode) |
28
+ | `decide` | Made by the SDK. It compares the stable value of the visitor for this feature flag against `weight`. |
29
+
30
+ The `decide` function that the SDK makes derives from the visitor ID, the ID of the
31
+ feature flag, and the weight. A feature flag that you control from the dashboard
32
+ therefore cannot use the data of your application. Define the feature flag in SDK code
33
+ when you need that.
34
+
35
+ ## Define a feature flag in SDK code
36
+
37
+ Define your feature flags when you create the Picked client. Pass a `features` object. Use
38
+ this when a decision needs data that only your application holds. Each feature flag needs a
39
+ unique identifier, a version key, a name that a person can read, and a `decide`
40
+ function:
12
41
 
13
42
  ```ts
14
43
  import { createClient } from '@picked/client'
@@ -17,20 +46,20 @@ const client = createClient({
17
46
  projectId: 'your-project-id',
18
47
  features: {
19
48
  'summer-sale': {
20
- key: 'summer-sale-v1', // Version key - change to invalidate cache
21
- name: 'Summer Sale Experiment', // Human-readable name for analytics
49
+ key: 'summer-sale-v1', // The version key. Change it to invalidate the cache.
50
+ name: 'Summer Sale Experiment', // A name for analytics that a person can read.
22
51
  decide: async () => {
23
- // Your decision logic here
52
+ // Put your decision rules here.
24
53
  const isEligible = await checkUserEligibility()
25
- return isEligible // Returns boolean
54
+ return isEligible // Returns a boolean.
26
55
  },
27
- storage: 'local', // Optional: 'local' (default) or 'session'
56
+ storage: 'local', // Optional: 'local' is the default, or 'session'.
28
57
  },
29
58
  'homepage-variant': {
30
59
  key: 'homepage-test-v2',
31
60
  name: 'Homepage A/B Test',
32
61
  decide: async () => {
33
- // Return string for multi-variant tests
62
+ // Return a string for a test of more than two variants.
34
63
  return Math.random() < 0.5 ? 'variant-a' : 'variant-b'
35
64
  },
36
65
  },
@@ -38,7 +67,8 @@ const client = createClient({
38
67
  })
39
68
  ```
40
69
 
41
- Features can also be defined as factory functions for lazy initialization:
70
+ A feature flag can also be a factory function, which starts the feature flag only when it is
71
+ first needed:
42
72
 
43
73
  ```ts
44
74
  features: {
@@ -50,25 +80,35 @@ features: {
50
80
  }
51
81
  ```
52
82
 
53
- ## How features work
83
+ ## How feature flags work
84
+
85
+ After you register a feature flag, the SDK follows this order:
54
86
 
55
- When a feature is registered, the SDK follows this execution order:
87
+ 1. **Registration** — the client registers each feature flag at startup. A registration
88
+ allocates nobody.
89
+ 2. **Cache check** — the SDK looks in browser storage for a decision from an earlier
90
+ visit. The cache key holds the feature flag `id` and `key`. When an entry is there, the
91
+ feature flag starts in a resolved state. This is why Picked allocates a visitor who
92
+ returns before anything reads the feature flag.
93
+ 3. **Lazy evaluation** — with no cached decision, `decide` runs the first time that
94
+ something reads the bucket.
95
+ 4. **Storage** — the SDK writes the resolved bucket to browser storage.
96
+ 5. **Event emission** — the SDK sends a `feature_decide` event, but only for a
97
+ decision that it makes during this page view. A decision that step 2 restores
98
+ sends nothing.
56
99
 
57
- 1. **Registration**: Features are registered to the Picked client during initialization
58
- 2. **Cache Check**: The SDK checks browser storage (localStorage or sessionStorage) for a previously resolved value using the feature `id` and `key`
59
- 3. **Lazy Evaluation**: If no cached value exists, the `decide` function will be called the first time the feature is accessed
60
- 4. **Storage**: Once resolved, the bucket value is persisted to browser storage for consistency across page loads
61
- 5. **Event Emission**: A `feature_decide` event is fired on the event bus when a feature is resolved for the first time
100
+ See [Allocation and exposure](/sdk/feature-allocation) for what this means when you
101
+ track an exposure.
62
102
 
63
- ## URL-based overrides
103
+ ## Overrides in the URL
64
104
 
65
- You can override feature decisions via URL search parameters for testing purposes:
105
+ You can override a feature flag decision from the URL search parameters, for a test:
66
106
 
67
107
  ```
68
108
  https://example.com?picked-feature=my-feature:true
69
109
  ```
70
110
 
71
- Multiple overrides can be specified:
111
+ You can give more than one override value:
72
112
 
73
113
  ```
74
114
  https://example.com?picked-feature=my-feature:true&picked-feature=homepage-variant:variant-a
@@ -76,11 +116,15 @@ https://example.com?picked-feature=my-feature:true&picked-feature=homepage-varia
76
116
 
77
117
  The format is `?picked-feature=<feature-id>:<bucket-value>`.
78
118
 
79
- This is useful for QA testing specific feature variants without modifying code. The override is persisted in session storage and will be cleared when the browser tab is closed.
119
+ This is useful for QA, because you can test one bucket value and change no code. The
120
+ SDK keeps the override value in session storage. The browser clears it when you close
121
+ the tab.
80
122
 
81
- ## Accessing features
123
+ ## Read a feature flag
82
124
 
83
- Once features are defined, you can access their resolved bucket values in your application code. The SDK provides framework-specific hooks and a framework-agnostic event bus for subscribing to feature decisions.
125
+ After you define your feature flags, your application code can read the resolved
126
+ bucket value of each one. The SDK gives you a hook for each framework, and an event
127
+ bus that works with any framework.
84
128
 
85
129
  :::code-group
86
130
 
@@ -88,66 +132,89 @@ Once features are defined, you can access their resolved bucket values in your a
88
132
  import { createClient } from '@picked/client'
89
133
 
90
134
  const client = createClient({
91
- /* Create client options here */
135
+ /* Put your client options here. */
92
136
  })
93
137
 
94
- client.eventBus.subscribe('feature_decide', ({ feature }) => {
95
- if (feature.id === 'my-feature') {
96
- console.log(feature) // { id: 'my-feature', bucket: 'control' }
138
+ client.eventBus.watch((event) => {
139
+ if (event.type === 'feature_decide' && event.feature.id === 'my-feature') {
140
+ console.log(event.feature) // { id: 'my-feature', key: 'v1', bucket: 'control' }
97
141
  }
98
142
  })
99
143
  ```
100
144
 
101
- ```ts [Svelte]
145
+ ```svelte [Svelte]
102
146
  <script lang="ts">
103
- import { getFeature } from '@picked/svelte/hooks'
104
- // Returns an Effector store containing the feature resolver
105
- const myFeature = getFeature('my-feature')
147
+ import { getFeatureResult } from '@picked/svelte/hooks'
148
+
149
+ // A store that holds { id, bucket }, or undefined until the feature flag resolves.
150
+ const result = getFeatureResult('my-feature')
106
151
  </script>
107
- <!-- $myFeature is a reactive Effector store (see Effector docs for details) -->
108
- {#if $myFeature()}
109
- <p>My feature is enabled</p>
110
- {:else}
111
- <p>My feature is disabled</p>
152
+
153
+ {#if $result?.bucket === 'control'}
154
+ <p>Control</p>
155
+ {:else if $result?.bucket === 'variant-a'}
156
+ <p>Variant A</p>
112
157
  {/if}
113
158
  ```
114
159
 
115
- ```ts [React]
116
- import { useFeature } from '@picked/react/hooks'
160
+ ```tsx [React]
161
+ import { useFeatureResult } from '@picked/react/hooks'
117
162
 
118
163
  function MyComponent() {
119
- // Returns the resolved bucket value for 'my-feature'
120
- const myFeature = useFeature('my-feature')
164
+ // { id, bucket }, or undefined until the feature flag resolves.
165
+ const result = useFeatureResult('my-feature')
121
166
 
122
167
  return (
123
168
  <div>
124
- {myFeature === 'control' && <p>Control variant</p>}
125
- {myFeature === 'variant-a' && <p>Variant A</p>}
169
+ {result?.bucket === 'control' && <p>Control</p>}
170
+ {result?.bucket === 'variant-a' && <p>Variant A</p>}
126
171
  </div>
127
172
  )
128
173
  }
129
-
130
174
  ```
131
175
 
132
176
  :::
133
177
 
134
- ## Using features in experience conditions
178
+ ### Choose an accessor
179
+
180
+ Each accessor returns a different value. They also differ in whether a read resolves
181
+ the feature flag. A read that resolves the feature flag commits the visitor to a bucket and reports
182
+ the decision. See [Allocation and exposure](/sdk/feature-allocation).
183
+
184
+ | API | Returns | Does a read resolve the feature flag? |
185
+ | --- | --- | --- |
186
+ | `useFeature` / `getFeature` | `() => Promise<bucket>` | Only when you call the function that it returns |
187
+ | `useFeatureResult` / `getFeatureResult` | `{ id, bucket }`, and `undefined` until it resolves | Yes, as soon as the component renders |
188
+ | `useFeatureEnabled` / `getFeatureEnabled` | `boolean \| undefined`, the dashboard **Enabled** toggle | No |
189
+ | `client.features.listStore` | Every registered feature flag, each with an `isResolved` feature flag | No |
190
+ | `client.features.getById(id)` | A store that holds one feature flag | No |
191
+
192
+ :::warning
193
+ `useFeatureResult` and `getFeatureResult` resolve the feature flag as soon as the component
194
+ renders. They commit the visitor to a bucket at that moment.
195
+
196
+ Some decisions must wait for data that you do not have yet, such as a cart, a quote,
197
+ or a profile of a signed-in user. For those, use `useFeature` or `getFeature`. Call
198
+ the function that they return after the data arrives.
199
+ :::
200
+
201
+ ## Use a feature flag in an experience condition
135
202
 
136
- Features can be used with experience conditions via JSON logic:
203
+ You can use a feature flag in an experience condition through JSON logic:
137
204
 
138
205
  ```ts
139
206
  const myExperience = createExperience({
140
- // ... other config
207
+ // The other options go here.
141
208
  condition: {
142
209
  and: [
143
210
  {
144
211
  '==': [
145
212
  {
146
213
  invoke: {
147
- var: 'features.summer-sale', // Reference feature by ID
214
+ var: 'features.summer-sale', // Point at the feature flag by its ID.
148
215
  },
149
216
  },
150
- true, // Check if bucket equals true
217
+ true, // Check that the bucket equals true.
151
218
  ],
152
219
  },
153
220
  ],
@@ -155,16 +222,25 @@ const myExperience = createExperience({
155
222
  })
156
223
  ```
157
224
 
158
- ## Listening to feature decisions
225
+ The ID in `features.<id>` is the **Key** of the feature flag from the dashboard. It is the
226
+ same value that the detail page of the feature flag shows.
227
+
228
+ An evaluation of a condition reads the bucket. Picked therefore allocates a visitor
229
+ to a feature flag when the condition that points at it runs.
230
+
231
+ ## Watch for feature flag decisions
159
232
 
160
- Subscribe to the `feature_decide` event to track when features are resolved:
233
+ Subscribe to the `feature_decide` event to track when a feature flag resolves:
161
234
 
162
235
  ```ts
163
- client.eventBus.subscribe('feature_decide', ({ feature }) => {
164
- console.log(`Feature "${feature.id}" resolved to bucket:`, feature.bucket)
165
- // Example output: Feature "summer-sale" resolved to bucket: true
236
+ client.eventBus.watch((event) => {
237
+ if (event.type !== 'feature_decide') return
166
238
 
167
- // Send to analytics
239
+ const { feature } = event
240
+ console.log(`Flag "${feature.id}" resolved to bucket:`, feature.bucket)
241
+ // Example output: the feature flag resolved to the bucket true.
242
+
243
+ // Send the decision to your analytics.
168
244
  analytics.track('Feature Decision', {
169
245
  featureId: feature.id,
170
246
  bucket: feature.bucket,
@@ -172,41 +248,73 @@ client.eventBus.subscribe('feature_decide', ({ feature }) => {
172
248
  })
173
249
  ```
174
250
 
175
- ## Versioning and cache invalidation
251
+ ## Versions and cache invalidation
252
+
253
+ The SDK caches a feature flag decision in browser storage. It uses `localStorage`, or
254
+ `sessionStorage` in a preview. The cache key derives from the feature flag ID and the version,
255
+ so a visitor keeps the same decision across page loads.
176
256
 
177
- Feature decisions are cached in browser storage (localStorage or sessionStorage) using a key derived from the feature ID and version. This ensures visitors see a consistent experience across page loads.
257
+ :::warning
258
+ The SDK does not yet restore a string bucket value, such as `'variant-a'`, correctly
259
+ from storage. It treats the visitor as unallocated on the next page load. A boolean
260
+ bucket value is correct, and that includes every feature flag that you control from the
261
+ dashboard. See PICK-1574.
262
+ :::
178
263
 
179
- ### How versioning works
264
+ ### How versions work
180
265
 
181
- When you create a feature in the dashboard, it starts at version 1. The SDK uses the version to build the cache key — for example, a feature at version 3 uses the key `v3`. This version is also included in the allocation hash, so it directly affects which variant a visitor is assigned to.
266
+ A feature flag that you create in the dashboard starts at version 1. The SDK uses the
267
+ version to build the cache key. A feature flag at version 3 caches under `v3`.
182
268
 
183
- ### What happens when the version is incremented
269
+ The version affects **the cache only**. It is not part of the allocation. For a feature flag
270
+ that you control from the dashboard, the allocation derives from the visitor ID and
271
+ the weight of the feature flag.
184
272
 
185
- When you increment a feature's version in the dashboard:
273
+ ### What happens when you increment the version
186
274
 
187
- 1. **Cache keys change.** The old cache entry (e.g., `v3`) no longer matches the new key (`v4`), so the SDK treats the feature as undecided.
188
- 2. **All visitors are re-allocated.** The allocation hash includes the version, so every visitor gets a new hash value and may be assigned to a different variant.
189
- 3. **The `decide` function runs again.** On the next page load, the SDK calls `decide()` fresh and caches the new result.
275
+ When you increment the version of a feature flag in the dashboard:
190
276
 
191
- > **Warning:** Incrementing the version re-allocates all visitors, not just new ones. This is irreversible. Only increment when you intentionally want to reset all assignments — for example, after changing traffic allocation weights.
277
+ 1. **The cache key changes.** The old entry, `v3`, no longer matches the new key,
278
+ `v4`. The SDK therefore treats the feature flag as undecided.
279
+ 2. **The `decide` function runs again.** On the next page load, the SDK calls
280
+ `decide()` again and caches the new result.
281
+ 3. **Picked evaluates each visitor again and moves nobody.** For a feature flag that you
282
+ control from the dashboard the allocation is deterministic. The same visitor
283
+ against an unchanged weight gives the same bucket.
284
+
285
+ :::note
286
+ The new version clears the cached decisions and runs the allocation again. It moves
287
+ no visitor between groups. With an unchanged weight, everybody lands where they were.
288
+ Increment the version after you change a weight, so that Picked evaluates a visitor
289
+ with a cached decision against the new weight.
290
+ :::
192
291
 
193
- ### SDK `key` vs dashboard version
292
+ ### The SDK `key` and the dashboard version
194
293
 
195
- If you define features statically in the SDK (not loaded from the dashboard), use the `key` field for manual versioning:
294
+ For a feature flag that you define in SDK code, use the `key` field to set the version
295
+ yourself. This applies when you do not read the feature flag from the dashboard:
196
296
 
197
297
  ```ts
198
298
  features: {
199
299
  'summer-sale': {
200
- key: 'summer-sale-v2', // Change to invalidate cache
201
- // ...
300
+ key: 'summer-sale-v2', // Change this to invalidate the cache.
301
+ // The other options go here.
202
302
  },
203
303
  }
204
304
  ```
205
305
 
206
- For features managed through the dashboard, versioning is handled automatically — the dashboard appends the version number to the cache key used by the SDK.
306
+ For a feature flag that you control from the dashboard, Picked sets the version for you. The
307
+ dashboard adds the version number to the end of the cache key that the SDK uses.
207
308
 
208
309
  ## Best practices
209
310
 
210
- - **Use `session` storage for temporary tests**: Set `storage: 'session'` for short-lived experiments that shouldn't persist across browser sessions
211
- - **Keep `decide` functions fast**: The function blocks experience rendering, so avoid heavy computations or slow network calls
212
- - **Return consistent types**: Don't mix boolean and string returns for the same feature - pick one type and stick with it
311
+ - **Use `session` storage for a short test**: set `storage: 'session'` for an
312
+ experiment that must not stay across browser sessions.
313
+ - **Keep each `decide` function fast**: everything that waits for the feature flag waits for
314
+ `decide` to finish. This includes an experience condition and a component that
315
+ reads the result. Do not make a slow network call inside it.
316
+ - **Allocate when the decision matters**: read a feature flag only after the data that it
317
+ needs arrives. Do not read every feature flag at startup. See
318
+ [Allocation and exposure](/sdk/feature-allocation).
319
+ - **Return one type**: do not return a boolean from a feature flag one time and a string the
320
+ next. Choose one type and keep it.
@@ -4,16 +4,19 @@ layout: docs
4
4
 
5
5
  # Fields
6
6
 
7
- Define fields in code with `createField()`, validate input with JSON Logic, and read submitted values through step context.
7
+ Define your fields in code with `createField()`. Check the input with JSON Logic.
8
+ Read the values that a user sends through the step context.
8
9
 
9
- ## Defining fields
10
+ ## Define a field
10
11
 
11
- Define fields using `createField()` from the `@picked/client/plugins/create-experience` package. Each field requires a unique identifier, name, type, label, and cardinality:
12
+ Define a field with `createField()` from the
13
+ `@picked/client/plugins/create-experience` package. Each field needs a unique
14
+ identifier, a name, a type, a label, and a count of the values that it takes:
12
15
 
13
16
  ```ts
14
17
  import { createField } from '@picked/client/plugins/create-experience'
15
18
 
16
- // Required text input
19
+ // A text input that the user must fill in.
17
20
  export const emailField = createField({
18
21
  id: 'email-address',
19
22
  name: 'email-address',
@@ -24,7 +27,7 @@ export const emailField = createField({
24
27
  placeholder: 'you@example.com',
25
28
  })
26
29
 
27
- // Multi-select dropdown with static options
30
+ // A dropdown that takes more than one choice, with fixed options.
28
31
  export const communicationMethods = createField({
29
32
  id: 'communication-methods',
30
33
  name: 'communication-methods',
@@ -54,21 +57,28 @@ export const communicationMethods = createField({
54
57
 
55
58
  ## How fields work
56
59
 
57
- When a field is registered and used in an experience:
58
-
59
- 1. **Registration**: Fields are registered to the experience during step creation via `.addField()`
60
- 2. **Condition Evaluation**: If the field has a `condition`, it's evaluated against the current context to determine visibility
61
- 3. **Rendering**: Visible fields are rendered in the experience UI with their label, placeholder, and options
62
- 4. **User Input**: User enters or selects a value, triggering client-side validation if specified
63
- 5. **Storage**: Valid responses are stored in the experience state under `responses[field.name]`
64
- 6. **Reference**: Field values can be referenced in subsequent field conditions, step conditions, or experience logic via `{ var: 'responses.field-name' }`
60
+ After you register a field and an experience uses it:
61
+
62
+ 1. **Registration**: you register a field on the experience while you create the
63
+ step, with `.addField()`.
64
+ 2. **Condition evaluation**: when the field has a `condition`, the SDK evaluates it
65
+ against the current context. The result decides whether the field is visible.
66
+ 3. **Rendering**: the SDK renders each visible field in the experience UI, with its
67
+ label, its placeholder, and its options.
68
+ 4. **User input**: the user enters or chooses a value. Validation then runs in the
69
+ browser, when you set a rule.
70
+ 5. **Storage**: the SDK stores a valid response in the experience state, under
71
+ `responses[field.name]`.
72
+ 6. **Reference**: a later condition can point at the value with
73
+ `{ var: 'responses.field-name' }`. This works in a field condition, in a step
74
+ condition, and in the logic of an experience.
65
75
 
66
76
  ## Field types
67
77
 
68
- Fields support four primitive types, `text`, `number`, `boolean` and `object`:
78
+ A field has one of four basic types: `text`, `number`, `boolean`, or `object`.
69
79
 
70
80
  ```ts
71
- // Single text input
81
+ // One text input.
72
82
  const nameField = createField({
73
83
  type: 'text',
74
84
  multiple: false,
@@ -76,7 +86,7 @@ const nameField = createField({
76
86
  })
77
87
  ```
78
88
 
79
- Provide an `options` array to allow multiple select:
89
+ Give an `options` array to let the user choose more than one value:
80
90
 
81
91
  ```ts
82
92
  const nameField = createField({
@@ -104,7 +114,7 @@ const nameField = createField({
104
114
 
105
115
  ## Conditions
106
116
 
107
- Fields and field options are compatible with condition logic:
117
+ A field and a field option both work with condition logic:
108
118
 
109
119
  ```ts
110
120
  const conditionalField = createField({
@@ -131,7 +141,8 @@ const conditionalField = createField({
131
141
 
132
142
  ## Field validation
133
143
 
134
- Validate user input using JSON Logic rules. The `$input` variable contains the current field value:
144
+ Check the input of a user with a JSON Logic rule. The `$input` variable holds the
145
+ current value of the field:
135
146
 
136
147
  ```ts
137
148
  const quantityField = createField({
@@ -147,7 +158,7 @@ const quantityField = createField({
147
158
  {
148
159
  and: [
149
160
  {
150
- // Input value is above 0
161
+ // The input value is above 0.
151
162
  '>': [
152
163
  {
153
164
  var: '$input',
@@ -156,7 +167,7 @@ const quantityField = createField({
156
167
  ],
157
168
  },
158
169
  {
159
- // Input value is equal or below 999
170
+ // The input value is 999 or below.
160
171
  '<=': [
161
172
  {
162
173
  var: '$input',
@@ -176,12 +187,13 @@ const quantityField = createField({
176
187
  ```
177
188
 
178
189
  :::tip
179
- The `catch`/`throw` pattern in JSON Logic validates that the condition evaluates to `true`. If it doesn't, the `throw` message is displayed to the user.
190
+ The `catch` and `throw` pattern in JSON Logic checks that the condition gives `true`.
191
+ When it does not, the SDK shows the `throw` message to the user.
180
192
  :::
181
193
 
182
194
  ## Field policies
183
195
 
184
- Control how field data is tracked and stored using privacy policies:
196
+ Use a privacy policy to control how Picked tracks and stores the data of a field:
185
197
 
186
198
  ```ts
187
199
  const sensitiveField = createField({
@@ -192,22 +204,25 @@ const sensitiveField = createField({
192
204
  multiple: false,
193
205
  required: true,
194
206
  policies: {
195
- doNotTrackValue: true, // Don't send value to analytics
196
- doNotStoreValue: true, // Don't persist value to browser storage
207
+ doNotTrackValue: true, // Do not send the value to analytics.
208
+ doNotStoreValue: true, // Do not keep the value in browser storage.
197
209
  },
198
210
  })
199
211
  ```
200
212
 
201
213
  **Privacy policies**:
202
214
 
203
- - `doNotTrackValue`: Excludes field value from analytics events (only field ID is tracked)
204
- - `doNotStoreValue`: Prevents field value from being persisted to browser storage
215
+ - `doNotTrackValue`: keeps the value of the field out of your analytics events. The
216
+ events carry the field ID only.
217
+ - `doNotStoreValue`: stops the SDK, so that it does not put the value in browser
218
+ storage.
205
219
 
206
- Use these policies for sensitive data like SSNs, credit cards, or passwords that should only exist client-side.
220
+ Use these policies for sensitive data such as a tax number, a credit card number, or
221
+ a password. That data must stay in the browser.
207
222
 
208
223
  ## Field metadata
209
224
 
210
- Enhance user experience with additional descriptive text:
225
+ Add more text to help the user:
211
226
 
212
227
  ```ts
213
228
  const passwordField = createField({
@@ -225,18 +240,19 @@ const passwordField = createField({
225
240
 
226
241
  **Metadata fields**:
227
242
 
228
- - `placeholder`: Hint text displayed in empty input fields
229
- - `description`: Brief explanation shown below the label
230
- - `helpText`: Additional guidance or requirements displayed near the field
243
+ - `placeholder`: the hint text that an empty input shows.
244
+ - `description`: a short explanation below the label.
245
+ - `helpText`: more guidance, or the requirements, next to the field.
231
246
 
232
- ## Accessing field values
247
+ ## Read a field value
233
248
 
234
- Field responses are stored in the experience state and can be accessed via the `responses` object:
249
+ The SDK stores each field response in the experience state. Read a response through
250
+ the `responses` object:
235
251
 
236
252
  :::code-group
237
253
 
238
254
  ```ts [In Conditions]
239
- // Reference in subsequent field conditions
255
+ // Point at the value from a later field condition.
240
256
  const sensitiveField = createField({
241
257
  condition: {
242
258
  '==': [
@@ -253,11 +269,19 @@ const sensitiveField = createField({
253
269
 
254
270
  ## Best practices
255
271
 
256
- - **Use semantic IDs and names**: Name fields descriptively (e.g., `email-address` not `field1`) for easier debugging and analytics
257
- - **Match type to data**: Use `number` for numeric values, not `text`, to enable proper validation and comparison
258
- - **Set appropriate `required` flags**: Only mark fields as required when truly necessary to reduce form abandonment
259
- - **Keep validation messages clear**: Error messages should tell users exactly how to fix the input
260
- - **Use conditional fields for progressive disclosure**: Show fields only when relevant based on prior responses
261
- - **Apply privacy policies to sensitive data**: Use `doNotTrackValue` and `doNotStoreValue` for PII, passwords, or financial data
262
- - **Provide helpful metadata**: Add `placeholder` and `description` to guide users, especially for complex or formatted inputs
263
- - **Keep option lists scannable**: For more than 5 options, consider providing search/filter functionality or breaking into multiple steps
272
+ - **Give each field a name that says what it holds**: use `email-address` and not
273
+ `field1`. It is then easier to debug the flow and to read your analytics.
274
+ - **Match the type to the data**: use `number` for a numeric value, and not `text`.
275
+ The SDK can then check and compare the value correctly.
276
+ - **Mark a field as required only when it is**: fewer required fields means fewer
277
+ users who leave the form.
278
+ - **Keep each validation message clear**: an error message must tell the user exactly
279
+ how to correct the input.
280
+ - **Show a field only when it is relevant**: use a condition on the field, and decide
281
+ it from the earlier responses.
282
+ - **Apply a privacy policy to sensitive data**: use `doNotTrackValue` and
283
+ `doNotStoreValue` for personal information, a password, or financial data.
284
+ - **Give helpful metadata**: add a `placeholder` and a `description` to help the
285
+ user, above all for an input with a format.
286
+ - **Keep an option list easy to read**: for more than 5 options, add a search or
287
+ break the field into more than one step.