@sanity/personalization-plugin 2.5.0 → 3.0.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.
Files changed (48) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +570 -144
  3. package/dist/growthbook/index.d.ts +12 -15
  4. package/dist/growthbook/index.d.ts.map +1 -0
  5. package/dist/growthbook/index.js +104 -68
  6. package/dist/growthbook/index.js.map +1 -1
  7. package/dist/index.d.ts +212 -252
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +398 -301
  10. package/dist/index.js.map +1 -1
  11. package/dist/launchDarkly/index.d.ts +9 -12
  12. package/dist/launchDarkly/index.d.ts.map +1 -0
  13. package/dist/launchDarkly/index.js +74 -46
  14. package/dist/launchDarkly/index.js.map +1 -1
  15. package/package.json +35 -77
  16. package/dist/growthbook/index.d.mts +0 -15
  17. package/dist/growthbook/index.mjs +0 -124
  18. package/dist/growthbook/index.mjs.map +0 -1
  19. package/dist/index.d.mts +0 -267
  20. package/dist/index.mjs +0 -472
  21. package/dist/index.mjs.map +0 -1
  22. package/dist/launchDarkly/index.d.mts +0 -12
  23. package/dist/launchDarkly/index.mjs +0 -107
  24. package/dist/launchDarkly/index.mjs.map +0 -1
  25. package/sanity.json +0 -8
  26. package/src/components/Array.tsx +0 -68
  27. package/src/components/ExperimentContext.tsx +0 -65
  28. package/src/components/ExperimentField.tsx +0 -138
  29. package/src/components/ExperimentInput.tsx +0 -75
  30. package/src/components/ExperimentItem.tsx +0 -10
  31. package/src/components/Select.tsx +0 -43
  32. package/src/components/VariantInput.tsx +0 -19
  33. package/src/components/VariantPreview.tsx +0 -75
  34. package/src/fieldExperiments.tsx +0 -266
  35. package/src/growthbook/Components/GrowthbookContext.tsx +0 -38
  36. package/src/growthbook/Components/Secrets.tsx +0 -47
  37. package/src/growthbook/index.ts +0 -54
  38. package/src/growthbook/types.ts +0 -15
  39. package/src/growthbook/utils.ts +0 -94
  40. package/src/index.ts +0 -3
  41. package/src/launchDarkly/components/LaunchDarklyContext.tsx +0 -36
  42. package/src/launchDarkly/components/Secrets.tsx +0 -46
  43. package/src/launchDarkly/index.ts +0 -52
  44. package/src/launchDarkly/types.ts +0 -193
  45. package/src/launchDarkly/utils.ts +0 -54
  46. package/src/types.ts +0 -245
  47. package/src/utils/flattenSchemaType.ts +0 -47
  48. package/v2-incompatible.js +0 -11
