@picked/client 0.0.1-dev.1776647091821 → 0.0.1-dev.1776662399138
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/{api-BQ0tb4IV.js → api-C5p0q172.js} +1 -1
- package/dist/{api-pkh5K05P.cjs → api-CE3ISEsc.cjs} +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/plugins/dynamic-experiences/index.js +1 -1
- package/dist/docs/actions.md +58 -0
- package/dist/docs/api/index.md +32 -0
- package/dist/docs/api-keys.md +33 -0
- package/dist/docs/conditions.md +52 -0
- package/dist/docs/experiences.md +33 -0
- package/dist/docs/features.md +51 -0
- package/dist/docs/fields.md +70 -0
- package/dist/docs/flow-builder/building-flows.md +61 -0
- package/dist/docs/flow-builder/condition-builder.md +54 -0
- package/dist/docs/flow-builder/keyboard-shortcuts.md +66 -0
- package/dist/docs/flow-builder/nodes.md +43 -0
- package/dist/docs/flow-builder/overview.md +43 -0
- package/dist/docs/index.md +79 -0
- package/dist/docs/pickedql/data-sources.md +39 -0
- package/dist/docs/pickedql/dimensions.md +23 -0
- package/dist/docs/pickedql/examples.md +165 -0
- package/dist/docs/pickedql/filtering.md +31 -0
- package/dist/docs/pickedql/index.md +34 -0
- package/dist/docs/pickedql/schema.md +95 -0
- package/dist/docs/pickedql/virtual-functions.md +84 -0
- package/dist/docs/projects.md +30 -0
- package/dist/docs/reference/index.md +11 -0
- package/dist/docs/reports.md +11 -0
- package/dist/docs/sdk/actions.md +7 -0
- package/dist/docs/sdk/conditions.md +131 -0
- package/dist/docs/sdk/content-security-policy.md +31 -0
- package/dist/docs/sdk/experiences.md +41 -0
- package/dist/docs/sdk/features.md +213 -0
- package/dist/docs/sdk/fields.md +263 -0
- package/dist/docs/sdk/index.md +74 -0
- package/dist/docs/sdk/plugins/create-experience.md +133 -0
- package/dist/docs/sdk/plugins/dynamic-experiences.md +59 -0
- package/dist/docs/sdk/plugins/index.md +9 -0
- package/dist/docs/sdk/plugins/window-object.md +20 -0
- package/dist/docs/sdk/predicates.md +113 -0
- package/dist/docs/sdk/steps.md +411 -0
- package/dist/docs/steps.md +35 -0
- package/dist/docs/team-management.md +40 -0
- package/dist/docs/tooling/chrome-extension.md +29 -0
- package/dist/docs/tooling/gtm-template.md +69 -0
- package/dist/docs/tooling/index.md +19 -0
- package/dist/docs/tooling/mcp.md +120 -0
- package/dist/docs/tooling/vite-plugin.md +82 -0
- package/dist/docs/workspaces.md +35 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/plugins/dynamic-experiences/index.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Virtual functions
|
|
6
|
+
|
|
7
|
+
Virtual functions are shorthand for aggregate expressions. They are **table-aware** — the same function expands differently on `metrics` (pre-aggregated) vs `events` (user-level).
|
|
8
|
+
|
|
9
|
+
## A/B test metrics
|
|
10
|
+
|
|
11
|
+
| Function | Description |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `control_users()` | Unique users in the Control group |
|
|
14
|
+
| `variant_users()` | Unique users in the Variant group |
|
|
15
|
+
| `all_users()` | All unique users |
|
|
16
|
+
| `control_sessions()` | Unique sessions in the Control group |
|
|
17
|
+
| `variant_sessions()` | Unique sessions in the Variant group |
|
|
18
|
+
| `all_sessions()` | All unique sessions |
|
|
19
|
+
|
|
20
|
+
## Conversion metrics
|
|
21
|
+
|
|
22
|
+
| Function | Description |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `total_purchase_users()` | Users who completed a purchase |
|
|
25
|
+
| `control_purchase_users()` | Control users who purchased (`metrics` only) |
|
|
26
|
+
| `variant_purchase_users()` | Variant users who purchased (`metrics` only) |
|
|
27
|
+
| `total_purchase_sessions()` | Sessions with a purchase (`metrics` only) |
|
|
28
|
+
| `total_purchase_orders()` | Unique order count (`metrics` only) |
|
|
29
|
+
| `total_cart_add_users()` | Users who added to cart |
|
|
30
|
+
|
|
31
|
+
## Conversion rates
|
|
32
|
+
|
|
33
|
+
| Function | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `control_conversion_rate()` | Purchase conversion rate for Control (purchase users / total users) |
|
|
36
|
+
| `variant_conversion_rate()` | Purchase conversion rate for Variant |
|
|
37
|
+
| `conversion_rate_uplift()` | Relative improvement: `(variant CR - control CR) / control CR` |
|
|
38
|
+
|
|
39
|
+
## Visitor segmentation (`metrics` only)
|
|
40
|
+
|
|
41
|
+
| Function | Description |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `new_users()` | First-time visitors |
|
|
44
|
+
| `returning_users()` | Returning visitors |
|
|
45
|
+
| `new_sessions()` | Sessions from new visitors |
|
|
46
|
+
| `returning_sessions()` | Sessions from returning visitors |
|
|
47
|
+
|
|
48
|
+
## Funnel metrics (`events` only)
|
|
49
|
+
|
|
50
|
+
:::note
|
|
51
|
+
Funnel metrics are being continuously updated. Check back for new functions.
|
|
52
|
+
:::
|
|
53
|
+
|
|
54
|
+
| Function | Description |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `step_enter_users()` | Users who entered a funnel step |
|
|
57
|
+
| `step_enter_sessions()` | Sessions that entered a funnel step |
|
|
58
|
+
|
|
59
|
+
## Feature functions (`features` / `events`)
|
|
60
|
+
|
|
61
|
+
Parameterized functions that accept the feature name, bucket, and an optional feature key:
|
|
62
|
+
|
|
63
|
+
| Function | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `feature_users('name', 'bucket' [, 'key'])` | Unique users in a feature bucket |
|
|
66
|
+
| `feature_sessions('name', 'bucket' [, 'key'])` | Unique sessions in a feature bucket |
|
|
67
|
+
| `feature_events('name', 'bucket' [, 'key'])` | Unique events in a feature bucket |
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
-- Compare feature buckets
|
|
71
|
+
SELECT
|
|
72
|
+
feature_users('homepage_test', 'control') AS control,
|
|
73
|
+
feature_users('homepage_test', 'variant') AS variant
|
|
74
|
+
FROM features
|
|
75
|
+
|
|
76
|
+
-- With feature key filter
|
|
77
|
+
SELECT feature_users('homepage_test', 'control', 'v1') FROM features
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Other (`metrics` only)
|
|
81
|
+
|
|
82
|
+
| Function | Description |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `event_count()` | Total event count (from pre-aggregation) |
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Projects
|
|
6
|
+
|
|
7
|
+
A project represents one website or app you want to instrument with Picked. Each project has its own set of experiences, entities, and configuration.
|
|
8
|
+
|
|
9
|
+
## Create a project
|
|
10
|
+
|
|
11
|
+
You need an organization first. If you don't have one, create it when you sign up.
|
|
12
|
+
|
|
13
|
+
1. Open your organization.
|
|
14
|
+
2. Click **New Project**.
|
|
15
|
+
3. Enter a name.
|
|
16
|
+
4. Click **Create**.
|
|
17
|
+
|
|
18
|
+
## Workspaces
|
|
19
|
+
|
|
20
|
+
Each project contains one or more [Workspaces](/workspaces) for configuring and previewing experiences before publishing.
|
|
21
|
+
|
|
22
|
+
## Project home
|
|
23
|
+
|
|
24
|
+
When you open a project, you land on the project home page. It shows your SDK installation snippet and a getting-started checklist.
|
|
25
|
+
|
|
26
|
+
Copy the snippet and add it to your site. This connects your site to the project and enables experience delivery. See [SDK Installation](/sdk) for setup details.
|
|
27
|
+
|
|
28
|
+
## Project navigation
|
|
29
|
+
|
|
30
|
+
The sidebar shows all sections available in your project: **Experiences**, **Fields**, **Steps**, **Actions**, **Conditions**, and more. Some sections appear only when enabled for your account.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Reports
|
|
6
|
+
|
|
7
|
+
Reports show analytics data for your project. Track how visitors interact with your experiences and features.
|
|
8
|
+
|
|
9
|
+
## View report data
|
|
10
|
+
|
|
11
|
+
Open a report to see its analytics. Data updates as new visitor interactions are recorded.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Conditions
|
|
6
|
+
|
|
7
|
+
Conditions evaluate of the current state of the Picked client to decide outcomes as defined by the user. Conditions are `JsonLogic`, which will always return the same result given the same input.
|
|
8
|
+
|
|
9
|
+
## Conditions in Experiences
|
|
10
|
+
|
|
11
|
+
A common method of navigating a Funnel Experiences is using conditions between steps. The condition will evaludate values from the Picked state, and navigation the user based on a boolean value
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
const questionaireFunnel = createExperience({
|
|
15
|
+
id: 'mini-funnel',
|
|
16
|
+
// experience config
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const userInformationStep = funnel.addStep({
|
|
20
|
+
fields: ['email-address-field', 'sign-up-for-newsletter-field'],
|
|
21
|
+
})
|
|
22
|
+
const signUserUpToNewsletter = funnel.addStep({
|
|
23
|
+
// step config
|
|
24
|
+
})
|
|
25
|
+
const userDoesNotWantNewsletter = funnel.addStep({
|
|
26
|
+
// step config
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const isSigningUpForNewsletter = funnel.addConditional({
|
|
30
|
+
condition: {
|
|
31
|
+
'==': [
|
|
32
|
+
{
|
|
33
|
+
var: 'responses.sign-up-for-newsletter-field', // check if response value for the field is true
|
|
34
|
+
},
|
|
35
|
+
true,
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
userInformationStep.nextConditional(isSigningUpForNewsletter)
|
|
41
|
+
|
|
42
|
+
isSigningUpForNewsletter.addTrue(signUserUpToNewsletter)
|
|
43
|
+
isSigningUpForNewsletter.addFalse(userDoesNotWantNewsletter)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Conditional Fields
|
|
47
|
+
|
|
48
|
+
Fields can be shown or hidden based on previous responses or predicates using JSON Logic conditions:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// Show email field only if user selected "email" as a contact method
|
|
52
|
+
export const emailField = createField({
|
|
53
|
+
id: 'email-input',
|
|
54
|
+
name: 'email-input',
|
|
55
|
+
type: 'text',
|
|
56
|
+
label: 'Email Address',
|
|
57
|
+
multiple: false,
|
|
58
|
+
required: true,
|
|
59
|
+
condition: {
|
|
60
|
+
in: [
|
|
61
|
+
'email',
|
|
62
|
+
{
|
|
63
|
+
var: 'responses.communication-methods', // Reference previous field
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Show field based on predicate
|
|
70
|
+
export const mailingAddressField = createField({
|
|
71
|
+
id: 'mailing-address',
|
|
72
|
+
name: 'mailing-address',
|
|
73
|
+
type: 'text',
|
|
74
|
+
label: 'Mailing Address',
|
|
75
|
+
multiple: false,
|
|
76
|
+
required: true,
|
|
77
|
+
condition: {
|
|
78
|
+
'==': [
|
|
79
|
+
{
|
|
80
|
+
invoke: {
|
|
81
|
+
var: 'predicates.user-allows-mail',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
true,
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Field Options with Conditions
|
|
91
|
+
|
|
92
|
+
Individual options within a field can also have conditional visibility:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
export const contactMethodsField = createField({
|
|
96
|
+
id: 'contact-methods',
|
|
97
|
+
name: 'contact-methods',
|
|
98
|
+
type: 'text',
|
|
99
|
+
label: 'How should we contact you?',
|
|
100
|
+
multiple: true,
|
|
101
|
+
required: true,
|
|
102
|
+
options: [
|
|
103
|
+
{
|
|
104
|
+
id: 'email',
|
|
105
|
+
label: 'Email',
|
|
106
|
+
value: 'email',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'phone',
|
|
110
|
+
label: 'Phone',
|
|
111
|
+
value: 'phone',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'mail',
|
|
115
|
+
label: 'Postal Mail',
|
|
116
|
+
value: 'mail',
|
|
117
|
+
// Only show this option if user has a verified address
|
|
118
|
+
condition: {
|
|
119
|
+
'==': [
|
|
120
|
+
{
|
|
121
|
+
invoke: {
|
|
122
|
+
var: 'predicates.has-verified-address',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
true,
|
|
126
|
+
],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Content Security Policy
|
|
6
|
+
|
|
7
|
+
The Picked SDK makes outbound HTTPS requests to the following domains. Add them to your `connect-src` directive.
|
|
8
|
+
|
|
9
|
+
## Required domains
|
|
10
|
+
|
|
11
|
+
| Domain | Purpose |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `api.picked.app` | API requests — project configuration, experiences, session tracking |
|
|
14
|
+
| `evnts.pckd.dev` | Analytics event collection (Snowplow) |
|
|
15
|
+
|
|
16
|
+
## Configuration
|
|
17
|
+
|
|
18
|
+
As an HTTP header:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Content-Security-Policy: connect-src 'self' https://api.picked.app https://evnts.pckd.dev;
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Or as a `<meta>` tag:
|
|
25
|
+
|
|
26
|
+
```html
|
|
27
|
+
<meta
|
|
28
|
+
http-equiv="Content-Security-Policy"
|
|
29
|
+
content="connect-src 'self' https://api.picked.app https://evnts.pckd.dev;"
|
|
30
|
+
/>
|
|
31
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Experiences
|
|
6
|
+
|
|
7
|
+
Experiences enable you to conditionally show UI components in your application based on custom logic. Each experience defines a component to render, where to inject it in the DOM, and an optional condition that controls when it appears. Experiences support multi-step flows with fields and branching logic, making them ideal for onboarding, surveys, and interactive product tours
|
|
8
|
+
|
|
9
|
+
## Creating Experiences
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { createExperience } from '@picked/client/plugins/create-experience'
|
|
13
|
+
|
|
14
|
+
export const productPageExperience = createExperience({
|
|
15
|
+
id: 'product-page-experience',
|
|
16
|
+
name: 'Product Page Experience,
|
|
17
|
+
condition: {
|
|
18
|
+
'==': [
|
|
19
|
+
{
|
|
20
|
+
var: 'location.pathname',
|
|
21
|
+
},
|
|
22
|
+
'/product/',
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
targets: [{ selector: 'body', placement: 'after' }],
|
|
26
|
+
component: adaptSvelteComponent(import('../ui/ProductPageExperience.svelte', {
|
|
27
|
+
css: [appCss],
|
|
28
|
+
}),
|
|
29
|
+
})
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## How Experiences Work
|
|
33
|
+
|
|
34
|
+
When an experience is registered, the SDK follows this execution flow:
|
|
35
|
+
|
|
36
|
+
1. **Registration**: Experiences are registered to the Picked client using [createExperiencePlugin](./plugins/create-experience.md) for code-defined experiences, or [dynamicExperiencesPlugin](./plugins/dynamic-experiences.md) for dashboard-managed experiences
|
|
37
|
+
2. **Condition Evaluation**: The SDK evaluates the experience's `condition` using the logic engine to determine if it should be shown
|
|
38
|
+
3. **Target Resolution**: The SDK searches the DOM for elements matching the `targets` selectors (e.g., `body`, `.product-page`, `#checkout`)
|
|
39
|
+
4. **Initialization**: If the condition evaluates to `true` and at least one target element exists, the experience is initialized
|
|
40
|
+
5. **Component Rendering**: The experience's component is rendered and injected into the DOM at the specified target location and placement (before, after, append, prepend, or replace)
|
|
41
|
+
6. **Flow Execution**: If the experience contains steps, fields, and conditionals, users can progress through the multi-step flow with validation and branching logic
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
---
|
|
2
|
+
layout: docs
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Features
|
|
6
|
+
|
|
7
|
+
Features enable A/B testing and feature flagging in your application. Each feature defines decision logic that resolves to a `bucket` as either a boolean value (`true`/`false`) or a string value (like `"control"`, `"variant-a"`, or `"variant-b"`). Once a feature is decided, the result is cached in browser storage to ensure consistent user experiences across sessions.
|
|
8
|
+
|
|
9
|
+
## Defining Features
|
|
10
|
+
|
|
11
|
+
Define features when creating the Picked client by passing a `features` object. Each feature requires a unique identifier, a versioning key, a human-readable name, and a `decide` function:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createClient } from '@picked/client'
|
|
15
|
+
|
|
16
|
+
const client = createClient({
|
|
17
|
+
organizationId: 'your-org-id',
|
|
18
|
+
projectId: 'your-project-id',
|
|
19
|
+
features: {
|
|
20
|
+
'summer-sale': {
|
|
21
|
+
key: 'summer-sale-v1', // Version key - change to invalidate cache
|
|
22
|
+
name: 'Summer Sale Experiment', // Human-readable name for analytics
|
|
23
|
+
decide: async () => {
|
|
24
|
+
// Your decision logic here
|
|
25
|
+
const isEligible = await checkUserEligibility()
|
|
26
|
+
return isEligible // Returns boolean
|
|
27
|
+
},
|
|
28
|
+
storage: 'local', // Optional: 'local' (default) or 'session'
|
|
29
|
+
},
|
|
30
|
+
'homepage-variant': {
|
|
31
|
+
key: 'homepage-test-v2',
|
|
32
|
+
name: 'Homepage A/B Test',
|
|
33
|
+
decide: async () => {
|
|
34
|
+
// Return string for multi-variant tests
|
|
35
|
+
return Math.random() < 0.5 ? 'variant-a' : 'variant-b'
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
})
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Features can also be defined as factory functions for lazy initialization:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
features: {
|
|
46
|
+
'my-feature': () => ({
|
|
47
|
+
key: 'my-feature-v1',
|
|
48
|
+
name: 'My Feature',
|
|
49
|
+
decide: async () => true,
|
|
50
|
+
}),
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## How Features Work
|
|
55
|
+
|
|
56
|
+
When a feature is registered, the SDK follows this execution order:
|
|
57
|
+
|
|
58
|
+
1. **Registration**: Features are registered to the Picked client during initialization
|
|
59
|
+
2. **Cache Check**: The SDK checks browser storage (localStorage or sessionStorage) for a previously resolved value using the feature `id` and `key`
|
|
60
|
+
3. **Lazy Evaluation**: If no cached value exists, the `decide` function will be called the first time the feature is accessed
|
|
61
|
+
4. **Storage**: Once resolved, the bucket value is persisted to browser storage for consistency across page loads
|
|
62
|
+
5. **Event Emission**: A `feature_decide` event is fired on the event bus when a feature is resolved for the first time
|
|
63
|
+
|
|
64
|
+
## URL-Based Overrides
|
|
65
|
+
|
|
66
|
+
You can override feature decisions via URL search parameters for testing purposes:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
https://example.com?picked-feature=my-feature:true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Multiple overrides can be specified:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
https://example.com?picked-feature=my-feature:true&picked-feature=homepage-variant:variant-a
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The format is `?picked-feature=<feature-id>:<bucket-value>`.
|
|
79
|
+
|
|
80
|
+
This is useful for QA testing specific feature variants without modifying code. The override is persisted in session storage and will be cleared when the browser tab is closed.
|
|
81
|
+
|
|
82
|
+
## Accessing Features
|
|
83
|
+
|
|
84
|
+
Once features are defined, you can access their resolved bucket values in your application code. The SDK provides framework-specific hooks and a framework-agnostic event bus for subscribing to feature decisions.
|
|
85
|
+
|
|
86
|
+
:::code-group
|
|
87
|
+
|
|
88
|
+
```ts [Vanilla JS]
|
|
89
|
+
import { createClient } from '@picked/client'
|
|
90
|
+
|
|
91
|
+
const client = createClient({
|
|
92
|
+
/* Create client options here */
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
client.eventBus.subscribe('feature_decide', ({ feature }) => {
|
|
96
|
+
if (feature.id === 'my-feature') {
|
|
97
|
+
console.log(feature) // { id: 'my-feature', bucket: 'control' }
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```ts [Svelte]
|
|
103
|
+
<script lang="ts">
|
|
104
|
+
import { getFeature } from '@picked/svelte/hooks'
|
|
105
|
+
// Returns an Effector store containing the feature resolver
|
|
106
|
+
const myFeature = getFeature('my-feature')
|
|
107
|
+
</script>
|
|
108
|
+
<!-- $myFeature is a reactive Effector store (see Effector docs for details) -->
|
|
109
|
+
{#if $myFeature()}
|
|
110
|
+
<p>My feature is enabled</p>
|
|
111
|
+
{:else}
|
|
112
|
+
<p>My feature is disabled</p>
|
|
113
|
+
{/if}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```ts [React]
|
|
117
|
+
import { useFeature } from '@picked/react/hooks'
|
|
118
|
+
|
|
119
|
+
function MyComponent() {
|
|
120
|
+
// Returns the resolved bucket value for 'my-feature'
|
|
121
|
+
const myFeature = useFeature('my-feature')
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div>
|
|
125
|
+
{myFeature === 'control' && <p>Control variant</p>}
|
|
126
|
+
{myFeature === 'variant-a' && <p>Variant A</p>}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
:::
|
|
134
|
+
|
|
135
|
+
## Using Features in Experience Conditions
|
|
136
|
+
|
|
137
|
+
Features can be used with experience conditions via JSON logic:
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const myExperience = createExperience({
|
|
141
|
+
// ... other config
|
|
142
|
+
condition: {
|
|
143
|
+
and: [
|
|
144
|
+
{
|
|
145
|
+
'==': [
|
|
146
|
+
{
|
|
147
|
+
invoke: {
|
|
148
|
+
var: 'features.summer-sale', // Reference feature by ID
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
true, // Check if bucket equals true
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Listening to Feature Decisions
|
|
160
|
+
|
|
161
|
+
Subscribe to the `feature_decide` event to track when features are resolved:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
client.eventBus.subscribe('feature_decide', ({ feature }) => {
|
|
165
|
+
console.log(`Feature "${feature.id}" resolved to bucket:`, feature.bucket)
|
|
166
|
+
// Example output: Feature "summer-sale" resolved to bucket: true
|
|
167
|
+
|
|
168
|
+
// Send to analytics
|
|
169
|
+
analytics.track('Feature Decision', {
|
|
170
|
+
featureId: feature.id,
|
|
171
|
+
bucket: feature.bucket,
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Versioning and cache invalidation
|
|
177
|
+
|
|
178
|
+
Feature decisions are cached in browser storage (localStorage or sessionStorage) using a key derived from the feature ID and version. This ensures visitors see a consistent experience across page loads.
|
|
179
|
+
|
|
180
|
+
### How versioning works
|
|
181
|
+
|
|
182
|
+
When you create a feature in the dashboard, it starts at version 1. The SDK uses the version to build the cache key — for example, a feature at version 3 uses the key `v3`. This version is also included in the allocation hash, so it directly affects which variant a visitor is assigned to.
|
|
183
|
+
|
|
184
|
+
### What happens when the version is incremented
|
|
185
|
+
|
|
186
|
+
When you increment a feature's version in the dashboard:
|
|
187
|
+
|
|
188
|
+
1. **Cache keys change.** The old cache entry (e.g., `v3`) no longer matches the new key (`v4`), so the SDK treats the feature as undecided.
|
|
189
|
+
2. **All visitors are re-allocated.** The allocation hash includes the version, so every visitor gets a new hash value and may be assigned to a different variant.
|
|
190
|
+
3. **The `decide` function runs again.** On the next page load, the SDK calls `decide()` fresh and caches the new result.
|
|
191
|
+
|
|
192
|
+
> **Warning:** Incrementing the version re-allocates all visitors, not just new ones. This is irreversible. Only increment when you intentionally want to reset all assignments — for example, after changing traffic allocation weights.
|
|
193
|
+
|
|
194
|
+
### SDK `key` vs dashboard version
|
|
195
|
+
|
|
196
|
+
If you define features statically in the SDK (not loaded from the dashboard), use the `key` field for manual versioning:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
features: {
|
|
200
|
+
'summer-sale': {
|
|
201
|
+
key: 'summer-sale-v2', // Change to invalidate cache
|
|
202
|
+
// ...
|
|
203
|
+
},
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
For features managed through the dashboard, versioning is handled automatically — the dashboard appends the version number to the cache key used by the SDK.
|
|
208
|
+
|
|
209
|
+
## Best Practices
|
|
210
|
+
|
|
211
|
+
- **Use `session` storage for temporary tests**: Set `storage: 'session'` for short-lived experiments that shouldn't persist across browser sessions
|
|
212
|
+
- **Keep `decide` functions fast**: The function blocks experience rendering, so avoid heavy computations or slow network calls
|
|
213
|
+
- **Return consistent types**: Don't mix boolean and string returns for the same feature - pick one type and stick with it
|