@picked/client 0.0.70-next.6 → 0.0.71-next.1
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/dist/.tsbuildinfo +1 -0
- package/dist/api-CH2FNa6S.cjs +1 -0
- package/dist/api-CgqRvuKL.js +38 -0
- package/dist/cjs/form/index.js +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/plugins/dom-observer/index.js +1 -1
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/cjs/server/index.js +1 -1
- package/dist/docs/actions.mdx +38 -18
- package/dist/docs/api/authentication.md +75 -48
- package/dist/docs/api/authorization-and-scopes.md +63 -47
- package/dist/docs/api/error-handling.md +70 -54
- package/dist/docs/api/index.md +41 -22
- package/dist/docs/api/pagination.md +30 -20
- package/dist/docs/api-keys.mdx +11 -6
- package/dist/docs/chrome-extension.md +22 -5
- package/dist/docs/conditions.mdx +32 -19
- package/dist/docs/content/asset-library.mdx +65 -34
- package/dist/docs/content/content-entries.mdx +8 -2
- package/dist/docs/content/layouts.mdx +45 -29
- package/dist/docs/content/pages.mdx +29 -14
- package/dist/docs/content/sections.mdx +19 -8
- package/dist/docs/delivery-api/experience-content.md +10 -5
- package/dist/docs/delivery-api/index.md +19 -12
- package/dist/docs/delivery-api/preview-caching-crawling.md +27 -14
- package/dist/docs/delivery-api/project-config.md +6 -4
- package/dist/docs/experiences.mdx +26 -10
- package/dist/docs/features.mdx +66 -25
- package/dist/docs/fields.mdx +42 -27
- package/dist/docs/flow-builder/building-flows.md +38 -20
- package/dist/docs/flow-builder/condition-builder.md +32 -21
- package/dist/docs/flow-builder/keyboard-shortcuts.md +18 -18
- package/dist/docs/flow-builder/nodes.md +26 -13
- package/dist/docs/flow-builder/overview.mdx +28 -15
- package/dist/docs/gtm-template.md +35 -26
- package/dist/docs/guides/build-a-personalized-page.mdx +36 -18
- package/dist/docs/image-optimisation-service.md +33 -17
- package/dist/docs/index.md +28 -17
- package/dist/docs/mcp.md +46 -38
- package/dist/docs/pickedql/data-sources.md +65 -43
- package/dist/docs/pickedql/dimensions.md +27 -12
- package/dist/docs/pickedql/examples.md +38 -26
- package/dist/docs/pickedql/filtering.md +10 -6
- package/dist/docs/pickedql/index.md +32 -14
- package/dist/docs/pickedql/schema.md +85 -76
- package/dist/docs/pickedql/virtual-functions.md +62 -36
- package/dist/docs/projects.mdx +14 -7
- package/dist/docs/reports.mdx +5 -3
- package/dist/docs/sdk/actions.md +33 -17
- package/dist/docs/sdk/conditions.md +8 -5
- package/dist/docs/sdk/content-security-policy.md +5 -4
- package/dist/docs/sdk/experiences.md +29 -11
- package/dist/docs/sdk/feature-allocation.md +79 -47
- package/dist/docs/sdk/features.md +130 -76
- package/dist/docs/sdk/fields.md +66 -42
- package/dist/docs/sdk/index.md +22 -13
- package/dist/docs/sdk/plugins/create-experience.md +18 -15
- package/dist/docs/sdk/plugins/dynamic-experiences.md +42 -16
- package/dist/docs/sdk/predicates.md +53 -37
- package/dist/docs/sdk/ssr/index.md +25 -8
- package/dist/docs/sdk/ssr/rendering-and-hydration.md +37 -16
- package/dist/docs/sdk/ssr/serialize.md +38 -20
- package/dist/docs/sdk/ssr/slots-and-placements.md +28 -10
- package/dist/docs/sdk/steps.md +100 -55
- package/dist/docs/steps.mdx +20 -11
- package/dist/docs/team-management.md +17 -11
- package/dist/docs/vite-plugin.md +26 -19
- package/dist/docs/workspace-versioning.mdx +14 -8
- package/dist/docs/workspaces.mdx +55 -24
- package/dist/{dom-BwmTHeT7.js → dom-BqcToNL0.js} +1 -3
- package/dist/{dom-CDrHc_j9.cjs → dom-DlLH3FxV.cjs} +1 -1
- package/dist/esm/form/index.js +2 -4
- package/dist/esm/index.js +50 -45
- package/dist/esm/plugins/dom-observer/index.js +1 -1
- package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
- package/dist/esm/preview/index.js +1 -1
- package/dist/esm/server/index.js +12 -10
- package/dist/{experience-instance-DOJShRk5.js → experience-instance-DGk11d8a.js} +3 -3
- package/dist/experience-instance-Dej3P1sE.cjs +1 -0
- package/dist/{plugin-BxMR7fOV.js → plugin-D2UqFikQ.js} +2 -2
- package/dist/plugin-DCW-tnjO.cjs +1 -0
- package/dist/{preview-channel-DW9J8t4B.js → preview-channel-CSr96XAo.js} +1 -1
- package/dist/{protocol-DSqOlW2r.js → protocol-C5KkkiZc.js} +1 -1
- package/dist/types/client.d.ts +7 -6
- package/dist/types/entities/action/action.d.ts +5 -4
- package/dist/types/entities/action/action.d.ts.map +1 -1
- package/dist/types/entities/component/component.d.ts +6 -5
- package/dist/types/entities/component/component.d.ts.map +1 -1
- package/dist/types/entities/condition/condition.d.ts +5 -5
- package/dist/types/entities/content/content.d.ts +4 -3
- package/dist/types/entities/experience/experience.d.ts +11 -10
- package/dist/types/entities/experience/experience.d.ts.map +1 -1
- package/dist/types/entities/experience-instance/experience-instance.d.ts +10 -9
- package/dist/types/entities/feature/feature.d.ts +3 -2
- package/dist/types/entities/feature/feature.d.ts.map +1 -1
- package/dist/types/entities/field/field.d.ts +7 -6
- package/dist/types/entities/framework/framework.d.ts +7 -6
- package/dist/types/entities/framework/framework.d.ts.map +1 -1
- package/dist/types/entities/history/history.d.ts +6 -6
- package/dist/types/entities/plugin/plugin.d.ts +3 -2
- package/dist/types/entities/plugin/plugin.d.ts.map +1 -1
- package/dist/types/entities/predicate/predicate.d.ts +4 -3
- package/dist/types/entities/predicate/predicate.d.ts.map +1 -1
- package/dist/types/entities/response/response-validation.d.ts +3 -2
- package/dist/types/entities/response/response.d.ts +12 -11
- package/dist/types/entities/session/session.d.ts +2 -1
- package/dist/types/entities/step/step.d.ts +6 -5
- package/dist/types/exports.d.ts +2 -1
- package/dist/types/exports.d.ts.map +1 -1
- package/dist/types/form/build-responses.d.ts +2 -1
- package/dist/types/form/create-field-evaluator.d.ts +4 -3
- package/dist/types/form/create-field-validation.d.ts +5 -4
- package/dist/types/form/create-form.d.ts +7 -6
- package/dist/types/form/create-submit-pipeline.d.ts +7 -6
- package/dist/types/form/get-step-fields.d.ts +3 -2
- package/dist/types/lifecycle/condition-selection.d.ts +14 -4
- package/dist/types/lifecycle/lifecycle.d.ts +4 -3
- package/dist/types/lifecycle/rules-logic.d.ts +4 -3
- package/dist/types/lifecycle/state-snapshot.d.ts +4 -3
- package/dist/types/picked.d.ts +64 -62
- package/dist/types/picked.d.ts.map +1 -1
- package/dist/types/ping.d.ts +1 -1
- package/dist/types/ping.d.ts.map +1 -1
- package/dist/types/plugins/create-experience/create-experience.d.ts +9 -8
- package/dist/types/plugins/create-experience/create-experience.d.ts.map +1 -1
- package/dist/types/plugins/create-experience/plugin.d.ts +3 -2
- package/dist/types/plugins/create-experience/plugin.d.ts.map +1 -1
- package/dist/types/plugins/create-experience/serialization.d.ts.map +1 -1
- package/dist/types/plugins/dom-observer/plugin.d.ts +1 -1
- package/dist/types/plugins/dynamic-experiences/plugin.d.ts +3 -3
- package/dist/types/plugins/dynamic-experiences/types.d.ts +8 -5
- package/dist/types/plugins/experience-browser-storage/plugin.d.ts +1 -1
- package/dist/types/plugins/experience-browser-storage/plugin.d.ts.map +1 -1
- package/dist/types/plugins/history-browser-storage/plugin.d.ts +1 -1
- package/dist/types/plugins/history-browser-storage/plugin.d.ts.map +1 -1
- package/dist/types/plugins/responses-browser-storage/plugin.d.ts +1 -1
- package/dist/types/plugins/responses-browser-storage/plugin.d.ts.map +1 -1
- package/dist/types/plugins/responses-indexeddb/plugin.d.ts +1 -1
- package/dist/types/plugins/single-page-app/plugin.d.ts +1 -1
- package/dist/types/plugins/single-page-app/plugin.d.ts.map +1 -1
- package/dist/types/plugins/web-components/plugin.d.ts +2 -1
- package/dist/types/plugins/window-object/plugin.d.ts +2 -2
- package/dist/types/preview-channel.d.ts +5 -1
- package/dist/types/preview-receiver/index.d.ts +18 -1
- package/dist/types/preview-receiver/protocol.d.ts +10 -1
- package/dist/types/server/index.d.ts +3 -3
- package/dist/types/server/serialize.d.ts +2 -2
- package/dist/types/utils/dom.d.ts +2 -1
- package/dist/types/utils/events.d.ts +6 -6
- package/dist/types/utils/events.d.ts.map +1 -1
- package/dist/types/utils/global-obj.d.ts +1 -1
- package/dist/types/utils/global-obj.d.ts.map +1 -1
- package/dist/types/utils/id.d.ts.map +1 -1
- package/dist/types/utils/logger.d.ts +2 -1
- package/dist/types/utils/logger.d.ts.map +1 -1
- package/dist/types/utils/node-tree/get-next-node-from-action.d.ts +2 -1
- package/dist/types/utils/node-tree/get-next-node-from-condition.d.ts +2 -1
- package/dist/types/utils/node-tree/get-next-node.d.ts +1 -1
- package/dist/types/utils/node-tree/get-next-node.d.ts.map +1 -1
- package/dist/types/utils/node-tree/get-origin-node.d.ts +1 -1
- package/dist/types/utils/node-tree/get-origin-node.d.ts.map +1 -1
- package/dist/types/utils/node-tree/min-max-steps-to-leaf.d.ts +1 -1
- package/dist/types/utils/node-tree/min-max-steps-to-leaf.d.ts.map +1 -1
- package/dist/types/utils/node-tree/min-max-steps-to-origin.d.ts +1 -1
- package/dist/types/utils/node-tree/min-max-steps-to-origin.d.ts.map +1 -1
- package/dist/types/utils/node-tree/next-reachable-step-nodes.d.ts +1 -1
- package/dist/types/utils/node-tree/next-reachable-step-nodes.d.ts.map +1 -1
- package/dist/types/utils/nodes.d.ts +1 -1
- package/dist/types/utils/picked-config.d.ts +2 -1
- package/dist/types/utils/rules-logic.d.ts +6 -2
- package/dist/types/utils/storage.d.ts +1 -1
- package/dist/types/utils/storage.d.ts.map +1 -1
- package/package.json +22 -23
- package/dist/api-CoUkIxIr.cjs +0 -1
- package/dist/api-ahdSBQ3Y.js +0 -40
- package/dist/experience-instance-DurC3pE-.cjs +0 -1
- package/dist/plugin-CASdGtTX.cjs +0 -1
|
@@ -4,29 +4,40 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Feature flags
|
|
6
6
|
|
|
7
|
-
|
|
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
11
|
## Where feature flags come from
|
|
10
12
|
|
|
11
|
-
|
|
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.
|
|
12
17
|
|
|
13
|
-
For a
|
|
18
|
+
For a feature flag that you control from the dashboard, the SDK makes the definition:
|
|
14
19
|
|
|
15
20
|
| Field | Value |
|
|
16
21
|
| --- | --- |
|
|
17
|
-
| `id` | The feature flag
|
|
18
|
-
| `key` | `v{version}
|
|
19
|
-
| `name` | The feature flag
|
|
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 |
|
|
20
25
|
| `enabled` | The dashboard **Enabled** toggle |
|
|
21
|
-
| `weight` | The dashboard **Weight**, 0
|
|
26
|
+
| `weight` | The dashboard **Weight**, 0 to 100 |
|
|
22
27
|
| `storage` | `local`, or `session` in [preview mode](/sdk/feature-allocation#preview-mode) |
|
|
23
|
-
| `decide` |
|
|
28
|
+
| `decide` | Made by the SDK. It compares the stable value of the visitor for this feature flag against `weight`. |
|
|
24
29
|
|
|
25
|
-
The
|
|
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.
|
|
26
34
|
|
|
27
|
-
##
|
|
35
|
+
## Define a feature flag in SDK code
|
|
28
36
|
|
|
29
|
-
Define feature flags when
|
|
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:
|
|
30
41
|
|
|
31
42
|
```ts
|
|
32
43
|
import { createClient } from '@picked/client'
|
|
@@ -35,20 +46,20 @@ const client = createClient({
|
|
|
35
46
|
projectId: 'your-project-id',
|
|
36
47
|
features: {
|
|
37
48
|
'summer-sale': {
|
|
38
|
-
key: 'summer-sale-v1', //
|
|
39
|
-
name: 'Summer Sale Experiment', //
|
|
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.
|
|
40
51
|
decide: async () => {
|
|
41
|
-
//
|
|
52
|
+
// Put your decision rules here.
|
|
42
53
|
const isEligible = await checkUserEligibility()
|
|
43
|
-
return isEligible // Returns boolean
|
|
54
|
+
return isEligible // Returns a boolean.
|
|
44
55
|
},
|
|
45
|
-
storage: 'local', // Optional: 'local'
|
|
56
|
+
storage: 'local', // Optional: 'local' is the default, or 'session'.
|
|
46
57
|
},
|
|
47
58
|
'homepage-variant': {
|
|
48
59
|
key: 'homepage-test-v2',
|
|
49
60
|
name: 'Homepage A/B Test',
|
|
50
61
|
decide: async () => {
|
|
51
|
-
// Return string for
|
|
62
|
+
// Return a string for a test of more than two variants.
|
|
52
63
|
return Math.random() < 0.5 ? 'variant-a' : 'variant-b'
|
|
53
64
|
},
|
|
54
65
|
},
|
|
@@ -56,7 +67,8 @@ const client = createClient({
|
|
|
56
67
|
})
|
|
57
68
|
```
|
|
58
69
|
|
|
59
|
-
|
|
70
|
+
A feature flag can also be a factory function, which starts the feature flag only when it is
|
|
71
|
+
first needed:
|
|
60
72
|
|
|
61
73
|
```ts
|
|
62
74
|
features: {
|
|
@@ -70,25 +82,33 @@ features: {
|
|
|
70
82
|
|
|
71
83
|
## How feature flags work
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
After you register a feature flag, the SDK follows this order:
|
|
74
86
|
|
|
75
|
-
1. **Registration** —
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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.
|
|
80
99
|
|
|
81
|
-
See [Allocation and exposure](/sdk/feature-allocation) for what this means when
|
|
100
|
+
See [Allocation and exposure](/sdk/feature-allocation) for what this means when you
|
|
101
|
+
track an exposure.
|
|
82
102
|
|
|
83
|
-
## URL
|
|
103
|
+
## Overrides in the URL
|
|
84
104
|
|
|
85
|
-
You can override feature flag
|
|
105
|
+
You can override a feature flag decision from the URL search parameters, for a test:
|
|
86
106
|
|
|
87
107
|
```
|
|
88
108
|
https://example.com?picked-feature=my-feature:true
|
|
89
109
|
```
|
|
90
110
|
|
|
91
|
-
|
|
111
|
+
You can give more than one override value:
|
|
92
112
|
|
|
93
113
|
```
|
|
94
114
|
https://example.com?picked-feature=my-feature:true&picked-feature=homepage-variant:variant-a
|
|
@@ -96,11 +116,15 @@ https://example.com?picked-feature=my-feature:true&picked-feature=homepage-varia
|
|
|
96
116
|
|
|
97
117
|
The format is `?picked-feature=<feature-id>:<bucket-value>`.
|
|
98
118
|
|
|
99
|
-
This is useful for QA
|
|
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.
|
|
100
122
|
|
|
101
|
-
##
|
|
123
|
+
## Read a feature flag
|
|
102
124
|
|
|
103
|
-
|
|
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.
|
|
104
128
|
|
|
105
129
|
:::code-group
|
|
106
130
|
|
|
@@ -108,7 +132,7 @@ Once feature flags are defined, you can read their resolved bucket values in you
|
|
|
108
132
|
import { createClient } from '@picked/client'
|
|
109
133
|
|
|
110
134
|
const client = createClient({
|
|
111
|
-
/*
|
|
135
|
+
/* Put your client options here. */
|
|
112
136
|
})
|
|
113
137
|
|
|
114
138
|
client.eventBus.watch((event) => {
|
|
@@ -122,7 +146,7 @@ client.eventBus.watch((event) => {
|
|
|
122
146
|
<script lang="ts">
|
|
123
147
|
import { getFeatureResult } from '@picked/svelte/hooks'
|
|
124
148
|
|
|
125
|
-
// A store
|
|
149
|
+
// A store that holds { id, bucket }, or undefined until the feature flag resolves.
|
|
126
150
|
const result = getFeatureResult('my-feature')
|
|
127
151
|
</script>
|
|
128
152
|
|
|
@@ -137,7 +161,7 @@ client.eventBus.watch((event) => {
|
|
|
137
161
|
import { useFeatureResult } from '@picked/react/hooks'
|
|
138
162
|
|
|
139
163
|
function MyComponent() {
|
|
140
|
-
// { id, bucket }, or undefined until the flag resolves
|
|
164
|
+
// { id, bucket }, or undefined until the feature flag resolves.
|
|
141
165
|
const result = useFeatureResult('my-feature')
|
|
142
166
|
|
|
143
167
|
return (
|
|
@@ -151,41 +175,46 @@ function MyComponent() {
|
|
|
151
175
|
|
|
152
176
|
:::
|
|
153
177
|
|
|
154
|
-
###
|
|
178
|
+
### Choose an accessor
|
|
155
179
|
|
|
156
|
-
|
|
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).
|
|
157
183
|
|
|
158
|
-
| API | Returns |
|
|
184
|
+
| API | Returns | Does a read resolve the feature flag? |
|
|
159
185
|
| --- | --- | --- |
|
|
160
|
-
| `useFeature` / `getFeature` | `() => Promise<bucket>` | Only when you call the
|
|
161
|
-
| `useFeatureResult` / `getFeatureResult` | `{ id, bucket }`, `undefined` until
|
|
162
|
-
| `useFeatureEnabled` / `getFeatureEnabled` | `boolean \| undefined
|
|
163
|
-
| `client.features.listStore` | Every registered feature flag, each with an `isResolved`
|
|
164
|
-
| `client.features.getById(id)` | A store
|
|
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 |
|
|
165
191
|
|
|
166
192
|
:::warning
|
|
167
|
-
`useFeatureResult` and `getFeatureResult` resolve the feature flag as soon as the component
|
|
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.
|
|
168
195
|
|
|
169
|
-
|
|
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.
|
|
170
199
|
:::
|
|
171
200
|
|
|
172
|
-
##
|
|
201
|
+
## Use a feature flag in an experience condition
|
|
173
202
|
|
|
174
|
-
|
|
203
|
+
You can use a feature flag in an experience condition through JSON logic:
|
|
175
204
|
|
|
176
205
|
```ts
|
|
177
206
|
const myExperience = createExperience({
|
|
178
|
-
//
|
|
207
|
+
// The other options go here.
|
|
179
208
|
condition: {
|
|
180
209
|
and: [
|
|
181
210
|
{
|
|
182
211
|
'==': [
|
|
183
212
|
{
|
|
184
213
|
invoke: {
|
|
185
|
-
var: 'features.summer-sale', //
|
|
214
|
+
var: 'features.summer-sale', // Point at the feature flag by its ID.
|
|
186
215
|
},
|
|
187
216
|
},
|
|
188
|
-
true, // Check
|
|
217
|
+
true, // Check that the bucket equals true.
|
|
189
218
|
],
|
|
190
219
|
},
|
|
191
220
|
],
|
|
@@ -193,13 +222,15 @@ const myExperience = createExperience({
|
|
|
193
222
|
})
|
|
194
223
|
```
|
|
195
224
|
|
|
196
|
-
The ID in `features.<id>` is the
|
|
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.
|
|
197
227
|
|
|
198
|
-
|
|
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.
|
|
199
230
|
|
|
200
|
-
##
|
|
231
|
+
## Watch for feature flag decisions
|
|
201
232
|
|
|
202
|
-
Subscribe to the `feature_decide` event to track when feature
|
|
233
|
+
Subscribe to the `feature_decide` event to track when a feature flag resolves:
|
|
203
234
|
|
|
204
235
|
```ts
|
|
205
236
|
client.eventBus.watch((event) => {
|
|
@@ -207,9 +238,9 @@ client.eventBus.watch((event) => {
|
|
|
207
238
|
|
|
208
239
|
const { feature } = event
|
|
209
240
|
console.log(`Flag "${feature.id}" resolved to bucket:`, feature.bucket)
|
|
210
|
-
// Example output:
|
|
241
|
+
// Example output: the feature flag resolved to the bucket true.
|
|
211
242
|
|
|
212
|
-
// Send to analytics
|
|
243
|
+
// Send the decision to your analytics.
|
|
213
244
|
analytics.track('Feature Decision', {
|
|
214
245
|
featureId: feature.id,
|
|
215
246
|
bucket: feature.bucket,
|
|
@@ -217,50 +248,73 @@ client.eventBus.watch((event) => {
|
|
|
217
248
|
})
|
|
218
249
|
```
|
|
219
250
|
|
|
220
|
-
##
|
|
251
|
+
## Versions and cache invalidation
|
|
221
252
|
|
|
222
|
-
|
|
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.
|
|
223
256
|
|
|
224
257
|
:::warning
|
|
225
|
-
|
|
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.
|
|
226
262
|
:::
|
|
227
263
|
|
|
228
|
-
### How
|
|
264
|
+
### How versions work
|
|
229
265
|
|
|
230
|
-
|
|
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`.
|
|
231
268
|
|
|
232
|
-
The version affects **
|
|
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.
|
|
233
272
|
|
|
234
|
-
### What happens when the version
|
|
273
|
+
### What happens when you increment the version
|
|
235
274
|
|
|
236
|
-
When you increment a feature flag
|
|
275
|
+
When you increment the version of a feature flag in the dashboard:
|
|
237
276
|
|
|
238
|
-
1. **
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
241
284
|
|
|
242
285
|
:::note
|
|
243
|
-
|
|
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.
|
|
244
290
|
:::
|
|
245
291
|
|
|
246
|
-
### SDK `key`
|
|
292
|
+
### The SDK `key` and the dashboard version
|
|
247
293
|
|
|
248
|
-
|
|
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:
|
|
249
296
|
|
|
250
297
|
```ts
|
|
251
298
|
features: {
|
|
252
299
|
'summer-sale': {
|
|
253
|
-
key: 'summer-sale-v2', // Change to invalidate cache
|
|
254
|
-
//
|
|
300
|
+
key: 'summer-sale-v2', // Change this to invalidate the cache.
|
|
301
|
+
// The other options go here.
|
|
255
302
|
},
|
|
256
303
|
}
|
|
257
304
|
```
|
|
258
305
|
|
|
259
|
-
For feature
|
|
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.
|
|
260
308
|
|
|
261
309
|
## Best practices
|
|
262
310
|
|
|
263
|
-
- **Use `session` storage for
|
|
264
|
-
|
|
265
|
-
- **
|
|
266
|
-
|
|
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.
|
package/dist/docs/sdk/fields.md
CHANGED
|
@@ -4,16 +4,19 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Fields
|
|
6
6
|
|
|
7
|
-
Define fields in code with `createField()
|
|
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
|
-
##
|
|
10
|
+
## Define a field
|
|
10
11
|
|
|
11
|
-
Define
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
1. **Registration**:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
78
|
+
A field has one of four basic types: `text`, `number`, `boolean`, or `object`.
|
|
69
79
|
|
|
70
80
|
```ts
|
|
71
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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, //
|
|
196
|
-
doNotStoreValue: true, //
|
|
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`:
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
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`:
|
|
229
|
-
- `description`:
|
|
230
|
-
- `helpText`:
|
|
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
|
-
##
|
|
247
|
+
## Read a field value
|
|
233
248
|
|
|
234
|
-
|
|
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
|
-
//
|
|
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
|
-
- **
|
|
257
|
-
|
|
258
|
-
- **
|
|
259
|
-
|
|
260
|
-
- **
|
|
261
|
-
|
|
262
|
-
- **
|
|
263
|
-
|
|
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.
|