@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.
- package/dist/.tsbuildinfo +1 -0
- package/dist/api-Djuh2gfe.cjs +1 -0
- package/dist/api-d0qKmzPH.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/preview/index.js +1 -0
- 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 +93 -22
- 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 +47 -39
- 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 +161 -0
- package/dist/docs/sdk/features.md +181 -73
- package/dist/docs/sdk/fields.md +66 -42
- package/dist/docs/sdk/index.md +25 -10
- package/dist/docs/sdk/plugins/create-experience.md +18 -15
- package/dist/docs/sdk/plugins/dynamic-experiences.md +43 -17
- 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 +434 -334
- 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 +2 -0
- package/dist/esm/server/index.js +13 -11
- package/dist/{experience-instance-DOJShRk5.js → experience-instance-DGk11d8a.js} +3 -3
- package/dist/experience-instance-Dej3P1sE.cjs +1 -0
- package/dist/inert-script-report-B_bMPRzp.js +18 -0
- package/dist/inert-script-report-ijRHPtcY.cjs +1 -0
- package/dist/{plugin-C46DEiGE.js → plugin-BVAFWjpT.js} +61 -56
- package/dist/plugin-BcCn2llk.cjs +1 -0
- package/dist/preview-channel-CSr96XAo.js +98 -0
- package/dist/preview-channel-Cx0e6fye.cjs +1 -0
- package/dist/protocol-Bb_vpLKE.cjs +1 -0
- package/dist/protocol-C5KkkiZc.js +84 -0
- 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 +14 -3
- package/dist/types/entities/content/content.d.ts.map +1 -1
- package/dist/types/entities/content/reference.d.ts +101 -0
- package/dist/types/entities/content/reference.d.ts.map +1 -0
- 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 +6 -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/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- 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 +102 -68
- 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/hash.d.ts +40 -5
- package/dist/types/plugins/dynamic-experiences/hash.d.ts.map +1 -1
- package/dist/types/plugins/dynamic-experiences/plugin.d.ts +3 -3
- package/dist/types/plugins/dynamic-experiences/plugin.d.ts.map +1 -1
- package/dist/types/plugins/dynamic-experiences/types.d.ts +24 -5
- package/dist/types/plugins/dynamic-experiences/types.d.ts.map +1 -1
- 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/index.d.ts +18 -0
- package/dist/types/preview/index.d.ts.map +1 -0
- package/dist/types/preview-channel.d.ts +16 -0
- package/dist/types/preview-channel.d.ts.map +1 -0
- package/dist/types/preview-receiver/apply-ops.d.ts +32 -0
- package/dist/types/preview-receiver/apply-ops.d.ts.map +1 -0
- package/dist/types/preview-receiver/index.d.ts +32 -0
- package/dist/types/preview-receiver/index.d.ts.map +1 -0
- package/dist/types/preview-receiver/protocol.d.ts +69 -0
- package/dist/types/preview-receiver/protocol.d.ts.map +1 -0
- package/dist/types/server/index.d.ts +3 -3
- package/dist/types/server/serialize.d.ts +8 -3
- package/dist/types/server/serialize.d.ts.map +1 -1
- package/dist/types/utils/content-path.d.ts +37 -0
- package/dist/types/utils/content-path.d.ts.map +1 -0
- 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/inert-script-report.d.ts +40 -0
- package/dist/types/utils/inert-script-report.d.ts.map +1 -0
- 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/preview-session.d.ts +12 -0
- package/dist/types/utils/preview-session.d.ts.map +1 -0
- package/dist/types/utils/rehydrate-scripts.d.ts +6 -0
- package/dist/types/utils/rehydrate-scripts.d.ts.map +1 -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 +35 -24
- package/dist/api-BzcdC_7V.cjs +0 -1
- package/dist/api-o8w9kJwm.js +0 -40
- package/dist/experience-instance-DurC3pE-.cjs +0 -1
- package/dist/plugin-nrEJGTuE.cjs +0 -1
- package/dist/preview-bridge-CpX1BkSA.cjs +0 -1
- package/dist/preview-bridge-D76l-dPy.js +0 -33
|
@@ -2,13 +2,42 @@
|
|
|
2
2
|
layout: docs
|
|
3
3
|
---
|
|
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.
|
|
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', //
|
|
21
|
-
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.
|
|
22
51
|
decide: async () => {
|
|
23
|
-
//
|
|
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'
|
|
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
|
|
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
|
-
|
|
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
|
|
83
|
+
## How feature flags work
|
|
84
|
+
|
|
85
|
+
After you register a feature flag, the SDK follows this order:
|
|
54
86
|
|
|
55
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
103
|
+
## Overrides in the URL
|
|
64
104
|
|
|
65
|
-
You can override feature
|
|
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
|
-
|
|
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
|
|
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
|
-
##
|
|
123
|
+
## Read a feature flag
|
|
82
124
|
|
|
83
|
-
|
|
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
|
-
/*
|
|
135
|
+
/* Put your client options here. */
|
|
92
136
|
})
|
|
93
137
|
|
|
94
|
-
client.eventBus.
|
|
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
|
-
```
|
|
145
|
+
```svelte [Svelte]
|
|
102
146
|
<script lang="ts">
|
|
103
|
-
import {
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
108
|
-
{#if $
|
|
109
|
-
<p>
|
|
110
|
-
{:else}
|
|
111
|
-
<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
|
-
```
|
|
116
|
-
import {
|
|
160
|
+
```tsx [React]
|
|
161
|
+
import { useFeatureResult } from '@picked/react/hooks'
|
|
117
162
|
|
|
118
163
|
function MyComponent() {
|
|
119
|
-
//
|
|
120
|
-
const
|
|
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
|
-
{
|
|
125
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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', //
|
|
214
|
+
var: 'features.summer-sale', // Point at the feature flag by its ID.
|
|
148
215
|
},
|
|
149
216
|
},
|
|
150
|
-
true, // Check
|
|
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
|
-
|
|
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
|
|
233
|
+
Subscribe to the `feature_decide` event to track when a feature flag resolves:
|
|
161
234
|
|
|
162
235
|
```ts
|
|
163
|
-
client.eventBus.
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
264
|
+
### How versions work
|
|
180
265
|
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
273
|
+
### What happens when you increment the version
|
|
186
274
|
|
|
187
|
-
|
|
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
|
-
|
|
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`
|
|
292
|
+
### The SDK `key` and the dashboard version
|
|
194
293
|
|
|
195
|
-
|
|
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
|
|
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
|
|
211
|
-
|
|
212
|
-
- **
|
|
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.
|