@sanity/personalization-plugin 2.2.0-growthbook.1 → 2.2.0
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 +0 -2
- package/dist/index.d.mts +1 -205
- package/dist/index.d.ts +1 -205
- package/dist/index.js +42 -133
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +45 -137
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -3
- package/src/components/ExperimentContext.tsx +5 -8
- package/src/components/ExperimentField.tsx +32 -56
- package/src/components/ExperimentInput.tsx +14 -4
- package/src/fieldExperiments.tsx +5 -2
- package/src/index.ts +0 -1
- package/src/types.ts +1 -182
- package/src/components/Secrets.tsx +0 -47
- package/src/growthbookFieldExperiments.tsx +0 -51
- package/src/utils/growthbook.ts +0 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity/personalization-plugin",
|
|
3
|
-
"version": "2.2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Plugin to help with personalization, a/b testing when using Sanity",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -45,7 +45,6 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@sanity/incompatible-plugin": "^1.0.4",
|
|
48
|
-
"@sanity/studio-secrets": "^3.0.0",
|
|
49
48
|
"@sanity/ui": "^2.8.19",
|
|
50
49
|
"@sanity/uuid": "^3.0.2",
|
|
51
50
|
"fast-deep-equal": "^3.1.3",
|
|
@@ -79,7 +78,7 @@
|
|
|
79
78
|
"typescript": "^5.7.3"
|
|
80
79
|
},
|
|
81
80
|
"peerDependencies": {
|
|
82
|
-
"react": "^18",
|
|
81
|
+
"react": "^18 || ^19",
|
|
83
82
|
"sanity": "^3"
|
|
84
83
|
},
|
|
85
84
|
"engines": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import equal from 'fast-deep-equal'
|
|
2
|
-
import {createContext, useContext, useMemo
|
|
2
|
+
import {createContext, useContext, useMemo} from 'react'
|
|
3
3
|
import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
|
|
4
4
|
import {suspend} from 'suspend-react'
|
|
5
5
|
|
|
@@ -21,8 +21,6 @@ export const CONFIG_DEFAULT = {
|
|
|
21
21
|
export const ExperimentContext = createContext<ExperimentContextProps>({
|
|
22
22
|
...CONFIG_DEFAULT,
|
|
23
23
|
experiments: [],
|
|
24
|
-
setSecret: () => undefined,
|
|
25
|
-
secret: undefined,
|
|
26
24
|
})
|
|
27
25
|
|
|
28
26
|
export function useExperimentContext() {
|
|
@@ -35,7 +33,6 @@ type ExperimentProps = ObjectInputProps & {
|
|
|
35
33
|
|
|
36
34
|
export function ExperimentProvider(props: ExperimentProps) {
|
|
37
35
|
const {experimentFieldPluginConfig} = props
|
|
38
|
-
const [secret, setSecret] = useState<string | undefined>()
|
|
39
36
|
|
|
40
37
|
const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
|
|
41
38
|
const workspace = useWorkspace()
|
|
@@ -47,17 +44,17 @@ export function ExperimentProvider(props: ExperimentProps) {
|
|
|
47
44
|
// eslint-disable-next-line require-await
|
|
48
45
|
async () => {
|
|
49
46
|
if (typeof experimentFieldPluginConfig.experiments === 'function') {
|
|
50
|
-
return experimentFieldPluginConfig.experiments(client
|
|
47
|
+
return experimentFieldPluginConfig.experiments(client)
|
|
51
48
|
}
|
|
52
49
|
return experimentFieldPluginConfig.experiments
|
|
53
50
|
},
|
|
54
|
-
[workspace
|
|
51
|
+
[workspace],
|
|
55
52
|
{equal},
|
|
56
53
|
)
|
|
57
54
|
|
|
58
55
|
const context = useMemo(
|
|
59
|
-
() => ({...experimentFieldPluginConfig, experiments
|
|
60
|
-
[experimentFieldPluginConfig, experiments
|
|
56
|
+
() => ({...experimentFieldPluginConfig, experiments}),
|
|
57
|
+
[experimentFieldPluginConfig, experiments],
|
|
61
58
|
)
|
|
62
59
|
|
|
63
60
|
return (
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {CloseIcon} from '@sanity/icons'
|
|
2
|
-
import {useCallback, useMemo} from 'react'
|
|
3
2
|
import {GiSoapExperiment} from 'react-icons/gi'
|
|
4
3
|
import {
|
|
5
4
|
defineDocumentFieldAction,
|
|
@@ -19,9 +18,9 @@ const useAddExperimentAction = (
|
|
|
19
18
|
): DocumentFieldActionItem => {
|
|
20
19
|
const {onChange, active, experimentNameOverride} = props
|
|
21
20
|
|
|
22
|
-
const handleAddAction =
|
|
21
|
+
const handleAddAction = () => {
|
|
23
22
|
onChange([set(!active, ['active'])])
|
|
24
|
-
}
|
|
23
|
+
}
|
|
25
24
|
|
|
26
25
|
return {
|
|
27
26
|
title: `Add ${experimentNameOverride}`,
|
|
@@ -34,22 +33,23 @@ const useAddExperimentAction = (
|
|
|
34
33
|
|
|
35
34
|
const useRemoveExperimentAction = (
|
|
36
35
|
props: DocumentFieldActionProps &
|
|
37
|
-
PatchStuff & {
|
|
38
|
-
experimentNameOverride: string
|
|
39
|
-
experimentId: string
|
|
40
|
-
active: boolean
|
|
41
|
-
variantNameOverride: string
|
|
42
|
-
},
|
|
36
|
+
PatchStuff & {experimentNameOverride: string; experimentId: string; active: boolean},
|
|
43
37
|
): DocumentFieldActionItem => {
|
|
44
|
-
const {onChange, active, experimentId, experimentNameOverride
|
|
45
|
-
|
|
46
|
-
const handleClearAction = useCallback(() => {
|
|
38
|
+
const {onChange, active, experimentId, experimentNameOverride} = props
|
|
39
|
+
const patchActiveFalseEvent = () => {
|
|
47
40
|
const activeId = ['active']
|
|
41
|
+
return set(!active, activeId)
|
|
42
|
+
}
|
|
43
|
+
const patchClearEvent = () => {
|
|
48
44
|
const experiment = [experimentId]
|
|
49
|
-
const variants = [
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
45
|
+
const variants = [experimentNameOverride]
|
|
46
|
+
return [unset(experiment), unset(variants)]
|
|
47
|
+
}
|
|
48
|
+
const handleClearAction = () => {
|
|
49
|
+
const clearEvents = patchClearEvent()
|
|
50
|
+
const activeEvent = patchActiveFalseEvent()
|
|
51
|
+
onChange([activeEvent, ...clearEvents])
|
|
52
|
+
}
|
|
53
53
|
return {
|
|
54
54
|
title: `Remove ${experimentNameOverride}`,
|
|
55
55
|
type: 'action',
|
|
@@ -59,19 +59,13 @@ const useRemoveExperimentAction = (
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
const
|
|
62
|
+
const newActions = ({
|
|
63
63
|
onChange,
|
|
64
64
|
inputId,
|
|
65
65
|
active,
|
|
66
66
|
experimentNameOverride,
|
|
67
67
|
experimentId,
|
|
68
|
-
|
|
69
|
-
}: PatchStuff & {
|
|
70
|
-
active?: boolean
|
|
71
|
-
experimentNameOverride: string
|
|
72
|
-
experimentId: string
|
|
73
|
-
variantNameOverride: string
|
|
74
|
-
}) => {
|
|
68
|
+
}: PatchStuff & {active?: boolean; experimentNameOverride: string; experimentId: string}) => {
|
|
75
69
|
const removeAction = defineDocumentFieldAction({
|
|
76
70
|
name: `Remove ${experimentNameOverride}`,
|
|
77
71
|
useAction: (props) =>
|
|
@@ -82,7 +76,6 @@ const createActions = ({
|
|
|
82
76
|
inputId,
|
|
83
77
|
experimentNameOverride,
|
|
84
78
|
experimentId,
|
|
85
|
-
variantNameOverride,
|
|
86
79
|
}),
|
|
87
80
|
})
|
|
88
81
|
const addAction = defineDocumentFieldAction({
|
|
@@ -97,44 +90,27 @@ const createActions = ({
|
|
|
97
90
|
experimentId,
|
|
98
91
|
}),
|
|
99
92
|
})
|
|
100
|
-
|
|
93
|
+
if (active) {
|
|
94
|
+
return removeAction
|
|
95
|
+
}
|
|
96
|
+
return addAction
|
|
101
97
|
}
|
|
102
98
|
|
|
103
99
|
export const ExperimentField = (
|
|
104
|
-
props: ObjectFieldProps & {
|
|
105
|
-
experimentNameOverride: string
|
|
106
|
-
experimentId: string
|
|
107
|
-
variantNameOverride: string
|
|
108
|
-
},
|
|
100
|
+
props: ObjectFieldProps & {experimentNameOverride: string; experimentId: string},
|
|
109
101
|
) => {
|
|
110
102
|
const {onChange} = props.inputProps
|
|
111
|
-
const {inputId, experimentNameOverride, experimentId
|
|
103
|
+
const {inputId, experimentNameOverride, experimentId} = props
|
|
112
104
|
const active = props.value?.active as boolean | undefined
|
|
113
105
|
|
|
114
|
-
const
|
|
115
|
-
() => ({
|
|
116
|
-
onChange,
|
|
117
|
-
inputId,
|
|
118
|
-
active,
|
|
119
|
-
experimentNameOverride,
|
|
120
|
-
experimentId,
|
|
121
|
-
variantNameOverride,
|
|
122
|
-
}),
|
|
123
|
-
[onChange, inputId, active, experimentNameOverride, experimentId, variantNameOverride],
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
const memoizedActions = useMemo(() => {
|
|
127
|
-
const oldActions = props.actions || []
|
|
128
|
-
return [createActions(actionProps), ...oldActions]
|
|
129
|
-
}, [actionProps, props.actions])
|
|
130
|
-
|
|
131
|
-
const withActionProps = useMemo(
|
|
132
|
-
() => ({
|
|
133
|
-
...props,
|
|
134
|
-
actions: memoizedActions,
|
|
135
|
-
}),
|
|
136
|
-
[props, memoizedActions],
|
|
137
|
-
)
|
|
106
|
+
const oldActions = props.actions || []
|
|
138
107
|
|
|
108
|
+
const withActionProps = {
|
|
109
|
+
...props,
|
|
110
|
+
actions: [
|
|
111
|
+
newActions({onChange, inputId, active, experimentNameOverride, experimentId}),
|
|
112
|
+
...oldActions,
|
|
113
|
+
],
|
|
114
|
+
}
|
|
139
115
|
return props.renderDefault(withActionProps)
|
|
140
116
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {Card, Text} from '@sanity/ui'
|
|
1
2
|
import {FormEvent, useCallback, useMemo} from 'react'
|
|
2
3
|
import {
|
|
3
4
|
FormPatch,
|
|
@@ -20,16 +21,18 @@ const formatlistOptions = (experiments: ExperimentType[]): SelectOption[] =>
|
|
|
20
21
|
value: experiment.id,
|
|
21
22
|
}))
|
|
22
23
|
|
|
23
|
-
export const ExperimentInput = (
|
|
24
|
+
export const ExperimentInput = (
|
|
25
|
+
props: StringInputProps & {variantNameOverride: string; experimentNameOverride: string},
|
|
26
|
+
) => {
|
|
24
27
|
const {experiments} = useExperimentContext()
|
|
25
28
|
|
|
26
29
|
const id = useFormValue(['_id']) as string
|
|
27
30
|
const aditionalChangePath = useMemo(
|
|
28
|
-
() => [...props.path.slice(0, -1),
|
|
31
|
+
() => [...props.path.slice(0, -1), props.variantNameOverride],
|
|
29
32
|
[props.variantNameOverride, props.path],
|
|
30
33
|
)
|
|
31
|
-
|
|
32
34
|
const subValues = useFormValue(aditionalChangePath)
|
|
35
|
+
|
|
33
36
|
const {patch} = useDocumentOperation(id.replace('drafts.', ''), props.schemaType.name)
|
|
34
37
|
|
|
35
38
|
const handleChange = useCallback(
|
|
@@ -56,7 +59,14 @@ export const ExperimentInput = (props: StringInputProps & {variantNameOverride:
|
|
|
56
59
|
[patch, subValues, aditionalChangePath],
|
|
57
60
|
)
|
|
58
61
|
|
|
59
|
-
if (!experiments.length)
|
|
62
|
+
if (!experiments.length)
|
|
63
|
+
return (
|
|
64
|
+
<Card padding={[3, 3, 4]} radius={2} shadow={1} tone="caution">
|
|
65
|
+
<Text align="center" size={[2, 2, 3]}>
|
|
66
|
+
There are no defined {props.experimentNameOverride}s
|
|
67
|
+
</Text>
|
|
68
|
+
</Card>
|
|
69
|
+
)
|
|
60
70
|
|
|
61
71
|
return (
|
|
62
72
|
<Select {...props} listOptions={formatlistOptions(experiments)} handleChange={handleChange} />
|
package/src/fieldExperiments.tsx
CHANGED
|
@@ -44,7 +44,6 @@ const createExperimentType = ({
|
|
|
44
44
|
{...props}
|
|
45
45
|
experimentId={experimentId}
|
|
46
46
|
experimentNameOverride={experimentNameOverride}
|
|
47
|
-
variantNameOverride={variantNameOverride}
|
|
48
47
|
/>
|
|
49
48
|
),
|
|
50
49
|
},
|
|
@@ -70,7 +69,11 @@ const createExperimentType = ({
|
|
|
70
69
|
type: 'string',
|
|
71
70
|
components: {
|
|
72
71
|
input: (props) => (
|
|
73
|
-
<ExperimentInput
|
|
72
|
+
<ExperimentInput
|
|
73
|
+
{...props}
|
|
74
|
+
experimentNameOverride={experimentNameOverride}
|
|
75
|
+
variantNameOverride={variantNameOverride}
|
|
76
|
+
/>
|
|
74
77
|
),
|
|
75
78
|
},
|
|
76
79
|
hidden: ({parent}) => {
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {Dispatch, SetStateAction} from 'react'
|
|
2
1
|
import {
|
|
3
2
|
ArrayOfObjectsInputProps,
|
|
4
3
|
FieldDefinition,
|
|
@@ -21,9 +20,7 @@ export type ExperimentType = {
|
|
|
21
20
|
|
|
22
21
|
export type FieldPluginConfig = {
|
|
23
22
|
fields: (string | FieldDefinition)[]
|
|
24
|
-
experiments:
|
|
25
|
-
| ExperimentType[]
|
|
26
|
-
| ((client: SanityClient, secret?: string) => Promise<ExperimentType[]>)
|
|
23
|
+
experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
|
|
27
24
|
apiVersion?: string
|
|
28
25
|
experimentNameOverride?: string
|
|
29
26
|
variantNameOverride?: string
|
|
@@ -40,8 +37,6 @@ export type VariantPreviewProps = Omit<PreviewProps, 'SchemaType'> & {
|
|
|
40
37
|
|
|
41
38
|
export type ExperimentContextProps = Required<FieldPluginConfig> & {
|
|
42
39
|
experiments: ExperimentType[]
|
|
43
|
-
setSecret: Dispatch<SetStateAction<string | undefined>>
|
|
44
|
-
secret: string | undefined
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
export type ArrayInputProps = ArrayOfObjectsInputProps & {
|
|
@@ -72,179 +67,3 @@ export type ExperimentGeneric<T> = {
|
|
|
72
67
|
| T
|
|
73
68
|
| undefined
|
|
74
69
|
}
|
|
75
|
-
|
|
76
|
-
export type GrowthbookExperiment = {
|
|
77
|
-
id: string
|
|
78
|
-
dateCreated: string
|
|
79
|
-
dateUpdated: string
|
|
80
|
-
name: string
|
|
81
|
-
project: string
|
|
82
|
-
hypothesis: string
|
|
83
|
-
description: string
|
|
84
|
-
tags: [string]
|
|
85
|
-
owner: string
|
|
86
|
-
archived: boolean
|
|
87
|
-
status: string
|
|
88
|
-
autoRefresh: boolean
|
|
89
|
-
hashAttribute: string
|
|
90
|
-
fallbackAttribute: string
|
|
91
|
-
hashVersion: number
|
|
92
|
-
disableStickyBucketing: boolean
|
|
93
|
-
bucketVersion: number
|
|
94
|
-
minBucketVersion: number
|
|
95
|
-
variations: [
|
|
96
|
-
{
|
|
97
|
-
variationId: string
|
|
98
|
-
key: string
|
|
99
|
-
name: string
|
|
100
|
-
description: string
|
|
101
|
-
screenshots: [string]
|
|
102
|
-
},
|
|
103
|
-
]
|
|
104
|
-
phases: [
|
|
105
|
-
{
|
|
106
|
-
name: string
|
|
107
|
-
dateStarted: string
|
|
108
|
-
dateEnded: string
|
|
109
|
-
reasonForStopping: string
|
|
110
|
-
seed: string
|
|
111
|
-
coverage: 0
|
|
112
|
-
trafficSplit: [
|
|
113
|
-
{
|
|
114
|
-
variationId: string
|
|
115
|
-
weight: 0
|
|
116
|
-
},
|
|
117
|
-
]
|
|
118
|
-
namespace: {
|
|
119
|
-
namespaceId: string
|
|
120
|
-
range: []
|
|
121
|
-
}
|
|
122
|
-
targetingCondition: string
|
|
123
|
-
savedGroupTargeting: [
|
|
124
|
-
{
|
|
125
|
-
matchType: string
|
|
126
|
-
savedGroups: [string]
|
|
127
|
-
},
|
|
128
|
-
]
|
|
129
|
-
},
|
|
130
|
-
]
|
|
131
|
-
settings: {
|
|
132
|
-
datasourceId: string
|
|
133
|
-
assignmentQueryId: string
|
|
134
|
-
experimentId: string
|
|
135
|
-
segmentId: string
|
|
136
|
-
queryFilter: string
|
|
137
|
-
inProgressConversions: string
|
|
138
|
-
attributionModel: string
|
|
139
|
-
statsEngine: string
|
|
140
|
-
regressionAdjustmentEnabled: boolean
|
|
141
|
-
goals: [
|
|
142
|
-
{
|
|
143
|
-
metricId: string
|
|
144
|
-
overrides: {
|
|
145
|
-
delayHours: 0
|
|
146
|
-
windowHours: 0
|
|
147
|
-
window: string
|
|
148
|
-
winRiskThreshold: 0
|
|
149
|
-
loseRiskThreshold: 0
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
]
|
|
153
|
-
secondaryMetrics: [
|
|
154
|
-
{
|
|
155
|
-
metricId: string
|
|
156
|
-
overrides: {
|
|
157
|
-
delayHours: 0
|
|
158
|
-
windowHours: 0
|
|
159
|
-
window: string
|
|
160
|
-
winRiskThreshold: 0
|
|
161
|
-
loseRiskThreshold: 0
|
|
162
|
-
}
|
|
163
|
-
},
|
|
164
|
-
]
|
|
165
|
-
guardrails: [
|
|
166
|
-
{
|
|
167
|
-
metricId: string
|
|
168
|
-
overrides: {
|
|
169
|
-
delayHours: 0
|
|
170
|
-
windowHours: 0
|
|
171
|
-
window: string
|
|
172
|
-
winRiskThreshold: 0
|
|
173
|
-
loseRiskThreshold: 0
|
|
174
|
-
}
|
|
175
|
-
},
|
|
176
|
-
]
|
|
177
|
-
activationMetric: {
|
|
178
|
-
metricId: string
|
|
179
|
-
overrides: {
|
|
180
|
-
delayHours: 0
|
|
181
|
-
windowHours: 0
|
|
182
|
-
window: string
|
|
183
|
-
winRiskThreshold: 0
|
|
184
|
-
loseRiskThreshold: 0
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
resultSummary: {
|
|
189
|
-
status: string
|
|
190
|
-
winner: string
|
|
191
|
-
conclusions: string
|
|
192
|
-
releasedVariationId: string
|
|
193
|
-
excludeFromPayload: boolean
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
export type GrowthbookFeature = {
|
|
198
|
-
id: string
|
|
199
|
-
dateCreated: string
|
|
200
|
-
dateUpdated: string
|
|
201
|
-
archived: boolean
|
|
202
|
-
description: string
|
|
203
|
-
owner: string
|
|
204
|
-
project: string
|
|
205
|
-
valueType: string
|
|
206
|
-
defaultValue: string
|
|
207
|
-
tags: string[]
|
|
208
|
-
environments: {
|
|
209
|
-
[key: string]: {
|
|
210
|
-
enabled: boolean
|
|
211
|
-
defaultValue: string
|
|
212
|
-
rules: {
|
|
213
|
-
description: string
|
|
214
|
-
condition: string
|
|
215
|
-
savedGroupTargeting: {matchType: string; savedGroups: string[]}[]
|
|
216
|
-
id: string
|
|
217
|
-
enabled: boolean
|
|
218
|
-
type: string
|
|
219
|
-
value: string
|
|
220
|
-
variations: {value: string; variationId: string}[]
|
|
221
|
-
}[]
|
|
222
|
-
definition: string
|
|
223
|
-
draft: {
|
|
224
|
-
enabled: boolean
|
|
225
|
-
defaultValue: string
|
|
226
|
-
rules: {
|
|
227
|
-
description: string
|
|
228
|
-
condition: string
|
|
229
|
-
savedGroupTargeting: {matchType: string; savedGroups: string[]}[]
|
|
230
|
-
id: string
|
|
231
|
-
enabled: boolean
|
|
232
|
-
type: string
|
|
233
|
-
value: string
|
|
234
|
-
variations: {value: string; variationId: string}[]
|
|
235
|
-
}[]
|
|
236
|
-
definition: string
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
prerequisites: {
|
|
241
|
-
parentId: string
|
|
242
|
-
parentCondition: string
|
|
243
|
-
}[]
|
|
244
|
-
revision: {
|
|
245
|
-
version: number
|
|
246
|
-
comment: string
|
|
247
|
-
date: string
|
|
248
|
-
publishedBy: string
|
|
249
|
-
}
|
|
250
|
-
}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import {SettingsView, useSecrets} from '@sanity/studio-secrets'
|
|
2
|
-
import {useEffect, useState} from 'react'
|
|
3
|
-
import {ObjectInputProps} from 'sanity'
|
|
4
|
-
|
|
5
|
-
import {useExperimentContext} from './ExperimentContext'
|
|
6
|
-
|
|
7
|
-
const namespace = 'growthbook'
|
|
8
|
-
|
|
9
|
-
const pluginConfigKeys = [
|
|
10
|
-
{
|
|
11
|
-
key: 'apiKey',
|
|
12
|
-
title: 'Your secret API key',
|
|
13
|
-
},
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
export const Secrets = (props: ObjectInputProps) => {
|
|
17
|
-
const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}
|
|
18
|
-
const {setSecret} = useExperimentContext()
|
|
19
|
-
const [showSettings, setShowSettings] = useState<boolean>(false)
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (loading) return undefined
|
|
23
|
-
if (!secrets && !loading) {
|
|
24
|
-
setSecret(undefined)
|
|
25
|
-
return setShowSettings(true)
|
|
26
|
-
}
|
|
27
|
-
setSecret(secrets.apiKey)
|
|
28
|
-
return setShowSettings(false)
|
|
29
|
-
}, [secrets, loading, setSecret])
|
|
30
|
-
|
|
31
|
-
if (!showSettings) {
|
|
32
|
-
return props.renderDefault(props)
|
|
33
|
-
}
|
|
34
|
-
return (
|
|
35
|
-
<>
|
|
36
|
-
<SettingsView
|
|
37
|
-
title={'Growthbook secret'}
|
|
38
|
-
namespace={namespace}
|
|
39
|
-
keys={pluginConfigKeys}
|
|
40
|
-
onClose={() => {
|
|
41
|
-
setShowSettings(false)
|
|
42
|
-
}}
|
|
43
|
-
/>
|
|
44
|
-
{props.renderDefault(props)}
|
|
45
|
-
</>
|
|
46
|
-
)
|
|
47
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import {definePlugin, FieldDefinition, isObjectInputProps} from 'sanity'
|
|
2
|
-
|
|
3
|
-
import {Secrets} from './components/Secrets'
|
|
4
|
-
import {fieldLevelExperiments} from './fieldExperiments'
|
|
5
|
-
import {flattenSchemaType} from './utils/flattenSchemaType'
|
|
6
|
-
import {getExperiments} from './utils/growthbook'
|
|
7
|
-
|
|
8
|
-
export type GrowthbookABConfig = {
|
|
9
|
-
fields: (string | FieldDefinition)[]
|
|
10
|
-
environment: string
|
|
11
|
-
baseUrl?: string
|
|
12
|
-
project?: string
|
|
13
|
-
convertBooleans?: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const growthbookFieldLevel = definePlugin<GrowthbookABConfig>((config) => {
|
|
17
|
-
const {fields, environment, project, convertBooleans, baseUrl} = config
|
|
18
|
-
return {
|
|
19
|
-
name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',
|
|
20
|
-
plugins: [
|
|
21
|
-
fieldLevelExperiments({
|
|
22
|
-
fields,
|
|
23
|
-
experiments: (client) =>
|
|
24
|
-
getExperiments({client, environment, baseUrl, project, convertBooleans}),
|
|
25
|
-
}),
|
|
26
|
-
],
|
|
27
|
-
|
|
28
|
-
form: {
|
|
29
|
-
components: {
|
|
30
|
-
input: (props) => {
|
|
31
|
-
const isRootInput = props.id === 'root' && isObjectInputProps(props)
|
|
32
|
-
|
|
33
|
-
if (!isRootInput) {
|
|
34
|
-
return props.renderDefault(props)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
|
|
38
|
-
(field) => field.type.name,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))
|
|
42
|
-
|
|
43
|
-
if (!hasExperiment) {
|
|
44
|
-
return props.renderDefault(props)
|
|
45
|
-
}
|
|
46
|
-
return Secrets(props)
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
}
|
|
51
|
-
})
|
package/src/utils/growthbook.ts
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {SanityClient} from 'sanity'
|
|
2
|
-
|
|
3
|
-
import {GrowthbookABConfig} from '../growthbookFieldExperiments'
|
|
4
|
-
import {ExperimentType, GrowthbookFeature, VariantType} from '../types'
|
|
5
|
-
|
|
6
|
-
const getBooleanConversion = (value: string) => {
|
|
7
|
-
// this way or the other way around?
|
|
8
|
-
if (value === 'true') {
|
|
9
|
-
return 'variant'
|
|
10
|
-
} else if (value === 'false') {
|
|
11
|
-
return 'control'
|
|
12
|
-
}
|
|
13
|
-
return value
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const getExperiments = async ({
|
|
17
|
-
client,
|
|
18
|
-
environment,
|
|
19
|
-
baseUrl,
|
|
20
|
-
project,
|
|
21
|
-
convertBooleans,
|
|
22
|
-
}: Omit<GrowthbookABConfig, 'fields'> & {client: SanityClient}): Promise<ExperimentType[]> => {
|
|
23
|
-
const query = `*[_id == 'secrets.growthbook'][0].secrets.apiKey`
|
|
24
|
-
|
|
25
|
-
const secret = await client.fetch(query) // secret is stored in the content lake using @sanity/studio-secrets
|
|
26
|
-
if (!secret) return []
|
|
27
|
-
|
|
28
|
-
const featureExperiments: ExperimentType[] = []
|
|
29
|
-
let hasMore = true
|
|
30
|
-
let offset = 0
|
|
31
|
-
const url = new URL(baseUrl ?? 'https://api.growthbook.io/api/v1/features')
|
|
32
|
-
if (project) {
|
|
33
|
-
url.searchParams.set('projectId', project)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
while (hasMore) {
|
|
37
|
-
url.searchParams.set('offset', offset.toString())
|
|
38
|
-
const response = await fetch(url, {
|
|
39
|
-
headers: {
|
|
40
|
-
Authorization: `Bearer ${secret}`,
|
|
41
|
-
},
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const {features, hasMore: responseHasMore, nextOffset} = await response.json()
|
|
45
|
-
|
|
46
|
-
hasMore = responseHasMore
|
|
47
|
-
offset = nextOffset
|
|
48
|
-
if (!features) continue
|
|
49
|
-
|
|
50
|
-
features.forEach((feature: GrowthbookFeature) => {
|
|
51
|
-
if (feature.archived) {
|
|
52
|
-
return undefined
|
|
53
|
-
}
|
|
54
|
-
const experiments = feature.environments[environment]?.rules.filter(
|
|
55
|
-
(experiment) => experiment.type === 'experiment-ref' || experiment.type === 'experiment',
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
if (!experiments) {
|
|
59
|
-
return undefined
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const variations = new Set<VariantType>()
|
|
63
|
-
experiments.forEach((experiment) => {
|
|
64
|
-
experiment?.variations.forEach((variant) => {
|
|
65
|
-
variations.add({
|
|
66
|
-
id: convertBooleans ? getBooleanConversion(variant.value) : variant.value,
|
|
67
|
-
label: convertBooleans ? getBooleanConversion(variant.value) : variant.value,
|
|
68
|
-
})
|
|
69
|
-
})
|
|
70
|
-
})
|
|
71
|
-
const value = {id: feature.id, label: feature.id, variants: [...variations]}
|
|
72
|
-
|
|
73
|
-
featureExperiments.push(value)
|
|
74
|
-
return undefined
|
|
75
|
-
})
|
|
76
|
-
}
|
|
77
|
-
return featureExperiments
|
|
78
|
-
}
|