@@ -1,266 +0,0 @@
1
- import {
2
- ArrayOfObjectsInputProps,
3
- defineField,
4
- definePlugin,
5
- defineType,
6
- FieldDefinition,
7
- isObjectInputProps,
8
- } from 'sanity'
9
-
10
- import {ArrayInput} from './components/Array'
11
- import {CONFIG_DEFAULT, ExperimentProvider} from './components/ExperimentContext'
12
- import {ExperimentField} from './components/ExperimentField'
13
- import {ExperimentInput} from './components/ExperimentInput'
14
- import {ExperimentItem} from './components/ExperimentItem'
15
- import {VariantInput} from './components/VariantInput'
16
- import {VariantPreview} from './components/VariantPreview'
17
- import {FieldPluginConfig} from './types'
18
- import {flattenSchemaType} from './utils/flattenSchemaType'
19
-
20
- const createExperimentType = ({
21
- field,
22
- experimentNameOverride,
23
- variantNameOverride,
24
- variantId,
25
- variantArrayName,
26
- experimentId,
27
- }: {
28
- field: string | FieldDefinition
29
- experimentNameOverride: string
30
- variantNameOverride: string
31
- variantId: string
32
- variantArrayName: string
33
- experimentId: string
34
- }) => {
35
- const typeName = typeof field === `string` ? field : field.name
36
- const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
37
- const variantName = `${variantNameOverride}${usedName}`
38
-
39
- return defineType({
40
- name: `${experimentNameOverride}${usedName}`,
41
- type: 'object',
42
- components: {
43
- field: (props) => (
44
- <ExperimentField
45
- {...props}
46
- experimentId={experimentId}
47
- experimentNameOverride={experimentNameOverride}
48
- variantNameOverride={variantNameOverride}
49
- />
50
- ),
51
- item: ExperimentItem,
52
- },
53
- fields: [
54
- typeof field === `string`
55
- ? // Define a simple field if all we have is the name as a string
56
- defineField({
57
- name: 'default',
58
- type: field,
59
- })
60
- : // Pass in the configured options, but overwrite the name
61
- {
62
- ...field,
63
- name: 'default',
64
- },
65
- defineField({
66
- name: 'active',
67
- type: 'boolean',
68
- hidden: true,
69
- initialValue: false,
70
- }),
71
- defineField({
72
- name: experimentId,
73
- type: 'string',
74
- components: {
75
- input: (props) => (
76
- <ExperimentInput
77
- {...props}
78
- experimentNameOverride={experimentNameOverride}
79
- variantNameOverride={variantNameOverride}
80
- />
81
- ),
82
- },
83
- hidden: ({parent}) => {
84
- return !parent?.active
85
- },
86
- }),
87
- defineField({
88
- name: variantArrayName,
89
- type: 'array',
90
- hidden: ({parent}) => {
91
- return !parent?.[experimentId]
92
- },
93
- components: {
94
- input: (props: ArrayOfObjectsInputProps) => (
95
- <ArrayInput
96
- {...props}
97
- variantName={variantName}
98
- variantId={variantId}
99
- experimentId={experimentId}
100
- />
101
- ),
102
- },
103
- of: [
104
- defineField({
105
- name: variantName,
106
- type: variantName,
107
- }),
108
- ],
109
- }),
110
- ],
111
- preview: {
112
- select: {
113
- base: 'default',
114
- experiment: experimentId,
115
- },
116
- prepare: ({base, experiment}) => {
117
- const title = base?.title || base?.name || typeof base === 'string' ? base : ''
118
- const experimentTitle = experiment ? `Experiment: ${experiment}` : ''
119
- const media = base?.image || base?.photo || base?.media || ''
120
- return {
121
- title: title || experimentTitle,
122
- subtitle: title ? experimentTitle : '',
123
- media,
124
- }
125
- },
126
- },
127
- })
128
- }
129
-
130
- const createVariantType = ({
131
- field,
132
- variantNameOverride,
133
- variantId,
134
- experimentId,
135
- }: {
136
- field: string | FieldDefinition
137
- variantNameOverride: string
138
- variantId: string
139
- experimentId: string
140
- }) => {
141
- const typeName = typeof field === `string` ? field : field.name
142
- const usedName = String(typeName[0]).toUpperCase() + String(typeName).slice(1)
143
- return defineType({
144
- name: `${variantNameOverride}${usedName}`,
145
- title: `${variantNameOverride} array ${usedName}`,
146
- type: 'object',
147
- components: {
148
- preview: VariantPreview,
149
- input: VariantInput,
150
- },
151
- fields: [
152
- {
153
- type: 'string',
154
- name: variantId,
155
- readOnly: true,
156
- },
157
- {
158
- type: 'string',
159
- name: experimentId,
160
- hidden: true,
161
- },
162
- typeof field === `string`
163
- ? // Define a simple field if all we have is the name as a string
164
- defineField({
165
- name: 'value',
166
- type: field,
167
- // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
168
- })
169
- : // Pass in the configured options, but overwrite the name
170
- {
171
- ...field,
172
- name: 'value',
173
- // hidden: ({parent}) => !parent?.[`${objectNameOverride}Id`],
174
- },
175
- ],
176
- preview: {
177
- select: {
178
- variant: variantId,
179
- experiment: experimentId,
180
- value: 'value',
181
- },
182
- },
183
- })
184
- }
185
-
186
- const fieldSchema = ({
187
- fields,
188
- experimentNameOverride,
189
- variantNameOverride,
190
- variantId,
191
- variantArrayName,
192
- experimentId,
193
- }: Required<Omit<FieldPluginConfig, 'apiVersion' | 'experiments'>>) => {
194
- return [
195
- ...fields.map((field) =>
196
- createVariantType({field, variantNameOverride, variantId, experimentId}),
197
- ),
198
- ...fields.map((field) =>
199
- createExperimentType({
200
- field,
201
- experimentNameOverride,
202
- variantNameOverride,
203
- variantId,
204
- variantArrayName,
205
- experimentId,
206
- }),
207
- ),
208
- ]
209
- }
210
-
211
- export const fieldLevelExperiments = definePlugin<FieldPluginConfig>((config) => {
212
- const pluginConfig = {...CONFIG_DEFAULT, ...config}
213
- const {fields, experimentNameOverride, variantNameOverride} = pluginConfig
214
-
215
- const experimentId = `${experimentNameOverride}Id`
216
- const variantArrayName = `${variantNameOverride}s`
217
- const variantId = `${variantNameOverride}Id`
218
-
219
- const fieldSchemaConfig = fieldSchema({
220
- fields,
221
- experimentNameOverride,
222
- variantNameOverride,
223
- variantId,
224
- variantArrayName,
225
- experimentId,
226
- })
227
- return {
228
- name: 'sanity-personalistaion-plugin-field-level-experiments',
229
- schema: {
230
- types: fieldSchemaConfig,
231
- },
232
- form: {
233
- components: {
234
- input: (props) => {
235
- const isRootInput = props.id === 'root' && isObjectInputProps(props)
236
-
237
- if (!isRootInput) {
238
- return props.renderDefault(props)
239
- }
240
-
241
- const flatFields = flattenSchemaType(props.schemaType)
242
- const hasExperiment = flatFields.some(
243
- (field) =>
244
- field.type.name.startsWith(experimentNameOverride) ||
245
- field.name.startsWith(experimentNameOverride),
246
- )
247
-
248
- if (!hasExperiment) {
249
- return props.renderDefault(props)
250
- }
251
-
252
- const providerProps = {
253
- ...props,
254
- experimentFieldPluginConfig: {
255
- ...pluginConfig,
256
- variantId,
257
- variantArrayName,
258
- experimentId,
259
- },
260
- }
261
- return ExperimentProvider(providerProps)
262
- },
263
- },
264
- },
265
- }
266
- })
@@ -1,38 +0,0 @@
1
- import {createContext, useContext, useMemo, useState} from 'react'
2
- import {ObjectInputProps} from 'sanity'
3
-
4
- import {GrowthbookContextProps, GrowthbookExperimentFieldPluginConfig} from '../types'
5
- import {Secrets} from './Secrets'
6
-
7
- export const GROWTHBOOK_CONFIG_DEFAULT = {
8
- baseUrl: 'https://api.growthbook.io/api/v1',
9
- }
10
-
11
- export const GrowthbookContext = createContext<GrowthbookContextProps>({
12
- setSecret: () => undefined,
13
- secret: undefined,
14
- })
15
-
16
- export function useGrowthbookContext() {
17
- return useContext(GrowthbookContext)
18
- }
19
-
20
- type GrowthbookProps = ObjectInputProps & {
21
- growthbookFieldPluginConfig: GrowthbookExperimentFieldPluginConfig
22
- }
23
-
24
- export function GrowthbookProvider(props: GrowthbookProps) {
25
- const {growthbookFieldPluginConfig} = props
26
- const [secret, setSecret] = useState<string | undefined>()
27
-
28
- const context = useMemo(
29
- () => ({...growthbookFieldPluginConfig, secret, setSecret}),
30
- [growthbookFieldPluginConfig, secret, setSecret],
31
- )
32
-
33
- return (
34
- <GrowthbookContext.Provider value={context}>
35
- <Secrets {...props} />
36
- </GrowthbookContext.Provider>
37
- )
38
- }
@@ -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 {useGrowthbookContext} from './GrowthbookContext'
6
-
7
- export const namespace = 'growthbook'
8
-
9
- export 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} = useGrowthbookContext()
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,54 +0,0 @@
1
- import {definePlugin, isObjectInputProps} from 'sanity'
2
-
3
- import {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'
4
- import {flattenSchemaType} from '../utils/flattenSchemaType'
5
- import {GROWTHBOOK_CONFIG_DEFAULT, GrowthbookProvider} from './Components/GrowthbookContext'
6
- import {GrowthbookExperimentFieldPluginConfig} from './types'
7
- import {getExperiments} from './utils'
8
-
9
- export const fieldLevelExperiments = definePlugin<GrowthbookExperimentFieldPluginConfig>(
10
- (config) => {
11
- const pluginConfig = {...GROWTHBOOK_CONFIG_DEFAULT, ...config}
12
- const {fields, environment, project, convertBooleans, baseUrl, tags} = pluginConfig
13
- return {
14
- name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',
15
- plugins: [
16
- baseFieldLevelExperiments({
17
- fields,
18
- experiments: (client) =>
19
- getExperiments({client, environment, baseUrl, project, convertBooleans, tags}),
20
- }),
21
- ],
22
-
23
- form: {
24
- components: {
25
- input: (props) => {
26
- const isRootInput = props.id === 'root' && isObjectInputProps(props)
27
-
28
- if (!isRootInput) {
29
- return props.renderDefault(props)
30
- }
31
-
32
- const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
33
- (field) => field.type.name,
34
- )
35
-
36
- const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))
37
-
38
- if (!hasExperiment) {
39
- return props.renderDefault(props)
40
- }
41
-
42
- const providerProps = {
43
- ...props,
44
- growthbookFieldPluginConfig: {
45
- ...pluginConfig,
46
- },
47
- }
48
- return GrowthbookProvider(providerProps)
49
- },
50
- },
51
- },
52
- }
53
- },
54
- )
@@ -1,15 +0,0 @@
1
- import {FieldDefinition} from 'sanity'
2
-
3
- export type GrowthbookExperimentFieldPluginConfig = {
4
- fields: (string | FieldDefinition)[]
5
- environment: string
6
- baseUrl?: string
7
- project?: string
8
- convertBooleans?: boolean
9
- tags?: string[]
10
- }
11
-
12
- export type GrowthbookContextProps = {
13
- setSecret: (secret: string | undefined) => void
14
- secret: string | undefined
15
- }
@@ -1,94 +0,0 @@
1
- import {SanityClient} from 'sanity'
2
-
3
- import {ExperimentType, GrowthbookFeature, VariantType} from '../types'
4
- import {namespace, pluginConfigKeys} from './Components/Secrets'
5
- import {GrowthbookExperimentFieldPluginConfig} from './types'
6
-
7
- const getBooleanConversion = (value: string) => {
8
- // control is false
9
- if (value === 'true') {
10
- return 'variant'
11
- } else if (value === 'false') {
12
- return 'control'
13
- }
14
- return value
15
- }
16
-
17
- export const getExperiments = async ({
18
- client,
19
- environment,
20
- baseUrl,
21
- project,
22
- convertBooleans,
23
- tags,
24
- }: Omit<GrowthbookExperimentFieldPluginConfig, 'fields' | 'baseUrl'> & {
25
- client: SanityClient
26
- baseUrl: string
27
- }): Promise<ExperimentType[]> => {
28
- const query = `*[_id == 'secrets.${namespace}'][0].secrets.${pluginConfigKeys[0].key}`
29
-
30
- const secret = await client.fetch(query) // secret is stored in the content lake using @sanity/studio-secrets
31
- if (!secret) return []
32
-
33
- const featureExperiments: ExperimentType[] = []
34
- let hasMore = true
35
- let offset = 0
36
- const url = new URL(`${baseUrl}/features`)
37
- if (project) {
38
- url.searchParams.set('projectId', project)
39
- }
40
-
41
- while (hasMore) {
42
- url.searchParams.set('offset', offset.toString())
43
- const response = await fetch(url, {
44
- headers: {
45
- Authorization: `Bearer ${secret}`,
46
- },
47
- })
48
-
49
- const {features, hasMore: responseHasMore, nextOffset} = await response.json()
50
-
51
- hasMore = responseHasMore
52
- offset = nextOffset
53
- if (!features) continue
54
-
55
- features.forEach((feature: GrowthbookFeature) => {
56
- if (feature.archived) {
57
- return undefined
58
- }
59
- if (tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag))) {
60
- return undefined
61
- }
62
-
63
- const experiments = feature.environments[environment]?.rules.filter(
64
- (experiment) => experiment.type === 'experiment-ref' || experiment.type === 'experiment',
65
- )
66
-
67
- if (!experiments) {
68
- return undefined
69
- }
70
-
71
- const variations: VariantType[] = []
72
- const uniqueValues = new Set<string>()
73
-
74
- experiments.forEach((experiment) => {
75
- experiment?.variations.forEach((variant) => {
76
- const value = convertBooleans ? getBooleanConversion(variant.value) : variant.value
77
- if (!uniqueValues.has(value)) {
78
- uniqueValues.add(value)
79
- variations.push({
80
- id: value,
81
- label: value,
82
- })
83
- }
84
- })
85
- })
86
- const value = {id: feature.id, label: feature.id, variants: variations}
87
-
88
- featureExperiments.push(value)
89
- return undefined
90
- })
91
- }
92
- const sortedFeatureExperiments = featureExperiments.sort((a, b) => a.id.localeCompare(b.id))
93
- return sortedFeatureExperiments
94
- }
package/src/index.ts DELETED
@@ -1,3 +0,0 @@
1
- export * from './fieldExperiments'
2
- export * from './types'
3
- export * from './utils/flattenSchemaType'
@@ -1,36 +0,0 @@
1
- import {createContext, useContext, useMemo, useState} from 'react'
2
- import {ObjectInputProps} from 'sanity'
3
-
4
- import {LaunchDarklyContextProps, LaunchDarklyFieldLevelConfig} from '../types'
5
- import {Secrets} from './Secrets'
6
-
7
- export const LAUNCHDARKLY_CONFIG_DEFAULT = {}
8
-
9
- export const LaunchDarklyContext = createContext<LaunchDarklyContextProps>({
10
- setSecret: () => undefined,
11
- secret: undefined,
12
- })
13
-
14
- export function useLaunchDarklyContext() {
15
- return useContext(LaunchDarklyContext)
16
- }
17
-
18
- type LaunchDarklyProps = ObjectInputProps & {
19
- launchDarklyFieldPluginConfig: LaunchDarklyFieldLevelConfig
20
- }
21
-
22
- export function LaunchDarklyProvider(props: LaunchDarklyProps) {
23
- const {launchDarklyFieldPluginConfig} = props
24
- const [secret, setSecret] = useState<string | undefined>()
25
-
26
- const context = useMemo(
27
- () => ({...launchDarklyFieldPluginConfig, secret, setSecret}),
28
- [launchDarklyFieldPluginConfig, secret, setSecret],
29
- )
30
-
31
- return (
32
- <LaunchDarklyContext.Provider value={context}>
33
- <Secrets {...props} />
34
- </LaunchDarklyContext.Provider>
35
- )
36
- }
@@ -1,46 +0,0 @@
1
- import {SettingsView, useSecrets} from '@sanity/studio-secrets'
2
- import {useEffect, useState} from 'react'
3
- import {ObjectInputProps} from 'sanity'
4
-
5
- import {useLaunchDarklyContext} from './LaunchDarklyContext'
6
-
7
- const namespace = 'launchdarkly'
8
- const pluginConfigKeys = [
9
- {
10
- key: 'apiKey',
11
- title: 'Your secret API key',
12
- },
13
- ]
14
-
15
- export const Secrets = (props: ObjectInputProps) => {
16
- const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}
17
- const {setSecret} = useLaunchDarklyContext()
18
- const [showSettings, setShowSettings] = useState<boolean>(false)
19
-
20
- useEffect(() => {
21
- if (loading) return undefined
22
- if (!secrets && !loading) {
23
- setSecret(undefined)
24
- return setShowSettings(true)
25
- }
26
- setSecret(secrets.apiKey)
27
- return setShowSettings(false)
28
- }, [secrets, loading, setSecret])
29
-
30
- if (!showSettings) {
31
- return props.renderDefault(props)
32
- }
33
- return (
34
- <>
35
- <SettingsView
36
- title={`${namespace} api key`}
37
- namespace={namespace}
38
- keys={pluginConfigKeys}
39
- onClose={() => {
40
- setShowSettings(false)
41
- }}
42
- />
43
- {props.renderDefault(props)}
44
- </>
45
- )
46
- }
@@ -1,52 +0,0 @@
1
- import {definePlugin, isObjectInputProps} from 'sanity'
2
-
3
- import {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'
4
- import {flattenSchemaType} from '../utils/flattenSchemaType'
5
- import {LAUNCHDARKLY_CONFIG_DEFAULT, LaunchDarklyProvider} from './components/LaunchDarklyContext'
6
- import {LaunchDarklyFieldLevelConfig} from './types'
7
- import {getExperiments} from './utils'
8
-
9
- export const fieldLevelExperiments = definePlugin<LaunchDarklyFieldLevelConfig>((config) => {
10
- const pluginConfig = {...LAUNCHDARKLY_CONFIG_DEFAULT, ...config}
11
- const {fields, projectKey, tags} = pluginConfig
12
- return {
13
- name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',
14
- plugins: [
15
- baseFieldLevelExperiments({
16
- fields,
17
- experiments: (client) => getExperiments({client, projectKey, tags}),
18
- experimentNameOverride: 'flag',
19
- }),
20
- ],
21
-
22
- form: {
23
- components: {
24
- input: (props) => {
25
- const isRootInput = props.id === 'root' && isObjectInputProps(props)
26
-
27
- if (!isRootInput) {
28
- return props.renderDefault(props)
29
- }
30
-
31
- const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(
32
- (field) => field.type.name,
33
- )
34
-
35
- const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('flag'))
36
-
37
- if (!hasExperiment) {
38
- return props.renderDefault(props)
39
- }
40
-
41
- const providerProps = {
42
- ...props,
43
- launchDarklyFieldPluginConfig: {
44
- ...pluginConfig,
45
- },
46
- }
47
- return LaunchDarklyProvider(providerProps)
48
- },
49
- },
50
- },
51
- }
52
- })