@picked/client 0.0.71-next.1 → 0.0.71-next.3
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 -1
- package/dist/{api-CH2FNa6S.cjs → api-CnjBi6JG.cjs} +1 -1
- package/dist/{api-CgqRvuKL.js → api-vBVGLtvU.js} +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/docs/explanation/conditions-and-the-logic-engine.md +94 -0
- package/dist/docs/explanation/feature-flag-allocation.md +176 -0
- package/dist/docs/explanation/how-picked-works.md +102 -0
- package/dist/docs/explanation/how-pickedql-works.md +76 -0
- package/dist/docs/explanation/pages-and-flows.md +64 -0
- package/dist/docs/explanation/the-content-model.md +77 -0
- package/dist/docs/explanation/the-two-apis.md +68 -0
- package/dist/docs/explanation/why-server-side-rendering.md +76 -0
- package/dist/docs/explanation/workspaces-and-versions.md +71 -0
- package/dist/docs/how-to/add-a-step-field-or-action.md +43 -0
- package/dist/docs/{pickedql/examples.md → how-to/analytics/index.md} +19 -6
- package/dist/docs/how-to/archive-or-delete-a-workspace.md +46 -0
- package/dist/docs/how-to/authenticate-with-a-magic-link.md +40 -0
- package/dist/docs/how-to/authenticate-with-a-password.md +51 -0
- package/dist/docs/how-to/authenticate-with-an-api-key.md +51 -0
- package/dist/docs/how-to/authenticate-with-oauth.md +40 -0
- package/dist/docs/{flow-builder/building-flows.md → how-to/build-a-flow.md} +13 -9
- package/dist/docs/how-to/connect-the-mcp-server.md +38 -0
- package/dist/docs/how-to/create-a-project.md +29 -0
- package/dist/docs/how-to/create-an-experience.md +42 -0
- package/dist/docs/how-to/create-and-revoke-an-api-key.md +37 -0
- package/dist/docs/how-to/debug-with-the-chrome-extension.md +57 -0
- package/dist/docs/how-to/define-a-page-layout.md +39 -0
- package/dist/docs/how-to/fill-and-publish-a-page.md +50 -0
- package/dist/docs/{gtm-template.md → how-to/install-the-gtm-template.md} +9 -3
- package/dist/docs/how-to/install-the-sdk.md +68 -0
- package/dist/docs/how-to/install-the-vite-plugin.md +56 -0
- package/dist/docs/how-to/invite-and-remove-members.md +34 -0
- package/dist/docs/how-to/preview-a-workspace.md +28 -0
- package/dist/docs/how-to/publish-a-workspace.md +30 -0
- package/dist/docs/how-to/read-a-feature-flag.md +101 -0
- package/dist/docs/how-to/register-a-component.md +55 -0
- package/dist/docs/how-to/register-an-action-handler.md +67 -0
- package/dist/docs/how-to/render-a-step-form.md +132 -0
- package/dist/docs/how-to/render-an-unpublished-preview.md +41 -0
- package/dist/docs/how-to/reuse-a-section.md +37 -0
- package/dist/docs/how-to/reuse-a-step-field-or-action.md +28 -0
- package/dist/docs/how-to/roll-back-a-published-version.md +26 -0
- package/dist/docs/how-to/run-an-ab-test.md +49 -0
- package/dist/docs/how-to/run-code-on-a-form-submission.md +125 -0
- package/dist/docs/how-to/server-render-a-page.md +81 -0
- package/dist/docs/how-to/set-a-content-security-policy.md +32 -0
- package/dist/docs/how-to/set-up-mfa.md +69 -0
- package/dist/docs/how-to/upload-and-use-an-asset.mdx +73 -0
- package/dist/docs/how-to/validate-a-field.md +52 -0
- package/dist/docs/how-to/write-a-condition.md +45 -0
- package/dist/docs/how-to/write-a-predicate.md +60 -0
- package/dist/docs/index.md +41 -70
- package/dist/docs/reference/dashboard/actions.mdx +53 -0
- package/dist/docs/{api-keys.mdx → reference/dashboard/api-keys.mdx} +7 -15
- package/dist/docs/reference/dashboard/conditions.mdx +42 -0
- package/dist/docs/reference/dashboard/content/asset-library.mdx +63 -0
- package/dist/docs/{content → reference/dashboard/content}/content-entries.mdx +11 -3
- package/dist/docs/{content → reference/dashboard/content}/layouts.mdx +11 -35
- package/dist/docs/reference/dashboard/content/pages.mdx +35 -0
- package/dist/docs/reference/dashboard/content/sections.mdx +28 -0
- package/dist/docs/{experiences.mdx → reference/dashboard/experiences.mdx} +9 -16
- package/dist/docs/{features.mdx → reference/dashboard/features.mdx} +26 -41
- package/dist/docs/{fields.mdx → reference/dashboard/fields.mdx} +26 -20
- package/dist/docs/{flow-builder/overview.mdx → reference/dashboard/flow-builder/canvas.mdx} +8 -13
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/condition-builder.md +5 -13
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/keyboard-shortcuts.md +7 -1
- package/dist/docs/{flow-builder → reference/dashboard/flow-builder}/nodes.md +10 -3
- package/dist/docs/reference/dashboard/projects.mdx +44 -0
- package/dist/docs/reference/dashboard/reports.mdx +43 -0
- package/dist/docs/reference/dashboard/steps.mdx +37 -0
- package/dist/docs/{team-management.md → reference/dashboard/team-management.md} +5 -22
- package/dist/docs/{workspace-versioning.mdx → reference/dashboard/workspace-versioning.mdx} +6 -10
- package/dist/docs/reference/dashboard/workspaces.mdx +76 -0
- package/dist/docs/{delivery-api → reference/delivery-api}/experience-content.md +11 -4
- package/dist/docs/reference/delivery-api/index.md +45 -0
- package/dist/docs/{delivery-api → reference/delivery-api}/preview-caching-crawling.md +12 -5
- package/dist/docs/{delivery-api → reference/delivery-api}/project-config.md +11 -4
- package/dist/docs/reference/glossary.md +173 -0
- package/dist/docs/reference/management-api/authentication.md +74 -0
- package/dist/docs/{api → reference/management-api}/authorization-and-scopes.md +10 -3
- package/dist/docs/{api → reference/management-api}/error-handling.md +9 -3
- package/dist/docs/{api → reference/management-api}/pagination.md +6 -0
- package/dist/docs/{pickedql → reference/pickedql}/data-sources.md +16 -9
- package/dist/docs/{pickedql → reference/pickedql}/dimensions.md +7 -0
- package/dist/docs/{pickedql → reference/pickedql}/filtering.md +7 -0
- package/dist/docs/reference/pickedql/index.md +18 -0
- package/dist/docs/{pickedql → reference/pickedql}/schema.md +13 -6
- package/dist/docs/{pickedql → reference/pickedql}/virtual-functions.md +7 -0
- package/dist/docs/reference/sdk/actions.md +30 -0
- package/dist/docs/{sdk → reference/sdk}/conditions.md +8 -1
- package/dist/docs/reference/sdk/content-security-policy.md +21 -0
- package/dist/docs/reference/sdk/experiences.md +51 -0
- package/dist/docs/{sdk → reference/sdk}/features.md +30 -117
- package/dist/docs/{sdk → reference/sdk}/fields.md +10 -88
- package/dist/docs/reference/sdk/index.md +68 -0
- package/dist/docs/{sdk → reference/sdk}/plugins/create-experience.md +10 -22
- package/dist/docs/reference/sdk/plugins/dynamic-experiences.md +26 -0
- package/dist/docs/reference/sdk/plugins/index.md +29 -0
- package/dist/docs/{sdk → reference/sdk}/plugins/window-object.md +6 -0
- package/dist/docs/reference/sdk/predicates.md +43 -0
- package/dist/docs/reference/sdk/ssr/index.md +33 -0
- package/dist/docs/{sdk → reference/sdk}/ssr/rendering-and-hydration.md +4 -63
- package/dist/docs/{sdk → reference/sdk}/ssr/serialize.md +6 -33
- package/dist/docs/{sdk → reference/sdk}/ssr/slots-and-placements.md +7 -1
- package/dist/docs/reference/sdk/steps.md +87 -0
- package/dist/docs/reference/tools/chrome-extension.md +16 -0
- package/dist/docs/{image-optimisation-service.md → reference/tools/image-optimisation-service.md} +8 -0
- package/dist/docs/{mcp.md → reference/tools/mcp.md} +5 -45
- package/dist/docs/{vite-plugin.md → reference/tools/vite-plugin.md} +8 -38
- package/dist/docs/{api/index.md → tutorials/first-api-call.md} +27 -15
- package/dist/docs/tutorials/first-experience-in-the-dashboard.mdx +143 -0
- package/dist/docs/tutorials/first-experience.mdx +255 -0
- package/dist/docs/tutorials/first-server-rendered-page.mdx +186 -0
- package/dist/esm/index.js +468 -426
- package/dist/esm/plugins/dynamic-experiences/index.js +2 -2
- package/dist/esm/server/index.js +1 -1
- package/dist/{plugin-DCW-tnjO.cjs → plugin-CSmD1gPl.cjs} +1 -1
- package/dist/{plugin-D2UqFikQ.js → plugin-GOTtoDlC.js} +1 -1
- package/dist/types/entities/feature/feature.d.ts +25 -2
- package/dist/types/entities/feature/feature.d.ts.map +1 -1
- package/dist/types/picked.d.ts +2 -1
- package/dist/types/picked.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/docs/actions.mdx +0 -98
- package/dist/docs/api/authentication.md +0 -403
- package/dist/docs/chrome-extension.md +0 -68
- package/dist/docs/conditions.mdx +0 -72
- package/dist/docs/content/asset-library.mdx +0 -124
- package/dist/docs/content/pages.mdx +0 -82
- package/dist/docs/content/sections.mdx +0 -44
- package/dist/docs/delivery-api/index.md +0 -53
- package/dist/docs/guides/build-a-personalized-page.mdx +0 -161
- package/dist/docs/pickedql/index.md +0 -54
- package/dist/docs/projects.mdx +0 -44
- package/dist/docs/reports.mdx +0 -20
- package/dist/docs/sdk/actions.md +0 -82
- package/dist/docs/sdk/content-security-policy.md +0 -33
- package/dist/docs/sdk/experiences.md +0 -64
- package/dist/docs/sdk/feature-allocation.md +0 -161
- package/dist/docs/sdk/index.md +0 -98
- package/dist/docs/sdk/plugins/dynamic-experiences.md +0 -84
- package/dist/docs/sdk/plugins/index.md +0 -9
- package/dist/docs/sdk/predicates.md +0 -128
- package/dist/docs/sdk/ssr/index.md +0 -79
- package/dist/docs/sdk/steps.md +0 -479
- package/dist/docs/steps.mdx +0 -53
- package/dist/docs/workspaces.mdx +0 -133
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
## Each feature flag is independent
|
|
69
|
+
|
|
70
|
+
Picked builds that value from the visitor ID together with the ID of the feature flag.
|
|
71
|
+
Each feature flag therefore gets its own value, and tests that run at the same time are
|
|
72
|
+
independent of each other.
|
|
73
|
+
|
|
74
|
+
Two feature flags, both at weight 50, split your visitors into four equal parts:
|
|
75
|
+
allocated to both, to one, to the other, or to neither. Three feature flags at weight 50
|
|
76
|
+
give eight groups of 12.5%. Add a feature flag and the groups divide again.
|
|
77
|
+
|
|
78
|
+
This is what lets you read two experiments separately. Give every feature flag the same
|
|
79
|
+
value, and two feature flags at weight 50 hold exactly the same visitors. Your analytics
|
|
80
|
+
then never sees the mixed groups that tell one result from the other.
|
|
81
|
+
|
|
82
|
+
## The allocation ratchets
|
|
83
|
+
|
|
84
|
+
Inside one feature flag, a larger weight only adds visitors. Everybody allocated at
|
|
85
|
+
weight 30 is still allocated at weight 50. The allocation is a **ratchet**: it turns one
|
|
86
|
+
way, and a larger weight never moves a visitor back out.
|
|
87
|
+
|
|
88
|
+
That property is what makes a staged release safe. A visitor who sees the change at 10%
|
|
89
|
+
still sees it at 20%. A smaller weight is the reverse turn. It removes visitors from the
|
|
90
|
+
top of the range, and every visitor below the new weight keeps what they had.
|
|
91
|
+
|
|
92
|
+
## A disabled feature flag records nothing
|
|
93
|
+
|
|
94
|
+
A disabled feature flag allocates nobody, and the difference from a weight of 0
|
|
95
|
+
matters for your reports. At weight 0 the visitor is in the test, and in the
|
|
96
|
+
control group. When the feature flag is disabled, the visitor is not in the test at all.
|
|
97
|
+
|
|
98
|
+
The SDK does not evaluate a disabled feature flag. It writes no decision to browser
|
|
99
|
+
storage and it reports no decision. The feature flag is also absent from the feature
|
|
100
|
+
list on every analytics event. Expect a step change in your reports when you disable
|
|
101
|
+
one: fewer feature flags for each visitor, and fewer decision events.
|
|
102
|
+
|
|
103
|
+
This state is temporary. A visitor allocated before you disabled the feature flag keeps
|
|
104
|
+
that bucket in the browser. When you enable the feature flag again, that visitor continues
|
|
105
|
+
in the same group.
|
|
106
|
+
|
|
107
|
+
## What a version does, and what it does not
|
|
108
|
+
|
|
109
|
+
Each feature flag carries a version, and it starts at 1. The SDK caches the bucket in
|
|
110
|
+
browser storage under a key that holds the feature flag ID and the version. A visitor
|
|
111
|
+
therefore keeps the same bucket across page loads.
|
|
112
|
+
|
|
113
|
+
Change a weight and leave the version as it is, and that cache decides the result. A
|
|
114
|
+
visitor who has a cached bucket keeps it. The new weight reaches only a visitor who has no
|
|
115
|
+
cache. Your reports then hold two groups of visitors at the same time, one measured
|
|
116
|
+
against the old weight and one against the new.
|
|
117
|
+
|
|
118
|
+
Increment the version and the old cache key no longer matches. The feature flag counts as
|
|
119
|
+
undecided, `decide` runs again on the next page load, and every visitor is measured
|
|
120
|
+
against the new weight. Increment it after each weight change, unless you intend the
|
|
121
|
+
change to reach new visitors only.
|
|
122
|
+
|
|
123
|
+
**The version affects the cache only. It is not part of the allocation.** The ratchet
|
|
124
|
+
still holds after you increment the version: a visitor allocated at weight 30 is allocated
|
|
125
|
+
again at weight 50. You cannot decrement a version.
|
|
126
|
+
|
|
127
|
+
## When the SDK reports a decision
|
|
128
|
+
|
|
129
|
+
The SDK sends `feature_decide` the first time that it allocates a visitor to a bucket,
|
|
130
|
+
and only then. On a later page load it restores that bucket from browser storage and sends
|
|
131
|
+
nothing. A visitor who returns therefore produces no `feature_decide` event, although that
|
|
132
|
+
visitor is still in the group they went into the first time.
|
|
133
|
+
|
|
134
|
+
An integration that listens only for `feature_decide` under-reports every visitor who
|
|
135
|
+
returns. Choose the source that matches your question:
|
|
136
|
+
|
|
137
|
+
| Question | Source |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| Which bucket is this visitor in now? | [`client.features.listStore`](/how-to/read-a-feature-flag#1-choose-an-accessor) |
|
|
140
|
+
| Did the SDK allocate a visitor now? | [`feature_decide` on `client.eventBus`](/reference/sdk/features#watch-for-feature-flag-decisions) |
|
|
141
|
+
| Which allocations happened before I subscribed? | [`client.events`](/reference/sdk/features#read-the-events-that-already-happened) |
|
|
142
|
+
|
|
143
|
+
`listStore` updates each time that a feature flag resolves. It does this whether the SDK
|
|
144
|
+
restored the bucket from storage or allocated the visitor now. A watch on it therefore
|
|
145
|
+
always gives the current state.
|
|
146
|
+
Use `feature_decide` only when you need to know that Picked allocated a visitor at that
|
|
147
|
+
moment.
|
|
148
|
+
|
|
149
|
+
## What Picked reports for you
|
|
150
|
+
|
|
151
|
+
Every Picked analytics event carries a `feature_list` context, and a report of your own
|
|
152
|
+
can use it. The context describes each feature flag that this visitor resolved: the
|
|
153
|
+
feature flag ID, the name, the version key, and the bucket. Only a resolved feature flag
|
|
154
|
+
appears there, so the context shows what the visitor was exposed to at the time when the
|
|
155
|
+
event fired.
|
|
156
|
+
|
|
157
|
+
The optional [Sentry plugin](https://www.npmjs.com/package/@picked/sentry) also tags each
|
|
158
|
+
allocation onto the Sentry scope as `picked.feature.<id>`, with the bucket as the value.
|
|
159
|
+
You can then filter your errors by the variant.
|
|
160
|
+
|
|
161
|
+
## Preview mode
|
|
162
|
+
|
|
163
|
+
In preview mode the SDK stores the bucket in session storage and not in local storage. The
|
|
164
|
+
bucket does not stay after you close the tab, and Picked can allocate a preview visitor
|
|
165
|
+
differently from a production visitor. Treat a preview allocation as disposable.
|
|
166
|
+
|
|
167
|
+
A feature flag that you define in SDK code can ask for the same thing in production with
|
|
168
|
+
`storage: 'session'`. Use it for a short test that must not last across browser sessions.
|
|
169
|
+
|
|
170
|
+
## Related
|
|
171
|
+
|
|
172
|
+
- [How to read a feature flag](/how-to/read-a-feature-flag) — the accessors, and which
|
|
173
|
+
ones resolve the feature flag.
|
|
174
|
+
- [Feature flags](/reference/dashboard/features) — the dashboard controls.
|
|
175
|
+
- [SDK feature flags](/reference/sdk/features) — the definition shape and the accessor
|
|
176
|
+
table.
|
|
@@ -0,0 +1,102 @@
|
|
|
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 finds the configuration of your project.
|
|
41
|
+
It tries preview configuration first, then configuration built into your bundle, then a request to
|
|
42
|
+
the Delivery API.
|
|
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 flow is a graph
|
|
56
|
+
|
|
57
|
+
An experience is not a list of screens. It is a directed graph, and the SDK walks it.
|
|
58
|
+
|
|
59
|
+
The origin node is the entry point. From there each edge leads to a step, an action, or a
|
|
60
|
+
condition. A step waits for the visitor. An action runs a side effect and does not
|
|
61
|
+
interrupt them. A condition evaluates a rule and sends the flow down one of two paths.
|
|
62
|
+
|
|
63
|
+
The walk is what makes a branch cost so little. To show a different screen to a different
|
|
64
|
+
visitor you add a condition node, and not a second copy of the flow. The same step can
|
|
65
|
+
appear in several flows, because a step belongs to the project and the graph only
|
|
66
|
+
points at it.
|
|
67
|
+
|
|
68
|
+
## How an action decides which way to go
|
|
69
|
+
|
|
70
|
+
An action node can carry a success branch and a failure branch, and the SDK picks between
|
|
71
|
+
them from the outcome of your handler:
|
|
72
|
+
|
|
73
|
+
- The handler **throws**, or its promise rejects: the flow takes the failure branch.
|
|
74
|
+
- The handler **resolves** to any value at all, and that includes `false`: the flow takes
|
|
75
|
+
the success branch.
|
|
76
|
+
|
|
77
|
+
This difference matters more than it looks. A valid negative result is not a failure. An
|
|
78
|
+
endpoint that answers `200` with `{ serviced: false }` succeeded — it told you something
|
|
79
|
+
true. Record that value as a response and branch on it with a condition. Keep the failure
|
|
80
|
+
branch for a request that did not complete.
|
|
81
|
+
|
|
82
|
+
Get this wrong and your failure branch holds two different things at once: a network
|
|
83
|
+
error, and a visitor who lives outside your service area.
|
|
84
|
+
|
|
85
|
+
## Where content comes in
|
|
86
|
+
|
|
87
|
+
A flow renders in the browser. A page renders on your server, from content held in the
|
|
88
|
+
Picked CMS, so a search engine reads it in the first response. Both are experiences, and
|
|
89
|
+
both go through the same publish.
|
|
90
|
+
|
|
91
|
+
The difference is only the place where each one renders, and that follows from what the
|
|
92
|
+
content is for. See [About pages and flows](/explanation/pages-and-flows) and
|
|
93
|
+
[About server-side rendering](/explanation/why-server-side-rendering).
|
|
94
|
+
|
|
95
|
+
## Related
|
|
96
|
+
|
|
97
|
+
- [Add Picked to your application](/tutorials/first-experience) — the whole path, in
|
|
98
|
+
practice.
|
|
99
|
+
- [About workspaces and versions](/explanation/workspaces-and-versions) — draft, live,
|
|
100
|
+
and roll-back.
|
|
101
|
+
- [Delivery API](/reference/delivery-api) — what the published snapshot looks like on the
|
|
102
|
+
wire.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: explanation
|
|
3
|
+
layout: docs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# About PickedQL
|
|
7
|
+
|
|
8
|
+
PickedQL is SQL over your Picked analytics data, but it is not only SQL. It expands what
|
|
9
|
+
you write before it runs. Why add a layer at all, and not give you the tables?
|
|
10
|
+
|
|
11
|
+
## The problem it answers
|
|
12
|
+
|
|
13
|
+
An A/B test question is short to ask, and the SQL behind it is not. Take one question:
|
|
14
|
+
"How many users are in the control group?" It needs four things. It needs a filter on the
|
|
15
|
+
variant, a count of distinct users, a join to each dimension in the group, and a tenant
|
|
16
|
+
filter.
|
|
17
|
+
|
|
18
|
+
Write that by hand for every question, and most of each query is the same text again.
|
|
19
|
+
|
|
20
|
+
PickedQL lets you write the short version:
|
|
21
|
+
|
|
22
|
+
```sql
|
|
23
|
+
SELECT control_users(), variant_users()
|
|
24
|
+
FROM metrics
|
|
25
|
+
WHERE project_id = 'your-project-id'
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it expands
|
|
29
|
+
|
|
30
|
+
**Virtual tables.** A name such as `metrics` maps to the real tables below it. You name
|
|
31
|
+
the thing that you ask about, and not the storage.
|
|
32
|
+
|
|
33
|
+
**Virtual functions.** `control_users()` expands to a full aggregate expression. The
|
|
34
|
+
definition lives in one place, so every report that counts control users counts them the
|
|
35
|
+
same way.
|
|
36
|
+
|
|
37
|
+
**Automatic joins.** Point at a dimension column such as `device_category` or
|
|
38
|
+
`country_code` and PickedQL adds the `LEFT JOIN` for you. This gives you the most: a
|
|
39
|
+
dimension that you did not join is the most common reason for a wrong answer that looks
|
|
40
|
+
correct.
|
|
41
|
+
|
|
42
|
+
**SQL passthrough.** Standard SQL still works next to a virtual function, and that
|
|
43
|
+
includes `CASE WHEN`, window functions, and subqueries. The layer adds; it does not
|
|
44
|
+
restrict.
|
|
45
|
+
|
|
46
|
+
## Why every query must carry a tenant filter
|
|
47
|
+
|
|
48
|
+
Every query must carry a tenant filter in its `WHERE` clause. That filter is `project_id`
|
|
49
|
+
for most tables. The `projects` table uses `id` instead, because there the row ID *is* the
|
|
50
|
+
project ID.
|
|
51
|
+
|
|
52
|
+
PickedQL reads that filter and checks your permission for that project. The filter is
|
|
53
|
+
therefore not a convention that you can forget. Without one, the query does not run.
|
|
54
|
+
The isolation is a property of the language, and not of each author.
|
|
55
|
+
|
|
56
|
+
## Which table to reach for
|
|
57
|
+
|
|
58
|
+
| Table | Use it for |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `metrics` | A dashboard. The data is aggregated first, so it is much faster than raw events. |
|
|
61
|
+
| `events` | Anything deeper. Raw events let you build any breakdown or funnel. |
|
|
62
|
+
| `experience_journey` | A sankey or a funnel chart. One line of SQL returns `source, target, users`. |
|
|
63
|
+
|
|
64
|
+
Note what `experience_journey` does for you. It builds the eight common table expressions
|
|
65
|
+
that a path analysis needs. Those remove the duplicates, rank the top paths, and call
|
|
66
|
+
`lagInFrame`. You pass your parameters in the `WHERE` clause.
|
|
67
|
+
|
|
68
|
+
Some reference columns are free on `events`. `step_title`, `experience_title`, and
|
|
69
|
+
`project_name` come from reference tables that PickedQL joins for you.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [PickedQL](/reference/pickedql) — the language surface.
|
|
74
|
+
- [Data sources](/reference/pickedql/data-sources) — every table.
|
|
75
|
+
- [Virtual functions](/reference/pickedql/virtual-functions) — every function.
|
|
76
|
+
- [How to run an analytics query](/how-to/analytics) — worked examples.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: explanation
|
|
3
|
+
layout: docs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# About pages and flows
|
|
7
|
+
|
|
8
|
+
An experience has two kinds, and the dashboard makes you choose
|
|
9
|
+
between them when you create one. Why two, and how do you know which one a piece of work needs?
|
|
10
|
+
|
|
11
|
+
## The two kinds
|
|
12
|
+
|
|
13
|
+
| | Page | Flow |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| What it is | A standalone experience that your server renders from a content entry. | An interactive experience of more than one step, which the browser renders. |
|
|
16
|
+
| Use it for | A landing page or a campaign page where the search rank matters. | A quiz, a survey, a product finder, onboarding of more than one step. |
|
|
17
|
+
|
|
18
|
+
The split is not about how complex the content is. It is about **who has to read it**.
|
|
19
|
+
|
|
20
|
+
## Why the split exists
|
|
21
|
+
|
|
22
|
+
A crawler and a social preview read the HTML your server returns. Neither waits for
|
|
23
|
+
JavaScript in the browser. Content that a search engine must find is therefore
|
|
24
|
+
content that must be in the first response. Only a server render puts it there.
|
|
25
|
+
|
|
26
|
+
A flow has the reverse constraint. Its value is in the interaction: a visitor answers,
|
|
27
|
+
the flow branches, the next screen depends on what came before. None of that can be in the
|
|
28
|
+
first response, because none of it exists until the visitor acts.
|
|
29
|
+
|
|
30
|
+
Choose a Page when the content must reach a search engine. Choose a Flow when the value is
|
|
31
|
+
in the interaction and a browser render is acceptable.
|
|
32
|
+
|
|
33
|
+
## What backs a page
|
|
34
|
+
|
|
35
|
+
Four pieces sit behind every Page, and they are separate on purpose:
|
|
36
|
+
|
|
37
|
+
1. A **page layout** sets the shape: which fields the page can hold.
|
|
38
|
+
2. A **content entry** fills that shape with real data.
|
|
39
|
+
3. A **Page** binds one entry, so the SDK can render it.
|
|
40
|
+
4. The **SDK** reads the entry and renders it on your server.
|
|
41
|
+
|
|
42
|
+
Layout and entry are separate because one layout backs many entries. Describe a campaign
|
|
43
|
+
page one time, then make many pages from it. Change the layout and you must check every
|
|
44
|
+
entry again. That is why the dashboard reports an invalid entry and does not serve it.
|
|
45
|
+
|
|
46
|
+
A step layout does the same thing for one step inside a flow. The parts behave the same
|
|
47
|
+
way. Only the thing that they give shape to is different.
|
|
48
|
+
|
|
49
|
+
## They meet at the same publish
|
|
50
|
+
|
|
51
|
+
Both kinds are experiences. Both live in a workspace, both go into the same immutable
|
|
52
|
+
version, and both come out of the same Delivery API. The SDK decides where to render each
|
|
53
|
+
one, and every step before that treats them the same way.
|
|
54
|
+
|
|
55
|
+
This matters, because it means a project mixes them freely. A server-rendered
|
|
56
|
+
landing page can carry a browser-rendered quiz inside it.
|
|
57
|
+
|
|
58
|
+
## Related
|
|
59
|
+
|
|
60
|
+
- [About how Picked works](/explanation/how-picked-works) — the path that both kinds take.
|
|
61
|
+
- [About server-side rendering](/explanation/why-server-side-rendering) — how a Page
|
|
62
|
+
reaches the browser.
|
|
63
|
+
- [Pages](/reference/dashboard/content/pages) — the page editor.
|
|
64
|
+
- [Experiences](/reference/dashboard/experiences) — the flow options.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: explanation
|
|
3
|
+
layout: docs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# About the content model
|
|
7
|
+
|
|
8
|
+
Picked splits your content into three pieces: a layout, a section, and a content entry.
|
|
9
|
+
Why three, when a page editor needs only one?
|
|
10
|
+
|
|
11
|
+
## Schema and data are separate
|
|
12
|
+
|
|
13
|
+
A **layout** is a schema. It sets which fields a piece of content can hold, and it holds
|
|
14
|
+
no data itself. A **content entry** is one instance of that schema, filled with real
|
|
15
|
+
values.
|
|
16
|
+
|
|
17
|
+
One layout backs many entries. Describe a campaign page one time, then make many pages
|
|
18
|
+
from it. Each entry carries its own title, its own body, and its own image, but they all
|
|
19
|
+
share the shape.
|
|
20
|
+
|
|
21
|
+
That split is also what lets your code depend on content that it never saw. Your component
|
|
22
|
+
reads `content.title` because the layout promises a `title`. It works for every entry that
|
|
23
|
+
comes from that layout.
|
|
24
|
+
|
|
25
|
+
## A layout has a fixed category
|
|
26
|
+
|
|
27
|
+
Each layout is a **page layout** or a **step layout**, and you choose which when you
|
|
28
|
+
create it. The category cannot change later.
|
|
29
|
+
|
|
30
|
+
| Layout | It gives shape to | Who renders it |
|
|
31
|
+
| --- | --- | --- |
|
|
32
|
+
| Page layout | A standalone [Page](/reference/dashboard/content/pages). | The SDK renders the entry on your server. |
|
|
33
|
+
| Step layout | One [Step](/reference/dashboard/steps) inside a Flow. | The Step renders the entry in the browser. |
|
|
34
|
+
|
|
35
|
+
The category is fixed because it decides where the SDK renders the entry. That is not a
|
|
36
|
+
property you can change under an entry that exists already. See
|
|
37
|
+
[About pages and flows](/explanation/pages-and-flows).
|
|
38
|
+
|
|
39
|
+
## A section is a schema fragment
|
|
40
|
+
|
|
41
|
+
A layout gives shape to a whole page or a whole step. A **section** is a part that you put
|
|
42
|
+
inside one or more layouts.
|
|
43
|
+
|
|
44
|
+
Define a hero one time as a section, then point at it from every page layout that needs
|
|
45
|
+
one. Change the section and every layout that holds it changes with it.
|
|
46
|
+
|
|
47
|
+
This is the reason sections exist at all. Without them you must describe a common
|
|
48
|
+
structure again in each layout, and the copies then become different. It is the same
|
|
49
|
+
argument for one fact in one place that runs through the rest of this documentation.
|
|
50
|
+
|
|
51
|
+
## Why a schema change does not migrate your data
|
|
52
|
+
|
|
53
|
+
Change a layout or a section, and Picked does not rewrite the entries behind it. An entry
|
|
54
|
+
that no longer matches its schema becomes invalid, and the dashboard shows an error on
|
|
55
|
+
each field that does not fit.
|
|
56
|
+
|
|
57
|
+
That is deliberate. An automatic change must decide what a new required field holds for
|
|
58
|
+
every entry that came before it. Any value that it chooses is content that nobody wrote.
|
|
59
|
+
Picked reports the error instead, and you correct it.
|
|
60
|
+
|
|
61
|
+
The cost is real. After a schema change, check every entry that uses that layout and fix
|
|
62
|
+
each error before you publish. Picked serves valid content only, so an entry with an error
|
|
63
|
+
does not reach a visitor.
|
|
64
|
+
|
|
65
|
+
## An error points at the exact field
|
|
66
|
+
|
|
67
|
+
A validation error carries a path in JSON style, such as `sections[0].data.title`. The
|
|
68
|
+
path holds through nested groups, repeated fields, and sections. An error inside a
|
|
69
|
+
repeated group therefore tells you which item in that group holds it.
|
|
70
|
+
|
|
71
|
+
## Related
|
|
72
|
+
|
|
73
|
+
- [Page and step layouts](/reference/dashboard/content/layouts) — every field type.
|
|
74
|
+
- [Sections](/reference/dashboard/content/sections) — the reusable fragments.
|
|
75
|
+
- [Content entries](/reference/dashboard/content/content-entries) — the editor and the
|
|
76
|
+
validation.
|
|
77
|
+
- [About pages and flows](/explanation/pages-and-flows) — which layout category you need.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: explanation
|
|
3
|
+
layout: docs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# About the two APIs
|
|
7
|
+
|
|
8
|
+
Picked has two HTTP APIs, and a new reader often reaches for the wrong one. This page
|
|
9
|
+
covers which is which and why they are separate.
|
|
10
|
+
|
|
11
|
+
## The split
|
|
12
|
+
|
|
13
|
+
| | Delivery API | Management API |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| Style | REST | GraphQL |
|
|
16
|
+
| Access | Public, anonymous | Authenticated |
|
|
17
|
+
| Operations | Read-only | Read and write |
|
|
18
|
+
| Base | `https://api.picked.app/api/v1` | `POST https://api.picked.app/api/graphql` |
|
|
19
|
+
| Use it to | Serve published configuration and content to your application | Control your projects, experiences, and content |
|
|
20
|
+
|
|
21
|
+
The short rule is this: **the Delivery API serves your visitors, and the management API
|
|
22
|
+
serves you.**
|
|
23
|
+
|
|
24
|
+
## Why they are not one API
|
|
25
|
+
|
|
26
|
+
Three properties of the delivery path make it a different kind of API.
|
|
27
|
+
|
|
28
|
+
**It is anonymous.** A crawler must read your published page content with no credential at
|
|
29
|
+
all. An API that requires a token cannot serve a search engine, and an API that serves a
|
|
30
|
+
search engine cannot require one.
|
|
31
|
+
|
|
32
|
+
**It is cacheable.** A published response is the same for every reader, so a shared cache
|
|
33
|
+
can hold it. The Delivery API sends `s-maxage` for an SDK request, and it sends
|
|
34
|
+
`Vary: Picked-Client`. A GraphQL endpoint that takes any query over a `POST` has none of
|
|
35
|
+
those properties.
|
|
36
|
+
|
|
37
|
+
**It is read-only.** Nothing that a reader reaches without a credential can change your
|
|
38
|
+
data. Every write stays on a separate authenticated surface, so that property holds by
|
|
39
|
+
design.
|
|
40
|
+
|
|
41
|
+
## What the Delivery API serves
|
|
42
|
+
|
|
43
|
+
Two endpoints, and the SDK calls both:
|
|
44
|
+
|
|
45
|
+
- **Project config** returns the published configuration of a project: its experiences and
|
|
46
|
+
steps, and the content link on each step.
|
|
47
|
+
- **Experience content** returns the content entries behind an experience.
|
|
48
|
+
|
|
49
|
+
Your server usually calls the first and lets `serialize()` call the second. See
|
|
50
|
+
[About server-side rendering](/explanation/why-server-side-rendering).
|
|
51
|
+
|
|
52
|
+
## How a preview stays out of the search index
|
|
53
|
+
|
|
54
|
+
A preview needs unpublished content, and by the definition of it that content is not
|
|
55
|
+
public. The Delivery API takes a `preview_token` query parameter for exactly that. It then
|
|
56
|
+
treats the result differently. `versionId` comes back `null`, the response is `no-store`,
|
|
57
|
+
and `robots.txt` does not allow the preview path.
|
|
58
|
+
|
|
59
|
+
That last part matters more than it looks. A draft that anybody can read over an anonymous
|
|
60
|
+
URL is a draft that a search engine can find. A campaign page that is only part complete
|
|
61
|
+
can then appear in the search results of a customer.
|
|
62
|
+
|
|
63
|
+
## Related
|
|
64
|
+
|
|
65
|
+
- [Delivery API](/reference/delivery-api) — the endpoints and their responses.
|
|
66
|
+
- [Management API](/reference/management-api/authentication) — authentication and scopes.
|
|
67
|
+
- [Preview, cache, and crawlers](/reference/delivery-api/preview-caching-crawling) — the
|
|
68
|
+
exact cache rules and `robots.txt` rules.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: explanation
|
|
3
|
+
layout: docs
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# About server-side rendering
|
|
7
|
+
|
|
8
|
+
Why does a Page need a snapshot, when the SDK can fetch its content in the browser?
|
|
9
|
+
Because the reader that matters most does not run JavaScript.
|
|
10
|
+
|
|
11
|
+
## The constraint
|
|
12
|
+
|
|
13
|
+
A crawler and a social preview read the HTML your server returns, and they do not wait for
|
|
14
|
+
the browser to fetch anything. If the content of a Page arrives after the first response,
|
|
15
|
+
those readers never see it.
|
|
16
|
+
|
|
17
|
+
So the content has to be in the HTML. That means your server has to know, before it
|
|
18
|
+
answers, which experiences match this URL and what their content is.
|
|
19
|
+
|
|
20
|
+
## What the snapshot is for
|
|
21
|
+
|
|
22
|
+
`serialize()` answers that question on the server. It registers your project configuration and
|
|
23
|
+
evaluates each experience condition against the URL. It then reads the content for each
|
|
24
|
+
experience that matches, and returns a `PickedSnapshot` as plain JSON.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
projectConfig (Delivery API)
|
|
28
|
+
-> serialize(client, params) on the server -> PickedSnapshot
|
|
29
|
+
-> embed snapshot in HTML
|
|
30
|
+
-> client seeds from snapshot -> renders the same markup -> hydrates
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The snapshot carries the exact options the server chose with, and the condition results it
|
|
34
|
+
reached. The first render in the browser therefore reuses the decisions of the server. It
|
|
35
|
+
does not evaluate them a second time.
|
|
36
|
+
|
|
37
|
+
That last point is the whole reason the snapshot holds condition results and not only
|
|
38
|
+
content. A browser that evaluates the conditions itself can reach a different answer. A different URL shape, a
|
|
39
|
+
different time, or a different flag value each give a different result. The markup then does not
|
|
40
|
+
match what the server sent, and hydration discards the server HTML. That failure is the
|
|
41
|
+
one server-side rendering exists to prevent.
|
|
42
|
+
|
|
43
|
+
## Why a slot instead of a selector
|
|
44
|
+
|
|
45
|
+
A browser-rendered flow points at a DOM selector, and the SDK inserts the component next
|
|
46
|
+
to the element that it finds. That cannot work on a server, because a server has no DOM
|
|
47
|
+
to search.
|
|
48
|
+
|
|
49
|
+
A slot reverses that. `<ExperienceSlot name="page" />` is a mount point that you place in
|
|
50
|
+
your own markup. An experience joins it when its target selector is `.picked-slot-<name>`.
|
|
51
|
+
Your component tree decides where the content goes, so the server renders it in one pass.
|
|
52
|
+
|
|
53
|
+
## Why placements have an order
|
|
54
|
+
|
|
55
|
+
More than one experience can point at the same slot, so the slot needs a rule. The order
|
|
56
|
+
is:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
replace -> before -> prepend -> append -> after
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`replace` wins: an active experience with that placement renders alone. Every other active
|
|
63
|
+
experience renders in the order above.
|
|
64
|
+
|
|
65
|
+
The rule exists so the outcome does not depend on which experience came first. Two editors
|
|
66
|
+
can add an experience to the same slot on the same day. The result then follows from the
|
|
67
|
+
placements, and not from the database.
|
|
68
|
+
|
|
69
|
+
## Related
|
|
70
|
+
|
|
71
|
+
- [Your first server-rendered page](/tutorials/first-server-rendered-page) — the whole
|
|
72
|
+
path, in practice.
|
|
73
|
+
- [Server snapshot](/reference/sdk/ssr/serialize) — every `serialize()` parameter.
|
|
74
|
+
- [Slots and placements](/reference/sdk/ssr/slots-and-placements) — the exact rules for
|
|
75
|
+
the order.
|
|
76
|
+
- [About pages and flows](/explanation/pages-and-flows) — which kind you need.
|