@picked/client 0.0.101-next.2 → 0.0.101
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/README.md +34 -158
- package/dist/{api-BlRFU0Fi.js → api-BOKA2i46.js} +1 -1
- package/dist/{api-AaQeg3Ij.cjs → api-C5jUHF3A.cjs} +1 -1
- package/dist/cjs/index.js +2 -2
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/cjs/server/index.js +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
- package/dist/esm/server/index.js +2 -2
- package/dist/{plugin-CVD2TnRC.cjs → plugin-BcB9ah7q.cjs} +1 -1
- package/dist/{plugin-DhznDG-h.js → plugin-COWxZiTa.js} +2 -2
- package/dist/{preview-unavailable-mW4Pj3Ph.js → preview-unavailable-BJjc3UNr.js} +1 -1
- package/dist/{preview-unavailable-xWLCr93y.cjs → preview-unavailable-CV1BonKA.cjs} +1 -1
- package/dist/{provider-COb_V-yq.js → provider-CszmtTg3.js} +1 -1
- package/dist/{provider-zag5m5GH.cjs → provider-daBChM0G.cjs} +1 -1
- package/package.json +5 -5
- package/dist/docs/_root.css +0 -101
- package/dist/docs/explanation/conditions-and-the-logic-engine.md +0 -94
- package/dist/docs/explanation/content-references.md +0 -99
- package/dist/docs/explanation/feature-flag-allocation.md +0 -218
- package/dist/docs/explanation/how-picked-works.md +0 -124
- package/dist/docs/explanation/how-pickedql-works.md +0 -76
- package/dist/docs/explanation/pages-and-flows.md +0 -64
- package/dist/docs/explanation/the-content-model.md +0 -87
- package/dist/docs/explanation/the-two-apis.md +0 -68
- package/dist/docs/explanation/why-server-side-rendering.md +0 -76
- package/dist/docs/explanation/workspaces-and-versions.md +0 -71
- package/dist/docs/how-to/add-a-step-field-or-action.md +0 -43
- package/dist/docs/how-to/analytics/index.md +0 -316
- package/dist/docs/how-to/archive-or-delete-a-workspace.md +0 -46
- package/dist/docs/how-to/authenticate-the-cli.md +0 -101
- package/dist/docs/how-to/authenticate-with-a-magic-link.md +0 -47
- package/dist/docs/how-to/authenticate-with-a-password.md +0 -59
- package/dist/docs/how-to/authenticate-with-an-api-key.md +0 -51
- package/dist/docs/how-to/authenticate-with-oauth.md +0 -40
- package/dist/docs/how-to/build-a-flow.md +0 -93
- package/dist/docs/how-to/call-an-external-api-from-a-workflow.md +0 -102
- package/dist/docs/how-to/connect-the-mcp-server.md +0 -46
- package/dist/docs/how-to/create-a-content-entry.md +0 -48
- package/dist/docs/how-to/create-a-project.md +0 -29
- package/dist/docs/how-to/create-an-experience.md +0 -42
- package/dist/docs/how-to/create-and-revoke-an-api-key.md +0 -42
- package/dist/docs/how-to/debug-with-the-chrome-extension.md +0 -57
- package/dist/docs/how-to/define-a-page-layout.md +0 -46
- package/dist/docs/how-to/fill-and-publish-a-page.md +0 -66
- package/dist/docs/how-to/install-the-gtm-template.md +0 -84
- package/dist/docs/how-to/install-the-sdk.md +0 -93
- package/dist/docs/how-to/install-the-vite-plugin.md +0 -69
- package/dist/docs/how-to/invite-and-remove-members.md +0 -35
- package/dist/docs/how-to/point-one-entry-at-another.md +0 -61
- package/dist/docs/how-to/preview-a-workspace.md +0 -28
- package/dist/docs/how-to/publish-a-workspace.md +0 -30
- package/dist/docs/how-to/read-a-feature-flag.md +0 -113
- package/dist/docs/how-to/read-a-referenced-entry.md +0 -109
- package/dist/docs/how-to/register-a-component.md +0 -55
- package/dist/docs/how-to/register-an-action-handler.md +0 -85
- package/dist/docs/how-to/register-an-api-that-uses-oauth2.md +0 -107
- package/dist/docs/how-to/render-a-step-form.md +0 -131
- package/dist/docs/how-to/render-an-unpublished-preview.md +0 -41
- package/dist/docs/how-to/retry-a-failed-call-with-a-loop.md +0 -81
- package/dist/docs/how-to/reuse-a-section.md +0 -53
- package/dist/docs/how-to/reuse-a-step-field-or-action.md +0 -28
- package/dist/docs/how-to/roll-back-a-published-version.md +0 -26
- package/dist/docs/how-to/run-an-ab-test.md +0 -49
- package/dist/docs/how-to/run-code-on-a-form-submission.md +0 -125
- package/dist/docs/how-to/server-render-a-page.md +0 -81
- package/dist/docs/how-to/set-a-content-security-policy.md +0 -32
- package/dist/docs/how-to/set-up-mfa.md +0 -113
- package/dist/docs/how-to/switch-workspaces.md +0 -122
- package/dist/docs/how-to/upload-and-use-an-asset.mdx +0 -73
- package/dist/docs/how-to/validate-a-field.md +0 -52
- package/dist/docs/how-to/wait-for-an-action-response.md +0 -182
- package/dist/docs/how-to/write-a-condition.md +0 -45
- package/dist/docs/how-to/write-a-predicate.md +0 -60
- package/dist/docs/index.md +0 -59
- package/dist/docs/reference/dashboard/actions.mdx +0 -56
- package/dist/docs/reference/dashboard/api-keys.mdx +0 -38
- package/dist/docs/reference/dashboard/conditions.mdx +0 -42
- package/dist/docs/reference/dashboard/content/asset-library.mdx +0 -63
- package/dist/docs/reference/dashboard/content/blocks.mdx +0 -57
- package/dist/docs/reference/dashboard/content/content-entries.mdx +0 -95
- package/dist/docs/reference/dashboard/content/models.mdx +0 -108
- package/dist/docs/reference/dashboard/content/pages.mdx +0 -61
- package/dist/docs/reference/dashboard/experiences.mdx +0 -51
- package/dist/docs/reference/dashboard/external-apis.md +0 -169
- package/dist/docs/reference/dashboard/features.mdx +0 -154
- package/dist/docs/reference/dashboard/fields.mdx +0 -106
- package/dist/docs/reference/dashboard/flow-builder/canvas.mdx +0 -58
- package/dist/docs/reference/dashboard/flow-builder/condition-builder.md +0 -57
- package/dist/docs/reference/dashboard/flow-builder/keyboard-shortcuts.md +0 -72
- package/dist/docs/reference/dashboard/flow-builder/nodes.md +0 -63
- package/dist/docs/reference/dashboard/labels.md +0 -321
- package/dist/docs/reference/dashboard/lead-search.md +0 -224
- package/dist/docs/reference/dashboard/projects.mdx +0 -44
- package/dist/docs/reference/dashboard/reports.mdx +0 -43
- package/dist/docs/reference/dashboard/steps.mdx +0 -37
- package/dist/docs/reference/dashboard/team-management.md +0 -29
- package/dist/docs/reference/dashboard/workflow-runs.md +0 -142
- package/dist/docs/reference/dashboard/workflow-templates.md +0 -182
- package/dist/docs/reference/dashboard/workflow-test-runs.md +0 -77
- package/dist/docs/reference/dashboard/workspace-versioning.mdx +0 -30
- package/dist/docs/reference/dashboard/workspaces.mdx +0 -76
- package/dist/docs/reference/delivery-api/experience-content.md +0 -152
- package/dist/docs/reference/delivery-api/index.md +0 -45
- package/dist/docs/reference/delivery-api/preview-caching-crawling.md +0 -79
- package/dist/docs/reference/delivery-api/project-config.md +0 -65
- package/dist/docs/reference/delivery-api/workspace-schema.md +0 -107
- package/dist/docs/reference/glossary.md +0 -201
- package/dist/docs/reference/management-api/authentication.md +0 -87
- package/dist/docs/reference/management-api/authorization-and-scopes.md +0 -291
- package/dist/docs/reference/management-api/error-handling.md +0 -232
- package/dist/docs/reference/management-api/pagination.md +0 -192
- package/dist/docs/reference/pickedql/data-sources.md +0 -174
- package/dist/docs/reference/pickedql/dimensions.md +0 -103
- package/dist/docs/reference/pickedql/filtering.md +0 -64
- package/dist/docs/reference/pickedql/index.md +0 -18
- package/dist/docs/reference/pickedql/schema.md +0 -172
- package/dist/docs/reference/pickedql/virtual-functions.md +0 -248
- package/dist/docs/reference/sdk/actions.md +0 -154
- package/dist/docs/reference/sdk/analytics-events.md +0 -453
- package/dist/docs/reference/sdk/conditions.md +0 -147
- package/dist/docs/reference/sdk/content-security-policy.md +0 -21
- package/dist/docs/reference/sdk/content.md +0 -253
- package/dist/docs/reference/sdk/experiences.md +0 -51
- package/dist/docs/reference/sdk/features.md +0 -256
- package/dist/docs/reference/sdk/fields.md +0 -255
- package/dist/docs/reference/sdk/index.md +0 -68
- package/dist/docs/reference/sdk/plugins/create-experience.md +0 -123
- package/dist/docs/reference/sdk/plugins/dynamic-experiences.md +0 -74
- package/dist/docs/reference/sdk/plugins/index.md +0 -29
- package/dist/docs/reference/sdk/plugins/window-object.md +0 -25
- package/dist/docs/reference/sdk/predicates.md +0 -43
- package/dist/docs/reference/sdk/ssr/index.md +0 -33
- package/dist/docs/reference/sdk/ssr/rendering-and-hydration.md +0 -103
- package/dist/docs/reference/sdk/ssr/serialize.md +0 -80
- package/dist/docs/reference/sdk/ssr/slots-and-placements.md +0 -113
- package/dist/docs/reference/sdk/steps.md +0 -86
- package/dist/docs/reference/sdk/type-generation.md +0 -265
- package/dist/docs/reference/tools/chrome-extension.md +0 -16
- package/dist/docs/reference/tools/cli.md +0 -292
- package/dist/docs/reference/tools/image-optimisation-service.md +0 -104
- package/dist/docs/reference/tools/mcp.md +0 -136
- package/dist/docs/reference/tools/vite-plugin.md +0 -163
- package/dist/docs/tutorials/first-api-call.md +0 -235
- package/dist/docs/tutorials/first-experience-in-the-dashboard.mdx +0 -143
- package/dist/docs/tutorials/first-experience.mdx +0 -254
- package/dist/docs/tutorials/first-server-rendered-page.mdx +0 -186
package/dist/docs/_root.css
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Site font. Vocs 2 has no `font` config option, so Inter is declared here.
|
|
3
|
-
*/
|
|
4
|
-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap');
|
|
5
|
-
|
|
6
|
-
:root {
|
|
7
|
-
--vocs-fontFamily-default: 'Inter', sans-serif;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/*
|
|
11
|
-
* Embed mode — activated by the ?embed query param.
|
|
12
|
-
* Hides site chrome (gutters, top nav, sidebar, outline, footer) so the docs
|
|
13
|
-
* content can be displayed inside an iframe in the dashboard's docs panel.
|
|
14
|
-
*
|
|
15
|
-
* Every selector targets a `data-v-*` attribute. These are the hooks Vocs
|
|
16
|
-
* publishes for styling its layout, and they are what this file must use:
|
|
17
|
-
* matching on class names instead would bind to generated Tailwind utilities
|
|
18
|
-
* that carry no compatibility promise, and a rename would break the dashboard's
|
|
19
|
-
* docs panel silently — build and tests stay green while the panel renders with
|
|
20
|
-
* a sidebar and a top nav inside it.
|
|
21
|
-
*
|
|
22
|
-
* Nothing automated covers the visual result. After any Vocs upgrade, open the
|
|
23
|
-
* dashboard docs panel and look at it.
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
/*
|
|
27
|
-
* Hide the gutters. These hold the top nav, the sidebar and the outline, so
|
|
28
|
-
* hiding them removes the chrome in one step.
|
|
29
|
-
*
|
|
30
|
-
* Do NOT add `[data-v-sidebar]` or `[data-v-topnav]` here. Those sit on the
|
|
31
|
-
* layout root as flags describing which regions the layout has — the root
|
|
32
|
-
* carries both — so hiding them blanks the entire page, content included.
|
|
33
|
-
*/
|
|
34
|
-
.vocs-embed [data-v-gutter-top],
|
|
35
|
-
.vocs-embed [data-v-gutter-top-left],
|
|
36
|
-
.vocs-embed [data-v-gutter-left],
|
|
37
|
-
.vocs-embed [data-v-gutter-right],
|
|
38
|
-
.vocs-embed [data-v-gutter-logo],
|
|
39
|
-
.vocs-embed [data-v-mobile-nav] {
|
|
40
|
-
display: none !important;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/* Hide the sidebar's own container and the curtains behind it */
|
|
44
|
-
.vocs-embed [data-v-sidebar-container],
|
|
45
|
-
.vocs-embed [data-v-sidebar-curtain],
|
|
46
|
-
.vocs-embed [data-v-sidebar-footer-curtain] {
|
|
47
|
-
display: none !important;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/*
|
|
51
|
-
* Hide the surface layer that paints the sidebar and top-nav backgrounds.
|
|
52
|
-
* It sits behind the chrome rather than inside it, so hiding the gutters alone
|
|
53
|
-
* leaves its fill behind as a ghost panel down the left edge.
|
|
54
|
-
*/
|
|
55
|
-
.vocs-embed [data-v-surface-bg] {
|
|
56
|
-
display: none !important;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/*
|
|
60
|
-
* Hide the "Ask AI" prompt. The dashboard embeds these pages beside its own
|
|
61
|
-
* agent, so a second assistant entry point inside the panel competes with it.
|
|
62
|
-
*/
|
|
63
|
-
.vocs-embed [data-v-ask-ai-container] {
|
|
64
|
-
display: none !important;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/*
|
|
68
|
-
* Hide the mobile outline trigger, the footer, the skip link and the bottom
|
|
69
|
-
* observer. The outline and footer render on the client rather than into the
|
|
70
|
-
* prerendered HTML, so they are covered here even though a static build of this
|
|
71
|
-
* site does not emit them.
|
|
72
|
-
*/
|
|
73
|
-
.vocs-embed [data-v-outline],
|
|
74
|
-
.vocs-embed [data-v-outline-mobile],
|
|
75
|
-
.vocs-embed [data-v-footer],
|
|
76
|
-
.vocs-embed [data-v-skip-to-content],
|
|
77
|
-
.vocs-embed [data-bottom-observer],
|
|
78
|
-
.vocs-embed footer {
|
|
79
|
-
display: none !important;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/* Reclaim the space the hidden gutters and top nav were holding */
|
|
83
|
-
.vocs-embed [data-v-main] {
|
|
84
|
-
margin-left: 0 !important;
|
|
85
|
-
padding-top: 0 !important;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/* Expand content to fill the embed viewport */
|
|
89
|
-
.vocs-embed [data-v-content] {
|
|
90
|
-
max-width: 100% !important;
|
|
91
|
-
margin-left: auto !important;
|
|
92
|
-
margin-right: auto !important;
|
|
93
|
-
padding-left: 1.25rem !important;
|
|
94
|
-
padding-right: 1.25rem !important;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* Remove the top margin on the first child so there is no gap at the panel top */
|
|
98
|
-
.vocs-embed article > :first-child,
|
|
99
|
-
.vocs-embed [data-v-content] > :first-child {
|
|
100
|
-
margin-top: 0 !important;
|
|
101
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: explanation
|
|
3
|
-
layout: docs
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# About conditions and the logic engine
|
|
7
|
-
|
|
8
|
-
A condition decides whether a visitor sees something. One rule format covers every one of
|
|
9
|
-
those decisions, both in the dashboard and in code. This page covers why there is one
|
|
10
|
-
engine, and what it can reach.
|
|
11
|
-
|
|
12
|
-
## One rule format, many places
|
|
13
|
-
|
|
14
|
-
The dashboard uses the visual condition builder in four places, and the SDK evaluates the
|
|
15
|
-
same rules in each:
|
|
16
|
-
|
|
17
|
-
- **Condition nodes** decide which branch a flow takes.
|
|
18
|
-
- **Experience conditions** decide whether an experience starts for a visitor.
|
|
19
|
-
- **Initialize-open conditions** decide whether an experience opens by itself.
|
|
20
|
-
- **Field option conditions** show or hide one option in a field.
|
|
21
|
-
|
|
22
|
-
One format means one model to learn. Learn how a rule reads in the flow builder, and you
|
|
23
|
-
can read the rule that gates an experience.
|
|
24
|
-
|
|
25
|
-
## What a rule looks like below
|
|
26
|
-
|
|
27
|
-
A rule is JSON Logic. The visual builder writes it for you, and you can also write it
|
|
28
|
-
directly in SDK code:
|
|
29
|
-
|
|
30
|
-
```ts
|
|
31
|
-
{
|
|
32
|
-
'==': [{ var: 'responses.country' }, 'US']
|
|
33
|
-
}
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Rules sit in groups, and each group is an **AND** or an **OR**. Put a group inside another
|
|
37
|
-
group to build a larger rule.
|
|
38
|
-
|
|
39
|
-
The format is data, and not code. That is the point. A rule travels from the dashboard to
|
|
40
|
-
the browser inside the published snapshot. A function cannot travel that way.
|
|
41
|
-
|
|
42
|
-
## What a rule can read
|
|
43
|
-
|
|
44
|
-
| Source | What it holds |
|
|
45
|
-
| --- | --- |
|
|
46
|
-
| **Responses** | The values your visitors send through the fields of the current experience. |
|
|
47
|
-
| **Actions** | The actions that ran during the flow. |
|
|
48
|
-
| **Feature flags** | Whether Picked allocates this visitor to a flag. |
|
|
49
|
-
| **Location** | The parts of the current URL: pathname, search parameters, hostname, and hash. |
|
|
50
|
-
|
|
51
|
-
Each one is state the SDK already holds, which is why a rule evaluates without a network
|
|
52
|
-
request.
|
|
53
|
-
|
|
54
|
-
## Predicates are the escape hatch
|
|
55
|
-
|
|
56
|
-
The four sources above cover what Picked knows. They do not cover what *your application*
|
|
57
|
-
knows: a cart total, an account level, or the profile of a signed-in user.
|
|
58
|
-
|
|
59
|
-
A **predicate** gives you that. It is a function that you register on the client, and a
|
|
60
|
-
rule invokes it by name:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
{ invoke: { var: 'predicates.has-items-in-cart' } }
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
The predicate receives a frozen state snapshot and returns a boolean, a string, an array
|
|
67
|
-
of strings, or `undefined`. Because a rule can invoke one, the visual builder in the
|
|
68
|
-
dashboard reaches your application data, and nobody writes JSON Logic by hand.
|
|
69
|
-
|
|
70
|
-
A predicate must stay pure and fast. The SDK can invoke it many times while it evaluates a
|
|
71
|
-
flow, and everything that waits on the rule waits on the predicate.
|
|
72
|
-
|
|
73
|
-
## Why a precondition is not a condition
|
|
74
|
-
|
|
75
|
-
The client takes two gates, and they answer different questions:
|
|
76
|
-
|
|
77
|
-
| Gate | Question |
|
|
78
|
-
| --- | --- |
|
|
79
|
-
| Precondition | Must Picked start at all? |
|
|
80
|
-
| Condition | Is this visitor eligible? |
|
|
81
|
-
|
|
82
|
-
A precondition runs at startup, and that has a result which catches people out. A
|
|
83
|
-
precondition that points at a feature flag reads the bucket of that flag, and a read is
|
|
84
|
-
what allocates the visitor. Every visitor is therefore allocated to that flag before
|
|
85
|
-
anything else happens, and that includes a visitor who never reaches the experience the
|
|
86
|
-
flag controls. See
|
|
87
|
-
[About feature flag allocation](/explanation/feature-flag-allocation).
|
|
88
|
-
|
|
89
|
-
## Related
|
|
90
|
-
|
|
91
|
-
- [Condition builder](/reference/dashboard/flow-builder/condition-builder) — the visual
|
|
92
|
-
editor.
|
|
93
|
-
- [SDK conditions](/reference/sdk/conditions) — the JSON Logic shapes.
|
|
94
|
-
- [Predicates](/reference/sdk/predicates) — the state snapshot and the return types.
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: explanation
|
|
3
|
-
layout: docs
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# About content references
|
|
7
|
-
|
|
8
|
-
A [Reference field](/reference/dashboard/content/models#reference-fields) points one
|
|
9
|
-
content entry at another. Why a pointer, when Picked can put the content into the entry
|
|
10
|
-
that needs it?
|
|
11
|
-
|
|
12
|
-
## A pointer keeps one fact in one place
|
|
13
|
-
|
|
14
|
-
A product points at its category. Ten products can point at the same category.
|
|
15
|
-
|
|
16
|
-
A copy makes ten categories. Correct the name of the category and you must correct it ten
|
|
17
|
-
times. The copy that you do not correct is now wrong.
|
|
18
|
-
|
|
19
|
-
A pointer makes one category. Correct the name one time, and every product that points at
|
|
20
|
-
it is correct.
|
|
21
|
-
|
|
22
|
-
This is the same reason that Blocks exist, one level up. A Block shares a *shape* between
|
|
23
|
-
entries. A reference shares an *entry* between entries.
|
|
24
|
-
|
|
25
|
-
## The target travels beside the content
|
|
26
|
-
|
|
27
|
-
Picked sends the entries of your steps in `content`. It sends the entries that a
|
|
28
|
-
reference reaches in `references`. Both go into the same content index in the SDK, keyed
|
|
29
|
-
by the id.
|
|
30
|
-
|
|
31
|
-
Picked can put the target inside the field instead, and it did so before. Three things
|
|
32
|
-
count against that.
|
|
33
|
-
|
|
34
|
-
A shared target costs one blob, and not one blob for each entry that points at it. Ten
|
|
35
|
-
products with the same category send the category one time.
|
|
36
|
-
|
|
37
|
-
A separate entry has an id. Your code reads it with `useContent(id)`, and live preview
|
|
38
|
-
updates it while you edit. A target inside a field has no id, so an update finds nothing
|
|
39
|
-
to change.
|
|
40
|
-
|
|
41
|
-
The reader chooses how deep to go. A component that shows a category reads the category.
|
|
42
|
-
A component that shows a card reads the product only, and it costs nothing more.
|
|
43
|
-
|
|
44
|
-
## Picked stops a chain at a depth of five
|
|
45
|
-
|
|
46
|
-
Content can point at content that points at content. Such a chain has no end of its own.
|
|
47
|
-
A cycle has none at all. Two entries that point at each other keep the chain open for as
|
|
48
|
-
long as something asks.
|
|
49
|
-
|
|
50
|
-
Picked therefore resolves a chain to a depth of five. It counts the depth from the
|
|
51
|
-
entries that your steps point at. It also keeps the entries that it saw, so that a cycle
|
|
52
|
-
stops.
|
|
53
|
-
|
|
54
|
-
Five is a choice, and not a rule of the system. It is deep enough for the shapes that
|
|
55
|
-
content takes, such as a product, its category, and the owner of that category. It is
|
|
56
|
-
also small enough that one page cannot pull a whole workspace over the wire.
|
|
57
|
-
|
|
58
|
-
## A missing entry is how Picked says "no such entry"
|
|
59
|
-
|
|
60
|
-
Picked sends no blob for a pointer whose target it cannot resolve. The entry is absent
|
|
61
|
-
from `references`.
|
|
62
|
-
|
|
63
|
-
The other option is a blob with no data. That reads as an entry that exists and is empty,
|
|
64
|
-
which is a different fact from an entry that is not there.
|
|
65
|
-
|
|
66
|
-
Your code cannot tell one from the other. The empty record also goes into the content
|
|
67
|
-
index under an id that resolves. A missing entry looks like nothing else.
|
|
68
|
-
|
|
69
|
-
## A delete stops while a reference is live
|
|
70
|
-
|
|
71
|
-
Picked does not delete an entry that another entry points at. It does not delete a Model
|
|
72
|
-
that a Reference field holds as its target.
|
|
73
|
-
|
|
74
|
-
The other option is a pointer at nothing. Picked must then choose what the field holds,
|
|
75
|
-
and every answer is wrong. It can give your component an empty value that the component
|
|
76
|
-
does not expect. It can also remove content that somebody wrote, with no report.
|
|
77
|
-
|
|
78
|
-
A stop costs you one step. Clear the reference, then delete the target. In return, a
|
|
79
|
-
field that holds a pointer always points at something that exists.
|
|
80
|
-
|
|
81
|
-
## A reference stays inside its workspace
|
|
82
|
-
|
|
83
|
-
A field cannot point at an entry in another workspace. Picked reports such a target as
|
|
84
|
-
unknown.
|
|
85
|
-
|
|
86
|
-
A workspace is one version of the configuration of a project, and you publish it whole. A
|
|
87
|
-
pointer that leaves the workspace resolves against content that this workspace never
|
|
88
|
-
sent. The two sides can then go live at different times.
|
|
89
|
-
|
|
90
|
-
The delivery surface applies the same rule on a read, so a target outside the workspace
|
|
91
|
-
never reaches a visitor.
|
|
92
|
-
|
|
93
|
-
## Related
|
|
94
|
-
|
|
95
|
-
- [Models](/reference/dashboard/content/models) — the field and its options.
|
|
96
|
-
- [Content entries](/reference/dashboard/content/content-entries) — the editor and the delete rules.
|
|
97
|
-
- [Content](/reference/sdk/content) — how your code resolves a pointer.
|
|
98
|
-
- [Experience content endpoint](/reference/delivery-api/experience-content) — the wire shape.
|
|
99
|
-
- [About the content model](/explanation/the-content-model) — why a schema and its data are separate.
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: explanation
|
|
3
|
-
layout: docs
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# About feature flag allocation
|
|
7
|
-
|
|
8
|
-
When does a visitor enter a test, and what decides the group they land in? The answer is
|
|
9
|
-
not "when you create the feature flag" and not "when the page loads". It is the moment something
|
|
10
|
-
reads the bucket, and that moment is yours to choose.
|
|
11
|
-
|
|
12
|
-
## A registration is not a decision
|
|
13
|
-
|
|
14
|
-
The SDK loads every feature flag in your project at startup. It allocates nobody then. A feature flag
|
|
15
|
-
stays undecided until something reads its bucket. The first read runs the `decide`
|
|
16
|
-
function, stores the result, and reports the decision.
|
|
17
|
-
|
|
18
|
-
You therefore choose when Picked commits a visitor to a feature flag. You choose it by the point
|
|
19
|
-
at which you read the feature flag.
|
|
20
|
-
|
|
21
|
-
## A read of the bucket records an exposure
|
|
22
|
-
|
|
23
|
-
The first read is the decision, so it is also the moment the visitor enters the test. If
|
|
24
|
-
you read every feature flag at startup, every visitor counts as exposed to every test. That
|
|
25
|
-
includes a test the visitor never reached.
|
|
26
|
-
|
|
27
|
-
Read a feature flag where the decision matters: after the cart loads, after the quote returns, or
|
|
28
|
-
after you know who the visitor is. A feature flag that you read before that point puts a visitor in
|
|
29
|
-
a test they never saw, and the uplift number then means nothing.
|
|
30
|
-
|
|
31
|
-
## A condition reads the bucket, so a condition allocates
|
|
32
|
-
|
|
33
|
-
Most reads are not calls that you write. They come from a condition. A condition that
|
|
34
|
-
points at a feature flag with `invoke: { var: 'features.<id>' }` must have the bucket
|
|
35
|
-
before it can compare it. That read allocates the visitor, exactly as a call in your own
|
|
36
|
-
code does.
|
|
37
|
-
|
|
38
|
-
Where the condition sits therefore decides who enters the test:
|
|
39
|
-
|
|
40
|
-
| The condition is | It runs | Who enters the test |
|
|
41
|
-
| --- | --- | --- |
|
|
42
|
-
| An experience condition | When Picked evaluates that experience | Only a visitor who reaches the experience |
|
|
43
|
-
| A precondition | At startup, before anything else | Every visitor who loads the page |
|
|
44
|
-
|
|
45
|
-
A precondition that points at a feature flag allocates every visitor on your site, and it
|
|
46
|
-
does so before the visitor sees anything. Keep a feature flag out of a precondition unless
|
|
47
|
-
it truly controls whether Picked starts at all.
|
|
48
|
-
|
|
49
|
-
Keep each `decide` function fast for the same reason. Everything that waits for the
|
|
50
|
-
condition waits for `decide`, and that includes the experience itself. A network call
|
|
51
|
-
inside `decide` delays what the visitor sees.
|
|
52
|
-
|
|
53
|
-
## How Picked chooses
|
|
54
|
-
|
|
55
|
-
For a feature flag you control from the dashboard the choice is deterministic. Picked
|
|
56
|
-
gives each visitor a stable value for each feature flag. It derives that value from the
|
|
57
|
-
visitor ID and from the ID of the feature flag, and it compares the value against the
|
|
58
|
-
weight. Picked allocates the visitor when the value is below the weight.
|
|
59
|
-
|
|
60
|
-
The same visitor gets the same result for one feature flag on every visit, while the
|
|
61
|
-
weight stays the same.
|
|
62
|
-
|
|
63
|
-
That is also why a feature flag you control from the dashboard cannot use the data of
|
|
64
|
-
your application in its decision. The value comes from the visitor ID, the feature flag
|
|
65
|
-
ID, and the weight, and from nothing else. Define the feature flag in SDK code when the
|
|
66
|
-
decision needs a cart, a quote, or a profile.
|
|
67
|
-
|
|
68
|
-
## More than two groups
|
|
69
|
-
|
|
70
|
-
A Text feature flag has a table of variants, and each variant is one group. Picked
|
|
71
|
-
allocates a visitor to exactly one of them, and the control group is one of the rows.
|
|
72
|
-
|
|
73
|
-
The two types decide by different routes, and the difference matters if you compare them:
|
|
74
|
-
|
|
75
|
-
| Type | Picked compares | Against |
|
|
76
|
-
| --- | --- | --- |
|
|
77
|
-
| Boolean | The stable value of the visitor, out of 100 | The weight |
|
|
78
|
-
| Text | One stable value for each variant | The weight of that variant |
|
|
79
|
-
|
|
80
|
-
A Text feature flag gives each variant a score. Picked makes the score from the visitor
|
|
81
|
-
ID, the feature flag ID, and the key of the variant. The weight of the variant then scales
|
|
82
|
-
that score. The variant with the best score receives the visitor.
|
|
83
|
-
|
|
84
|
-
The weight of a variant therefore changes the score of that variant alone. No other
|
|
85
|
-
variant moves because of it.
|
|
86
|
-
|
|
87
|
-
A Boolean feature flag makes the same comparison it always has. A feature flag that you
|
|
88
|
-
made before variants existed therefore keeps every visitor where it put them.
|
|
89
|
-
|
|
90
|
-
Picked derives the stable value from the visitor ID and from the ID of the feature flag,
|
|
91
|
-
for both types. The two types are therefore independent of each other in the same way that
|
|
92
|
-
two feature flags are.
|
|
93
|
-
|
|
94
|
-
**The order of the rows has no effect.** Picked scores each variant from its key, and the
|
|
95
|
-
key of a variant never changes. Move a row in the dashboard and the
|
|
96
|
-
visitors stay where they are. The order that you see is the order that you chose to read
|
|
97
|
-
them in, and nothing more.
|
|
98
|
-
|
|
99
|
-
Rename a variant and nothing changes. The name travels no further than the dashboard.
|
|
100
|
-
|
|
101
|
-
Change the string that a variant serves and the visitors stay in the same group, but your
|
|
102
|
-
site and your reports receive the new string. A visitor with a decision in browser storage
|
|
103
|
-
keeps the old string until you increment the version.
|
|
104
|
-
|
|
105
|
-
## Each feature flag is independent
|
|
106
|
-
|
|
107
|
-
Picked builds that value from the visitor ID together with the ID of the feature flag.
|
|
108
|
-
Each feature flag therefore gets its own value, and tests that run at the same time are
|
|
109
|
-
independent of each other.
|
|
110
|
-
|
|
111
|
-
Two feature flags, both at weight 50, split your visitors into four equal parts:
|
|
112
|
-
allocated to both, to one, to the other, or to neither. Three feature flags at weight 50
|
|
113
|
-
give eight groups of 12.5%. Add a feature flag and the groups divide again.
|
|
114
|
-
|
|
115
|
-
This is what lets you read two experiments separately. Give every feature flag the same
|
|
116
|
-
value, and two feature flags at weight 50 hold exactly the same visitors. Your analytics
|
|
117
|
-
then never sees the mixed groups that tell one result from the other.
|
|
118
|
-
|
|
119
|
-
## The allocation ratchets
|
|
120
|
-
|
|
121
|
-
The ratchet applies to both types of feature flag. Inside one feature flag, a larger
|
|
122
|
-
weight only adds visitors. Everybody allocated at
|
|
123
|
-
weight 30 is still allocated at weight 50. The allocation is a **ratchet**: it turns one
|
|
124
|
-
way, and a larger weight never moves a visitor back out.
|
|
125
|
-
|
|
126
|
-
That property is what makes a staged release safe. A visitor who sees the change at 10%
|
|
127
|
-
still sees it at 20%. A smaller weight is the reverse turn. It removes visitors from the
|
|
128
|
-
top of the range, and every visitor below the new weight keeps what they had.
|
|
129
|
-
|
|
130
|
-
A Text feature flag has the same ratchet, one variant at a time. A larger weight on one variant only
|
|
131
|
-
adds visitors to that variant. A smaller weight only removes them. Two variants that you
|
|
132
|
-
do not change never exchange visitors with each other.
|
|
133
|
-
|
|
134
|
-
## A disabled feature flag records nothing
|
|
135
|
-
|
|
136
|
-
A disabled feature flag allocates nobody, and the difference from a weight of 0
|
|
137
|
-
matters for your reports. At weight 0 the visitor is in the test, and in the
|
|
138
|
-
control group. When the feature flag is disabled, the visitor is not in the test at all.
|
|
139
|
-
|
|
140
|
-
The SDK does not evaluate a disabled feature flag. It writes no decision to browser
|
|
141
|
-
storage and it reports no decision. The feature flag is also absent from the feature
|
|
142
|
-
list on every analytics event. Expect a step change in your reports when you disable
|
|
143
|
-
one: fewer feature flags for each visitor, and fewer decision events.
|
|
144
|
-
|
|
145
|
-
This state is temporary. A visitor allocated before you disabled the feature flag keeps
|
|
146
|
-
that bucket in the browser. When you enable the feature flag again, that visitor continues
|
|
147
|
-
in the same group.
|
|
148
|
-
|
|
149
|
-
## What a version does, and what it does not
|
|
150
|
-
|
|
151
|
-
Each feature flag carries a version, and it starts at 1. The SDK caches the bucket in
|
|
152
|
-
browser storage under a key that holds the feature flag ID and the version. A visitor
|
|
153
|
-
therefore keeps the same bucket across page loads.
|
|
154
|
-
|
|
155
|
-
Change a weight and leave the version as it is, and that cache decides the result. A
|
|
156
|
-
visitor who has a cached bucket keeps it. The new weight reaches only a visitor who has no
|
|
157
|
-
cache. Your reports then hold two groups of visitors at the same time, one measured
|
|
158
|
-
against the old weight and one against the new.
|
|
159
|
-
|
|
160
|
-
Increment the version and the old cache key no longer matches. The feature flag counts as
|
|
161
|
-
undecided, `decide` runs again on the next page load, and every visitor is measured
|
|
162
|
-
against the new weight. Increment it after each weight change, unless you intend the
|
|
163
|
-
change to reach new visitors only.
|
|
164
|
-
|
|
165
|
-
**The version affects the cache only. It is not part of the allocation.** The ratchet
|
|
166
|
-
still holds after you increment the version: a visitor allocated at weight 30 is allocated
|
|
167
|
-
again at weight 50. You cannot decrement a version.
|
|
168
|
-
|
|
169
|
-
## When the SDK reports a decision
|
|
170
|
-
|
|
171
|
-
The SDK sends `feature_decide` the first time that it allocates a visitor to a bucket,
|
|
172
|
-
and only then. On a later page load it restores that bucket from browser storage and sends
|
|
173
|
-
nothing. A visitor who returns therefore produces no `feature_decide` event, although that
|
|
174
|
-
visitor is still in the group they went into the first time.
|
|
175
|
-
|
|
176
|
-
An integration that listens only for `feature_decide` under-reports every visitor who
|
|
177
|
-
returns. Choose the source that matches your question:
|
|
178
|
-
|
|
179
|
-
| Question | Source |
|
|
180
|
-
| --- | --- |
|
|
181
|
-
| Which bucket is this visitor in now? | [`client.features.listStore`](/how-to/read-a-feature-flag#1-choose-an-accessor) |
|
|
182
|
-
| Did the SDK allocate a visitor now? | [`feature_decide` on `client.eventBus`](/reference/sdk/features#watch-for-feature-flag-decisions) |
|
|
183
|
-
| Which allocations happened before I subscribed? | [`client.events`](/reference/sdk/features#read-the-events-that-already-happened) |
|
|
184
|
-
|
|
185
|
-
`listStore` updates each time that a feature flag resolves. It does this whether the SDK
|
|
186
|
-
restored the bucket from storage or allocated the visitor now. A watch on it therefore
|
|
187
|
-
always gives the current state.
|
|
188
|
-
Use `feature_decide` only when you need to know that Picked allocated a visitor at that
|
|
189
|
-
moment.
|
|
190
|
-
|
|
191
|
-
## What Picked reports for you
|
|
192
|
-
|
|
193
|
-
Every Picked analytics event carries a `feature_list` context, and a report of your own
|
|
194
|
-
can use it. The context describes each feature flag that this visitor resolved: the
|
|
195
|
-
feature flag ID, the name, the version key, and the bucket. Only a resolved feature flag
|
|
196
|
-
appears there, so the context shows what the visitor was exposed to at the time when the
|
|
197
|
-
event fired.
|
|
198
|
-
|
|
199
|
-
The optional [Sentry plugin](https://www.npmjs.com/package/@picked/sentry) also tags each
|
|
200
|
-
allocation onto the Sentry scope as `picked.feature.<id>`, with the bucket as the value.
|
|
201
|
-
You can then filter your errors by the variant.
|
|
202
|
-
|
|
203
|
-
## Preview mode
|
|
204
|
-
|
|
205
|
-
In preview mode the SDK stores the bucket in session storage and not in local storage. The
|
|
206
|
-
bucket does not stay after you close the tab, and Picked can allocate a preview visitor
|
|
207
|
-
differently from a production visitor. Treat a preview allocation as disposable.
|
|
208
|
-
|
|
209
|
-
A feature flag that you define in SDK code can ask for the same thing in production with
|
|
210
|
-
`storage: 'session'`. Use it for a short test that must not last across browser sessions.
|
|
211
|
-
|
|
212
|
-
## Related
|
|
213
|
-
|
|
214
|
-
- [How to read a feature flag](/how-to/read-a-feature-flag) — the accessors, and which
|
|
215
|
-
ones resolve the feature flag.
|
|
216
|
-
- [Feature flags](/reference/dashboard/features) — the dashboard controls.
|
|
217
|
-
- [SDK feature flags](/reference/sdk/features) — the definition shape and the accessor
|
|
218
|
-
table.
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
type: explanation
|
|
3
|
-
layout: docs
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# About how Picked works
|
|
7
|
-
|
|
8
|
-
What happens between the moment an editor clicks **Publish** and the moment a visitor
|
|
9
|
-
sees the result? This page follows one change through the whole system.
|
|
10
|
-
|
|
11
|
-
## The shape of it
|
|
12
|
-
|
|
13
|
-
Picked splits into two parts that meet at a published snapshot.
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
Author (dashboard) -> Publish -> Delivery API (config + content)
|
|
17
|
-
-> SDK reads it -> conditions decide -> component renders
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
On the left, an editor works in a workspace. Nothing they do reaches a visitor. On the
|
|
21
|
-
right, the SDK in your application reads what was published and renders it. The snapshot
|
|
22
|
-
between them is immutable, so each part moves at its own speed. An editor publishes with
|
|
23
|
-
no deployment, and you deploy with no change to the content.
|
|
24
|
-
|
|
25
|
-
## What happens when you publish
|
|
26
|
-
|
|
27
|
-
Picked takes the current workspace and makes an immutable snapshot of it. The result is a
|
|
28
|
-
version, and a project serves exactly one version at a time, the live one. Nothing in a
|
|
29
|
-
published version can change later. A roll-back moves the live pointer of the project to
|
|
30
|
-
an earlier version, and it changes no data.
|
|
31
|
-
|
|
32
|
-
That is why the dashboard has a draft state at all. The workspace holds the work that is
|
|
33
|
-
still in progress. The version holds what your visitors get.
|
|
34
|
-
|
|
35
|
-
## What the SDK does at startup
|
|
36
|
-
|
|
37
|
-
`createClient()` builds the client, and `client.start()` runs it. Between those two calls
|
|
38
|
-
nothing happens yet.
|
|
39
|
-
|
|
40
|
-
1. **Options resolve.** The dynamic experiences plugin reads the configuration of your
|
|
41
|
-
project from the source that its `mode` names. That is the data in your bundle, or a
|
|
42
|
-
request to the Delivery API. A preview token replaces both.
|
|
43
|
-
2. **Everything registers.** The plugin registers every feature flag, condition, field,
|
|
44
|
-
step, and experience from that configuration. You declare none of them in code.
|
|
45
|
-
3. **Preconditions run.** A precondition asks whether Picked should start at all. A
|
|
46
|
-
condition asks whether this visitor is eligible.
|
|
47
|
-
4. **Experiences resolve.** Each experience evaluates its condition and looks for its
|
|
48
|
-
target element in the page.
|
|
49
|
-
5. **Components render.** An experience whose condition is true, and whose target exists,
|
|
50
|
-
renders the component you registered under its component name.
|
|
51
|
-
|
|
52
|
-
Step 1 is why the [Vite plugin](/reference/tools/vite-plugin) exists: it embeds the
|
|
53
|
-
options at build time, so production makes no request at startup.
|
|
54
|
-
|
|
55
|
-
## Why the browser and the server can read from different places
|
|
56
|
-
|
|
57
|
-
The two ends of a server-rendered page want different things. Your server wants a new
|
|
58
|
-
version to reach visitors at once. That needs a request to the API. The browser wants no
|
|
59
|
-
request at all. That needs the data in your bundle.
|
|
60
|
-
|
|
61
|
-
The `mode` option belongs to one end only, so you can have both. Give `'api'` to your
|
|
62
|
-
server and `'bundle'` to your browser.
|
|
63
|
-
|
|
64
|
-
Each end stays consistent with itself. The configuration and the content that one end
|
|
65
|
-
reads always come from the same source. Picked gives them the same version.
|
|
66
|
-
|
|
67
|
-
The two ends can still hold different versions. An editor publishes a workspace between
|
|
68
|
-
your build and the request, which moves the server to a newer version than the bundle.
|
|
69
|
-
The snapshot from the server then wins. The browser keeps it, and drops the configuration
|
|
70
|
-
that it read.
|
|
71
|
-
|
|
72
|
-
The reason is that the server made the page from that snapshot. To register a different
|
|
73
|
-
configuration over the top makes the page and the client show different data. One
|
|
74
|
-
version behind is the safer of the two results. The client records the two version IDs on
|
|
75
|
-
`client.versionSkew`, and the [devtools](/reference/tools/chrome-extension) show them.
|
|
76
|
-
|
|
77
|
-
## Why the flow is a graph
|
|
78
|
-
|
|
79
|
-
An experience is not a list of screens. It is a directed graph, and the SDK walks it.
|
|
80
|
-
|
|
81
|
-
The origin node is the entry point. From there each edge leads to a step, an action, or a
|
|
82
|
-
condition. A step waits for the visitor. An action runs a side effect and does not
|
|
83
|
-
interrupt them. A condition evaluates a rule and sends the flow down one of two paths.
|
|
84
|
-
|
|
85
|
-
The walk is what makes a branch cost so little. To show a different screen to a different
|
|
86
|
-
visitor you add a condition node, and not a second copy of the flow. The same step can
|
|
87
|
-
appear in several flows, because a step belongs to the project and the graph only
|
|
88
|
-
points at it.
|
|
89
|
-
|
|
90
|
-
## How an action decides which way to go
|
|
91
|
-
|
|
92
|
-
An action node can carry a success branch and a failure branch, and the SDK picks between
|
|
93
|
-
them from the outcome of your handler:
|
|
94
|
-
|
|
95
|
-
- The handler **throws**, or its promise rejects: the flow takes the failure branch.
|
|
96
|
-
- The handler **resolves** to any value at all, and that includes `false`: the flow takes
|
|
97
|
-
the success branch.
|
|
98
|
-
|
|
99
|
-
This difference matters more than it looks. A valid negative result is not a failure. An
|
|
100
|
-
endpoint that answers `200` with `{ serviced: false }` succeeded — it told you something
|
|
101
|
-
true. Record that value as a response and branch on it with a condition. Keep the failure
|
|
102
|
-
branch for a request that did not complete.
|
|
103
|
-
|
|
104
|
-
Get this wrong and your failure branch holds two different things at once: a network
|
|
105
|
-
error, and a visitor who lives outside your service area.
|
|
106
|
-
|
|
107
|
-
## Where content comes in
|
|
108
|
-
|
|
109
|
-
A flow renders in the browser. A page renders on your server, from content held in the
|
|
110
|
-
Picked CMS, so a search engine reads it in the first response. Both are experiences, and
|
|
111
|
-
both go through the same publish.
|
|
112
|
-
|
|
113
|
-
The difference is only the place where each one renders, and that follows from what the
|
|
114
|
-
content is for. See [About pages and flows](/explanation/pages-and-flows) and
|
|
115
|
-
[About server-side rendering](/explanation/why-server-side-rendering).
|
|
116
|
-
|
|
117
|
-
## Related
|
|
118
|
-
|
|
119
|
-
- [Add Picked to your application](/tutorials/first-experience) — the whole path, in
|
|
120
|
-
practice.
|
|
121
|
-
- [About workspaces and versions](/explanation/workspaces-and-versions) — draft, live,
|
|
122
|
-
and roll-back.
|
|
123
|
-
- [Delivery API](/reference/delivery-api) — what the published snapshot looks like on the
|
|
124
|
-
wire.
|