@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
package/dist/docs/sdk/actions.md
CHANGED
|
@@ -4,11 +4,13 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Actions
|
|
6
6
|
|
|
7
|
-
Register action
|
|
7
|
+
Register an action handler on the Picked client. The SDK then invokes the handler
|
|
8
|
+
when an experience flow reaches an action node.
|
|
8
9
|
|
|
9
|
-
##
|
|
10
|
+
## Register an action
|
|
10
11
|
|
|
11
|
-
Register actions when
|
|
12
|
+
Register your actions when you create the Picked client. Each key in the `actions`
|
|
13
|
+
object must match an action key from the dashboard:
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
16
|
import { createClient } from '@picked/client'
|
|
@@ -26,41 +28,55 @@ const picked = createClient({
|
|
|
26
28
|
})
|
|
27
29
|
```
|
|
28
30
|
|
|
29
|
-
Each action is a function
|
|
31
|
+
Each action is a function. It takes optional parameters and it usually runs a side
|
|
32
|
+
effect. When the flow reaches an action node, the SDK calls the function that
|
|
33
|
+
matches. The SDK then takes a branch, which depends on whether the function succeeds
|
|
34
|
+
or throws. See [Success and failure branches](#success-and-failure-branches).
|
|
30
35
|
|
|
31
36
|
## How actions work
|
|
32
37
|
|
|
33
|
-
1. **Registration**:
|
|
34
|
-
2. **Flow traversal**:
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
1. **Registration**: you register each action on the Picked client at startup.
|
|
39
|
+
2. **Flow traversal**: the SDK walks the experience flow. When it reaches an action
|
|
40
|
+
node, it finds the action by its key.
|
|
41
|
+
3. **Execution**: the SDK calls the function that matches and awaits it. The outcome
|
|
42
|
+
then decides which node comes next.
|
|
43
|
+
4. **No block**: an action does not interrupt your visitor. The current step
|
|
44
|
+
stays on the screen while an async action runs.
|
|
37
45
|
|
|
38
|
-
## Success and failure
|
|
46
|
+
## Success and failure branches
|
|
39
47
|
|
|
40
|
-
An action node can have
|
|
48
|
+
An action node can have a **success** branch and a **failure** branch. The SDK
|
|
49
|
+
chooses the branch from the outcome of the action:
|
|
41
50
|
|
|
42
|
-
- **Failure** — the action throws
|
|
43
|
-
|
|
51
|
+
- **Failure** — the action throws, or its promise rejects. The flow takes the failure
|
|
52
|
+
branch, `onFail`.
|
|
53
|
+
- **Success** — the action resolves to any value: `void`, `true`, `false`, or a
|
|
54
|
+
string. A resolved `false` is a valid response and not a failure. The flow takes
|
|
55
|
+
the success branch, `onSuccess`.
|
|
44
56
|
|
|
45
|
-
|
|
57
|
+
Keep the failure branch for a true error. Throw on the failure case, because a
|
|
58
|
+
returned value never takes the `onFail` branch:
|
|
46
59
|
|
|
47
60
|
```ts
|
|
48
61
|
'check-eligibility': async () => {
|
|
49
62
|
const response = await fetch('/api/eligibility')
|
|
50
|
-
// Throw only when the request itself fails
|
|
63
|
+
// Throw only when the request itself fails. That takes the failure branch.
|
|
51
64
|
if (!response.ok) throw new Error('Eligibility request failed')
|
|
52
65
|
}
|
|
53
66
|
```
|
|
54
67
|
|
|
55
|
-
A valid negative result is not a failure. An API
|
|
68
|
+
A valid negative result is not a failure. An API can return `200` with a "no", for
|
|
69
|
+
example `{ serviced: false }`. Record that value as a response and branch on it with
|
|
70
|
+
a condition. Do not throw:
|
|
56
71
|
|
|
57
72
|
```ts
|
|
58
73
|
'check-serviced-area': async ({ client, responses }) => {
|
|
59
74
|
const postcode = responses.find((r) => r.fieldKey === 'postcode')?.value
|
|
60
75
|
const { serviced } = await fetch(`/api/serviced?postcode=${postcode}`).then((r) => r.json())
|
|
61
|
-
//
|
|
76
|
+
// Store the outcome as a response. A later condition then takes the branch.
|
|
62
77
|
client.responses.register('is-serviced', serviced)
|
|
63
78
|
}
|
|
64
79
|
```
|
|
65
80
|
|
|
66
|
-
|
|
81
|
+
An action node can also have one plain connection, with no success or failure branch.
|
|
82
|
+
The flow then continues through it on either outcome.
|
|
@@ -4,11 +4,13 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Conditions
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A condition uses JSON Logic to evaluate the client state. Use a condition to control
|
|
8
|
+
when an experience starts, to choose the next step, and to show or hide a field.
|
|
8
9
|
|
|
9
10
|
## Conditions in experiences
|
|
10
11
|
|
|
11
|
-
A common pattern
|
|
12
|
+
A common pattern puts a condition between two steps. The flow then takes a branch
|
|
13
|
+
that the form responses decide:
|
|
12
14
|
|
|
13
15
|
```ts
|
|
14
16
|
import { createExperience } from '@picked/client/plugins/create-experience'
|
|
@@ -47,12 +49,13 @@ isSigningUpForNewsletter.addFalse(userDoesNotWantNewsletter)
|
|
|
47
49
|
|
|
48
50
|
## Conditional fields
|
|
49
51
|
|
|
50
|
-
|
|
52
|
+
A JSON Logic condition can show or hide a field. The condition can read an earlier
|
|
53
|
+
response or a [predicate](./predicates):
|
|
51
54
|
|
|
52
55
|
```ts
|
|
53
56
|
import { createField } from '@picked/client/plugins/create-experience'
|
|
54
57
|
|
|
55
|
-
// Show email field only
|
|
58
|
+
// Show the email field only when the user chose "email" as a contact method.
|
|
56
59
|
export const emailField = createField({
|
|
57
60
|
id: 'email-input',
|
|
58
61
|
name: 'email-input',
|
|
@@ -70,7 +73,7 @@ export const emailField = createField({
|
|
|
70
73
|
},
|
|
71
74
|
})
|
|
72
75
|
|
|
73
|
-
// Show field
|
|
76
|
+
// Show the field when the predicate returns true.
|
|
74
77
|
export const mailingAddressField = createField({
|
|
75
78
|
id: 'mailing-address',
|
|
76
79
|
name: 'mailing-address',
|
|
@@ -4,15 +4,16 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Content Security Policy
|
|
6
6
|
|
|
7
|
-
The Picked SDK loads resources from the
|
|
7
|
+
The Picked SDK loads resources from the domains below. Add each domain to the CSP
|
|
8
|
+
directive that the table gives.
|
|
8
9
|
|
|
9
10
|
## Required domains
|
|
10
11
|
|
|
11
12
|
| Domain | Directive | Purpose |
|
|
12
13
|
| --- | --- | --- |
|
|
13
|
-
| `api.picked.app` | `connect-src` | API requests, project
|
|
14
|
-
| `evnts.pckd.dev` | `connect-src` | Analytics
|
|
15
|
-
| `img.picked-content.com` | `img-src` | Optimised images
|
|
14
|
+
| `api.picked.app` | `connect-src` | API requests, project options, experiences, and the record of each session |
|
|
15
|
+
| `evnts.pckd.dev` | `connect-src` | Analytics events, which Snowplow collects |
|
|
16
|
+
| `img.picked-content.com` | `img-src` | Optimised images, which the [image optimisation service](../image-optimisation-service.md) serves |
|
|
16
17
|
|
|
17
18
|
## Configuration
|
|
18
19
|
|
|
@@ -4,11 +4,18 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Experiences
|
|
6
6
|
|
|
7
|
-
Define
|
|
7
|
+
Define an experience in code with `createExperience()`. You can also load an
|
|
8
|
+
experience from the dashboard at runtime with the
|
|
9
|
+
[Dynamic Experiences Plugin](./plugins/dynamic-experiences).
|
|
8
10
|
|
|
9
|
-
The
|
|
11
|
+
The flow below points at a DOM selector and renders into it. A Page experience
|
|
12
|
+
renders through a named slot instead. Point it at `.picked-slot-<name>` and render
|
|
13
|
+
`<ExperienceSlot name="<name>" />` on your server. Use the slot path for a Page that
|
|
14
|
+
the server renders and that search engines must read. Use the selector path for a
|
|
15
|
+
Flow that the browser renders. See [Server-Side Rendering](/sdk/ssr) and
|
|
16
|
+
[Slots and Placements](/sdk/ssr/slots-and-placements).
|
|
10
17
|
|
|
11
|
-
##
|
|
18
|
+
## Create an experience
|
|
12
19
|
|
|
13
20
|
```ts
|
|
14
21
|
import { createExperience } from '@picked/client/plugins/create-experience'
|
|
@@ -32,15 +39,26 @@ export const productPageExperience = createExperience({
|
|
|
32
39
|
})
|
|
33
40
|
```
|
|
34
41
|
|
|
35
|
-
|
|
42
|
+
To register an experience that you define in code, use the
|
|
43
|
+
[Create Experience Plugin](./plugins/create-experience). To read an experience from
|
|
44
|
+
the dashboard at runtime and control it there, use the
|
|
45
|
+
[Dynamic Experiences Plugin](./plugins/dynamic-experiences).
|
|
36
46
|
|
|
37
47
|
## How experiences work
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
After you register an experience, the SDK follows this flow:
|
|
40
50
|
|
|
41
|
-
1. **Registration**:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
1. **Registration**: You register the experience on the Picked client. Use
|
|
52
|
+
`createExperiencePlugin` for an experience that you define in code. Use
|
|
53
|
+
`dynamicExperiencesPlugin` for one that you control from the dashboard.
|
|
54
|
+
2. **Condition evaluation**: The SDK evaluates the `condition` of the experience with
|
|
55
|
+
the logic engine. The result decides whether a visitor sees it.
|
|
56
|
+
3. **Target resolution**: The SDK searches the DOM for an element that matches a
|
|
57
|
+
selector in `targets`. Examples are `body`, `.product-page`, and `#checkout`.
|
|
58
|
+
4. **Initialization**: The SDK starts the experience when the condition is `true` and
|
|
59
|
+
at least one target element is there.
|
|
60
|
+
5. **Component rendering**: The SDK renders the component of the experience. It puts
|
|
61
|
+
the component into the DOM at the target and the placement that you set. A
|
|
62
|
+
placement is `before`, `after`, `append`, `prepend`, or `replace`.
|
|
63
|
+
6. **Flow execution**: An experience can hold steps, fields, and conditions. A user
|
|
64
|
+
then moves through the flow. The flow checks each answer and takes a branch.
|
|
@@ -4,87 +4,108 @@ layout: docs
|
|
|
4
4
|
|
|
5
5
|
# Allocation and exposure
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A read of the bucket of a feature flag is what allocates a visitor to that feature flag. This
|
|
8
|
+
page covers when that happens and how to watch it. It also covers how to be sure that
|
|
9
|
+
Picked allocates a visitor before you report on that visitor.
|
|
8
10
|
|
|
9
|
-
##
|
|
11
|
+
## A registration is not a decision
|
|
10
12
|
|
|
11
|
-
The SDK loads every feature flag in your project at startup, but it
|
|
13
|
+
The SDK loads every feature flag in your project at startup, but it allocates no visitor
|
|
14
|
+
then. A feature flag stays undecided until something reads its bucket. The first read runs the
|
|
15
|
+
`decide` function, stores the result, and reports the decision.
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
You therefore choose when Picked commits a visitor to a feature flag. You choose it by the
|
|
18
|
+
point at which you read the feature flag.
|
|
14
19
|
|
|
15
|
-
##
|
|
20
|
+
## A read of a bucket records an exposure
|
|
16
21
|
|
|
17
|
-
|
|
22
|
+
The first read is the decision, so a read of a bucket is the moment when the visitor
|
|
23
|
+
enters the test. If you read every feature flag at startup, every visitor counts as exposed
|
|
24
|
+
to every test. This includes a test that the visitor never reached.
|
|
18
25
|
|
|
19
26
|
:::warning
|
|
20
|
-
|
|
27
|
+
Do not loop over every feature flag at startup to read its bucket. That commits every visitor
|
|
28
|
+
to every test immediately. You then have no way to wait for the data that a decision
|
|
29
|
+
needs.
|
|
21
30
|
:::
|
|
22
31
|
|
|
23
|
-
Read a feature flag at the point the decision matters
|
|
32
|
+
Read a feature flag at the point where the decision matters. That is after the cart loads,
|
|
33
|
+
after the quote returns, or after you know who the visitor is.
|
|
24
34
|
|
|
25
|
-
##
|
|
35
|
+
## Check a feature flag without a decision
|
|
26
36
|
|
|
27
|
-
`client.features.listStore` holds every registered feature flag. Each entry reports whether
|
|
37
|
+
`client.features.listStore` holds every registered feature flag. Each entry reports whether
|
|
38
|
+
the feature flag is decided yet, so you can check a feature flag and commit nobody:
|
|
28
39
|
|
|
29
40
|
```ts
|
|
30
|
-
for (const flag of client.features.listStore.getState()) {
|
|
31
|
-
console.log(flag.id, flag.isResolved)
|
|
41
|
+
for (const feature flag of client.features.listStore.getState()) {
|
|
42
|
+
console.log(feature flag.id, feature flag.isResolved)
|
|
32
43
|
}
|
|
33
44
|
```
|
|
34
45
|
|
|
35
|
-
`getBucket()` behaves differently
|
|
46
|
+
`getBucket()` behaves differently for each state:
|
|
36
47
|
|
|
37
|
-
- **Resolved** — returns `{ id, bucket }`
|
|
38
|
-
- **Not resolved** — returns a promise.
|
|
48
|
+
- **Resolved** — it returns `{ id, bucket }` at once. The read changes nothing.
|
|
49
|
+
- **Not resolved** — it returns a promise. An `await` runs `decide` and commits the
|
|
50
|
+
visitor.
|
|
39
51
|
|
|
40
|
-
`await` works for both, so you can treat them
|
|
52
|
+
`await` works for both states, so you can treat them the same way when you do intend
|
|
53
|
+
to decide.
|
|
41
54
|
|
|
42
55
|
## When `feature_decide` fires
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
The SDK sends a `feature_decide` event the first time that it decides a feature flag for a
|
|
58
|
+
visitor, and only then.
|
|
45
59
|
|
|
46
60
|
:::warning
|
|
47
|
-
On later page
|
|
61
|
+
On a later page load the SDK restores the decision from browser storage, and it
|
|
62
|
+
sends **no event**. A visitor who returns produces no `feature_decide` event at all,
|
|
63
|
+
although Picked still allocates that visitor.
|
|
48
64
|
|
|
49
|
-
An integration
|
|
65
|
+
An integration that reads only `feature_decide` therefore reports nothing for a
|
|
66
|
+
visitor who returns.
|
|
50
67
|
:::
|
|
51
68
|
|
|
52
|
-
Use the
|
|
69
|
+
Use the correct source for your question:
|
|
53
70
|
|
|
54
71
|
| Question | Use |
|
|
55
72
|
| --- | --- |
|
|
56
|
-
| What is this visitor allocated to
|
|
57
|
-
|
|
|
58
|
-
|
|
|
73
|
+
| What is this visitor allocated to now? | `client.features.listStore` |
|
|
74
|
+
| Did the SDK make a new decision? | `feature_decide` on `client.eventBus` |
|
|
75
|
+
| Which decisions happened before I subscribed? | `client.events` |
|
|
59
76
|
|
|
60
|
-
##
|
|
77
|
+
## Watch for decisions
|
|
61
78
|
|
|
62
|
-
`listStore` updates
|
|
79
|
+
`listStore` updates each time that a feature flag resolves. It does this whether the SDK
|
|
80
|
+
restored the feature flag from storage or decided it now. A watch on `listStore` therefore
|
|
81
|
+
always gives you the current state:
|
|
63
82
|
|
|
64
83
|
```ts
|
|
65
84
|
const buckets = new Map<string, string | boolean>()
|
|
66
85
|
|
|
67
|
-
client.features.listStore.watch((flags) => {
|
|
68
|
-
for (const flag of flags) {
|
|
69
|
-
if (!flag.isResolved) continue
|
|
70
|
-
buckets.set(flag.id, flag.getBucket().bucket)
|
|
86
|
+
client.features.listStore.watch((feature flags) => {
|
|
87
|
+
for (const feature flag of feature flags) {
|
|
88
|
+
if (!feature flag.isResolved) continue
|
|
89
|
+
buckets.set(feature flag.id, feature flag.getBucket().bucket)
|
|
71
90
|
}
|
|
72
91
|
})
|
|
73
92
|
```
|
|
74
93
|
|
|
75
|
-
Use `feature_decide` when you need to know
|
|
94
|
+
Use `feature_decide` when you need to know that the SDK *made* a decision, and not
|
|
95
|
+
what the current value is. An example is your own exposure event:
|
|
76
96
|
|
|
77
97
|
```ts
|
|
78
98
|
client.eventBus.watch((event) => {
|
|
79
99
|
if (event.type !== 'feature_decide') return
|
|
80
100
|
analytics.track('Experiment exposure', {
|
|
81
|
-
flag: event.feature.id,
|
|
101
|
+
feature flag: event.feature.id,
|
|
82
102
|
bucket: event.feature.bucket,
|
|
83
103
|
})
|
|
84
104
|
})
|
|
85
105
|
```
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
An earlier event is still available when you subscribe after startup. `client.events`
|
|
108
|
+
holds every event since the client started:
|
|
88
109
|
|
|
89
110
|
```ts
|
|
90
111
|
const earlier = client.events
|
|
@@ -92,38 +113,49 @@ const earlier = client.events
|
|
|
92
113
|
.filter((entry) => entry.payload.type === 'feature_decide')
|
|
93
114
|
```
|
|
94
115
|
|
|
95
|
-
##
|
|
116
|
+
## Decide before you report
|
|
96
117
|
|
|
97
|
-
To
|
|
118
|
+
To be sure that the SDK decides a feature flag before a later event fires, `await` the feature flag
|
|
119
|
+
first:
|
|
98
120
|
|
|
99
121
|
```ts
|
|
100
|
-
const flag = client.features.getById('quote-experiment').getState()
|
|
122
|
+
const feature flag = client.features.getById('quote-experiment').getState()
|
|
101
123
|
|
|
102
|
-
if (flag) {
|
|
103
|
-
const { bucket } = await flag.getBucket()
|
|
124
|
+
if (feature flag) {
|
|
125
|
+
const { bucket } = await feature flag.getBucket()
|
|
104
126
|
trackViewQuote({ experiment: bucket })
|
|
105
127
|
}
|
|
106
128
|
```
|
|
107
129
|
|
|
108
|
-
|
|
130
|
+
An `await` on `getBucket()` resolves the feature flag when it is undecided, and returns the
|
|
131
|
+
stored value when it is decided. In both cases the bucket is known before the next
|
|
132
|
+
line runs.
|
|
109
133
|
|
|
110
|
-
## What Picked
|
|
134
|
+
## What Picked reports for you
|
|
111
135
|
|
|
112
|
-
|
|
136
|
+
Read this before you build a report of your own.
|
|
113
137
|
|
|
114
|
-
Every Picked analytics event carries a `feature_list` context
|
|
138
|
+
Every Picked analytics event carries a `feature_list` context. The context describes
|
|
139
|
+
each feature flag that this visitor resolved:
|
|
115
140
|
|
|
116
141
|
| Field | Description |
|
|
117
142
|
| --- | --- |
|
|
118
|
-
| `ab_feature` |
|
|
119
|
-
| `ab_name` |
|
|
120
|
-
| `ab_key` |
|
|
143
|
+
| `ab_feature` | The feature flag ID |
|
|
144
|
+
| `ab_name` | The feature flag name |
|
|
145
|
+
| `ab_key` | The version key, such as `v3` |
|
|
121
146
|
| `ab_bucket` | The resolved bucket |
|
|
122
147
|
|
|
123
|
-
Only resolved feature
|
|
148
|
+
Only a resolved feature flag appears there. The context therefore shows what the visitor was
|
|
149
|
+
exposed to at the time when the event fired.
|
|
124
150
|
|
|
125
|
-
|
|
151
|
+
The [Sentry plugin](https://www.npmjs.com/package/@picked/sentry) is optional. When
|
|
152
|
+
it is installed, each decision also tags the Sentry scope as
|
|
153
|
+
`picked.feature.<feature flag-id>`, with the bucket as the value. You can then filter your
|
|
154
|
+
errors by the variant.
|
|
126
155
|
|
|
127
156
|
## Preview mode
|
|
128
157
|
|
|
129
|
-
In [preview mode](/workspaces#preview)
|
|
158
|
+
In [preview mode](/workspaces#preview) the SDK stores a decision in session storage
|
|
159
|
+
and not in local storage. The decision does not stay after you close the tab, and
|
|
160
|
+
Picked can allocate a preview visitor differently from a production visitor. Treat a
|
|
161
|
+
preview allocation as disposable.
|