@sanity/personalization-plugin 2.5.0 → 3.0.1
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/LICENSE +1 -1
- package/README.md +570 -144
- package/dist/growthbook/index.d.ts +12 -15
- package/dist/growthbook/index.d.ts.map +1 -0
- package/dist/growthbook/index.js +104 -68
- package/dist/growthbook/index.js.map +1 -1
- package/dist/index.d.ts +212 -252
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +398 -301
- package/dist/index.js.map +1 -1
- package/dist/launchDarkly/index.d.ts +9 -12
- package/dist/launchDarkly/index.d.ts.map +1 -0
- package/dist/launchDarkly/index.js +74 -46
- package/dist/launchDarkly/index.js.map +1 -1
- package/package.json +37 -79
- package/dist/growthbook/index.d.mts +0 -15
- package/dist/growthbook/index.mjs +0 -124
- package/dist/growthbook/index.mjs.map +0 -1
- package/dist/index.d.mts +0 -267
- package/dist/index.mjs +0 -472
- package/dist/index.mjs.map +0 -1
- package/dist/launchDarkly/index.d.mts +0 -12
- package/dist/launchDarkly/index.mjs +0 -107
- package/dist/launchDarkly/index.mjs.map +0 -1
- package/sanity.json +0 -8
- package/src/components/Array.tsx +0 -68
- package/src/components/ExperimentContext.tsx +0 -65
- package/src/components/ExperimentField.tsx +0 -138
- package/src/components/ExperimentInput.tsx +0 -75
- package/src/components/ExperimentItem.tsx +0 -10
- package/src/components/Select.tsx +0 -43
- package/src/components/VariantInput.tsx +0 -19
- package/src/components/VariantPreview.tsx +0 -75
- package/src/fieldExperiments.tsx +0 -266
- package/src/growthbook/Components/GrowthbookContext.tsx +0 -38
- package/src/growthbook/Components/Secrets.tsx +0 -47
- package/src/growthbook/index.ts +0 -54
- package/src/growthbook/types.ts +0 -15
- package/src/growthbook/utils.ts +0 -94
- package/src/index.ts +0 -3
- package/src/launchDarkly/components/LaunchDarklyContext.tsx +0 -36
- package/src/launchDarkly/components/Secrets.tsx +0 -46
- package/src/launchDarkly/index.ts +0 -52
- package/src/launchDarkly/types.ts +0 -193
- package/src/launchDarkly/utils.ts +0 -54
- package/src/types.ts +0 -245
- package/src/utils/flattenSchemaType.ts +0 -47
- package/v2-incompatible.js +0 -11
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {FieldDefinition} from
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export {}
|
|
1
|
+
import { FieldDefinition } from "sanity";
|
|
2
|
+
type GrowthbookExperimentFieldPluginConfig = {
|
|
3
|
+
fields: (string | FieldDefinition)[];
|
|
4
|
+
environment: string;
|
|
5
|
+
baseUrl?: string;
|
|
6
|
+
project?: string;
|
|
7
|
+
convertBooleans?: boolean;
|
|
8
|
+
tags?: string[];
|
|
9
|
+
};
|
|
10
|
+
declare const fieldLevelExperiments: import("sanity").Plugin<GrowthbookExperimentFieldPluginConfig>;
|
|
11
|
+
export { fieldLevelExperiments };
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/growthbook/types.ts","../../src/growthbook/index.ts"],"mappings":";KAEY,qCAAA;EACV,MAAA,YAAkB,eAAe;EACjC,WAAA;EACA,OAAA;EACA,OAAA;EACA,eAAA;EACA,IAAA;AAAA;AAAA,cCAW,qBAAA,mBAAqB,MAAA,CAAA,qCAAA"}
|
package/dist/growthbook/index.js
CHANGED
|
@@ -1,46 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { definePlugin, isObjectInputProps } from "sanity";
|
|
2
|
+
import { flattenSchemaType, fieldLevelExperiments as fieldLevelExperiments$1 } from "../index.js";
|
|
3
|
+
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
|
|
4
|
+
import { c } from "react/compiler-runtime";
|
|
5
|
+
import { useState, useEffect, createContext, useContext } from "react";
|
|
6
|
+
import { useSecrets, SettingsView } from "@sanity/studio-secrets";
|
|
7
|
+
const namespace = "growthbook", apiKeyName = "apiKey", pluginConfigKeys = [{
|
|
8
|
+
key: apiKeyName,
|
|
9
|
+
title: "Your secret API key"
|
|
10
|
+
}], Secrets = (props) => {
|
|
11
|
+
const $ = c(12), {
|
|
12
|
+
secrets,
|
|
13
|
+
loading
|
|
14
|
+
} = useSecrets(namespace), {
|
|
15
|
+
setSecret
|
|
16
|
+
} = useGrowthbookContext(), [showSettings, setShowSettings] = useState(!1);
|
|
17
|
+
let t0, t1;
|
|
18
|
+
if ($[0] !== loading || $[1] !== secrets || $[2] !== setSecret ? (t0 = () => {
|
|
12
19
|
if (!loading)
|
|
13
20
|
return !secrets && !loading ? (setSecret(void 0), setShowSettings(!0)) : (setSecret(secrets.apiKey), setShowSettings(!1));
|
|
14
|
-
}, [secrets, loading, setSecret]
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
}, t1 = [secrets, loading, setSecret], $[0] = loading, $[1] = secrets, $[2] = setSecret, $[3] = t0, $[4] = t1) : (t0 = $[3], t1 = $[4]), useEffect(t0, t1), !showSettings) {
|
|
22
|
+
let t22;
|
|
23
|
+
return $[5] !== props ? (t22 = props.renderDefault(props), $[5] = props, $[6] = t22) : t22 = $[6], t22;
|
|
24
|
+
}
|
|
25
|
+
let t2;
|
|
26
|
+
$[7] === /* @__PURE__ */ Symbol.for("react.memo_cache_sentinel") ? (t2 = /* @__PURE__ */ jsx(SettingsView, { title: "Growthbook secret", namespace, keys: pluginConfigKeys, onClose: () => {
|
|
27
|
+
setShowSettings(!1);
|
|
28
|
+
} }), $[7] = t2) : t2 = $[7];
|
|
29
|
+
let t3;
|
|
30
|
+
$[8] !== props ? (t3 = props.renderDefault(props), $[8] = props, $[9] = t3) : t3 = $[9];
|
|
31
|
+
let t4;
|
|
32
|
+
return $[10] !== t3 ? (t4 = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
33
|
+
t2,
|
|
34
|
+
t3
|
|
35
|
+
] }), $[10] = t3, $[11] = t4) : t4 = $[11], t4;
|
|
28
36
|
}, GROWTHBOOK_CONFIG_DEFAULT = {
|
|
29
37
|
baseUrl: "https://api.growthbook.io/api/v1"
|
|
30
|
-
}, GrowthbookContext =
|
|
38
|
+
}, GrowthbookContext = createContext({
|
|
31
39
|
setSecret: () => {
|
|
32
40
|
},
|
|
33
41
|
secret: void 0
|
|
34
42
|
});
|
|
35
43
|
function useGrowthbookContext() {
|
|
36
|
-
return
|
|
44
|
+
return useContext(GrowthbookContext);
|
|
37
45
|
}
|
|
38
46
|
function GrowthbookProvider(props) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
const $ = c(8), {
|
|
48
|
+
growthbookFieldPluginConfig
|
|
49
|
+
} = props, [secret, setSecret] = useState();
|
|
50
|
+
let t0;
|
|
51
|
+
$[0] !== growthbookFieldPluginConfig || $[1] !== secret ? (t0 = {
|
|
52
|
+
...growthbookFieldPluginConfig,
|
|
53
|
+
secret,
|
|
54
|
+
setSecret
|
|
55
|
+
}, $[0] = growthbookFieldPluginConfig, $[1] = secret, $[2] = t0) : t0 = $[2];
|
|
56
|
+
const context = t0;
|
|
57
|
+
let t1;
|
|
58
|
+
$[3] !== props ? (t1 = /* @__PURE__ */ jsx(Secrets, { ...props }), $[3] = props, $[4] = t1) : t1 = $[4];
|
|
59
|
+
let t2;
|
|
60
|
+
return $[5] !== context || $[6] !== t1 ? (t2 = /* @__PURE__ */ jsx(GrowthbookContext.Provider, { value: context, children: t1 }), $[5] = context, $[6] = t1, $[7] = t2) : t2 = $[7], t2;
|
|
44
61
|
}
|
|
45
62
|
const getBooleanConversion = (value) => value === "true" ? "variant" : value === "false" ? "control" : value, getExperiments = async ({
|
|
46
63
|
client,
|
|
@@ -50,7 +67,7 @@ const getBooleanConversion = (value) => value === "true" ? "variant" : value ===
|
|
|
50
67
|
convertBooleans,
|
|
51
68
|
tags
|
|
52
69
|
}) => {
|
|
53
|
-
const query = `*[_id == 'secrets.${namespace}'][0].secrets.${
|
|
70
|
+
const query = `*[_id == 'secrets.${namespace}'][0].secrets.${apiKeyName}`, secret = await client.fetch(query);
|
|
54
71
|
if (!secret) return [];
|
|
55
72
|
const featureExperiments = [];
|
|
56
73
|
let hasMore = !0, offset = 0;
|
|
@@ -61,13 +78,15 @@ const getBooleanConversion = (value) => value === "true" ? "variant" : value ===
|
|
|
61
78
|
headers: {
|
|
62
79
|
Authorization: `Bearer ${secret}`
|
|
63
80
|
}
|
|
64
|
-
}), {
|
|
81
|
+
}), {
|
|
82
|
+
features,
|
|
83
|
+
hasMore: responseHasMore,
|
|
84
|
+
nextOffset
|
|
85
|
+
} = await response.json();
|
|
65
86
|
hasMore = responseHasMore, offset = nextOffset, features && features.forEach((feature) => {
|
|
66
87
|
if (feature.archived || tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag)))
|
|
67
88
|
return;
|
|
68
|
-
const experiments = feature.environments[environment]?.rules.filter(
|
|
69
|
-
(experiment) => experiment.type === "experiment-ref" || experiment.type === "experiment"
|
|
70
|
-
);
|
|
89
|
+
const experiments = feature.environments[environment]?.rules.filter((experiment) => experiment.type === "experiment-ref" || experiment.type === "experiment");
|
|
71
90
|
if (!experiments)
|
|
72
91
|
return;
|
|
73
92
|
const variations = [], uniqueValues = /* @__PURE__ */ new Set();
|
|
@@ -80,41 +99,58 @@ const getBooleanConversion = (value) => value === "true" ? "variant" : value ===
|
|
|
80
99
|
}));
|
|
81
100
|
});
|
|
82
101
|
});
|
|
83
|
-
const value = {
|
|
102
|
+
const value = {
|
|
103
|
+
id: feature.id,
|
|
104
|
+
label: feature.id,
|
|
105
|
+
variants: variations
|
|
106
|
+
};
|
|
84
107
|
featureExperiments.push(value);
|
|
85
108
|
});
|
|
86
109
|
}
|
|
87
110
|
return featureExperiments.sort((a, b) => a.id.localeCompare(b.id));
|
|
88
|
-
}, fieldLevelExperiments =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
}, fieldLevelExperiments = definePlugin((config) => {
|
|
112
|
+
const pluginConfig = {
|
|
113
|
+
...GROWTHBOOK_CONFIG_DEFAULT,
|
|
114
|
+
...config
|
|
115
|
+
}, {
|
|
116
|
+
fields,
|
|
117
|
+
environment,
|
|
118
|
+
project,
|
|
119
|
+
convertBooleans,
|
|
120
|
+
baseUrl,
|
|
121
|
+
tags
|
|
122
|
+
} = pluginConfig;
|
|
123
|
+
return {
|
|
124
|
+
name: "sanity-growthbook-personalistaion-plugin-field-level-experiments",
|
|
125
|
+
plugins: [fieldLevelExperiments$1({
|
|
126
|
+
fields,
|
|
127
|
+
experiments: (client) => getExperiments({
|
|
128
|
+
client,
|
|
129
|
+
environment,
|
|
130
|
+
baseUrl,
|
|
131
|
+
project,
|
|
132
|
+
convertBooleans,
|
|
133
|
+
tags
|
|
134
|
+
})
|
|
135
|
+
})],
|
|
136
|
+
form: {
|
|
137
|
+
components: {
|
|
138
|
+
input: (props) => {
|
|
139
|
+
if (!(props.id === "root" && isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).map((field) => field.type.name).some((name) => name.startsWith("experiment")))
|
|
140
|
+
return props.renderDefault(props);
|
|
141
|
+
const providerProps = {
|
|
142
|
+
...props,
|
|
143
|
+
growthbookFieldPluginConfig: {
|
|
144
|
+
...pluginConfig
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
return GrowthbookProvider(providerProps);
|
|
114
148
|
}
|
|
115
149
|
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
);
|
|
119
|
-
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
export {
|
|
154
|
+
fieldLevelExperiments
|
|
155
|
+
};
|
|
120
156
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/growthbook/Components/Secrets.tsx","../../src/growthbook/Components/GrowthbookContext.tsx","../../src/growthbook/utils.ts","../../src/growthbook/index.ts"],"sourcesContent":["import {SettingsView, useSecrets} from '@sanity/studio-secrets'\nimport {useEffect, useState} from 'react'\nimport {ObjectInputProps} from 'sanity'\n\nimport {useGrowthbookContext} from './GrowthbookContext'\n\nexport const namespace = 'growthbook'\n\nexport const pluginConfigKeys = [\n {\n key: 'apiKey',\n title: 'Your secret API key',\n },\n]\n\nexport const Secrets = (props: ObjectInputProps) => {\n const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}\n const {setSecret} = useGrowthbookContext()\n const [showSettings, setShowSettings] = useState<boolean>(false)\n\n useEffect(() => {\n if (loading) return undefined\n if (!secrets && !loading) {\n setSecret(undefined)\n return setShowSettings(true)\n }\n setSecret(secrets.apiKey)\n return setShowSettings(false)\n }, [secrets, loading, setSecret])\n\n if (!showSettings) {\n return props.renderDefault(props)\n }\n return (\n <>\n <SettingsView\n title={'Growthbook secret'}\n namespace={namespace}\n keys={pluginConfigKeys}\n onClose={() => {\n setShowSettings(false)\n }}\n />\n {props.renderDefault(props)}\n </>\n )\n}\n","import {createContext, useContext, useMemo, useState} from 'react'\nimport {ObjectInputProps} from 'sanity'\n\nimport {GrowthbookContextProps, GrowthbookExperimentFieldPluginConfig} from '../types'\nimport {Secrets} from './Secrets'\n\nexport const GROWTHBOOK_CONFIG_DEFAULT = {\n baseUrl: 'https://api.growthbook.io/api/v1',\n}\n\nexport const GrowthbookContext = createContext<GrowthbookContextProps>({\n setSecret: () => undefined,\n secret: undefined,\n})\n\nexport function useGrowthbookContext() {\n return useContext(GrowthbookContext)\n}\n\ntype GrowthbookProps = ObjectInputProps & {\n growthbookFieldPluginConfig: GrowthbookExperimentFieldPluginConfig\n}\n\nexport function GrowthbookProvider(props: GrowthbookProps) {\n const {growthbookFieldPluginConfig} = props\n const [secret, setSecret] = useState<string | undefined>()\n\n const context = useMemo(\n () => ({...growthbookFieldPluginConfig, secret, setSecret}),\n [growthbookFieldPluginConfig, secret, setSecret],\n )\n\n return (\n <GrowthbookContext.Provider value={context}>\n <Secrets {...props} />\n </GrowthbookContext.Provider>\n )\n}\n","import {SanityClient} from 'sanity'\n\nimport {ExperimentType, GrowthbookFeature, VariantType} from '../types'\nimport {namespace, pluginConfigKeys} from './Components/Secrets'\nimport {GrowthbookExperimentFieldPluginConfig} from './types'\n\nconst getBooleanConversion = (value: string) => {\n // control is false\n if (value === 'true') {\n return 'variant'\n } else if (value === 'false') {\n return 'control'\n }\n return value\n}\n\nexport const getExperiments = async ({\n client,\n environment,\n baseUrl,\n project,\n convertBooleans,\n tags,\n}: Omit<GrowthbookExperimentFieldPluginConfig, 'fields' | 'baseUrl'> & {\n client: SanityClient\n baseUrl: string\n}): Promise<ExperimentType[]> => {\n const query = `*[_id == 'secrets.${namespace}'][0].secrets.${pluginConfigKeys[0].key}`\n\n const secret = await client.fetch(query) // secret is stored in the content lake using @sanity/studio-secrets\n if (!secret) return []\n\n const featureExperiments: ExperimentType[] = []\n let hasMore = true\n let offset = 0\n const url = new URL(`${baseUrl}/features`)\n if (project) {\n url.searchParams.set('projectId', project)\n }\n\n while (hasMore) {\n url.searchParams.set('offset', offset.toString())\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${secret}`,\n },\n })\n\n const {features, hasMore: responseHasMore, nextOffset} = await response.json()\n\n hasMore = responseHasMore\n offset = nextOffset\n if (!features) continue\n\n features.forEach((feature: GrowthbookFeature) => {\n if (feature.archived) {\n return undefined\n }\n if (tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag))) {\n return undefined\n }\n\n const experiments = feature.environments[environment]?.rules.filter(\n (experiment) => experiment.type === 'experiment-ref' || experiment.type === 'experiment',\n )\n\n if (!experiments) {\n return undefined\n }\n\n const variations: VariantType[] = []\n const uniqueValues = new Set<string>()\n\n experiments.forEach((experiment) => {\n experiment?.variations.forEach((variant) => {\n const value = convertBooleans ? getBooleanConversion(variant.value) : variant.value\n if (!uniqueValues.has(value)) {\n uniqueValues.add(value)\n variations.push({\n id: value,\n label: value,\n })\n }\n })\n })\n const value = {id: feature.id, label: feature.id, variants: variations}\n\n featureExperiments.push(value)\n return undefined\n })\n }\n const sortedFeatureExperiments = featureExperiments.sort((a, b) => a.id.localeCompare(b.id))\n return sortedFeatureExperiments\n}\n","import {definePlugin, isObjectInputProps} from 'sanity'\n\nimport {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'\nimport {flattenSchemaType} from '../utils/flattenSchemaType'\nimport {GROWTHBOOK_CONFIG_DEFAULT, GrowthbookProvider} from './Components/GrowthbookContext'\nimport {GrowthbookExperimentFieldPluginConfig} from './types'\nimport {getExperiments} from './utils'\n\nexport const fieldLevelExperiments = definePlugin<GrowthbookExperimentFieldPluginConfig>(\n (config) => {\n const pluginConfig = {...GROWTHBOOK_CONFIG_DEFAULT, ...config}\n const {fields, environment, project, convertBooleans, baseUrl, tags} = pluginConfig\n return {\n name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',\n plugins: [\n baseFieldLevelExperiments({\n fields,\n experiments: (client) =>\n getExperiments({client, environment, baseUrl, project, convertBooleans, tags}),\n }),\n ],\n\n form: {\n components: {\n input: (props) => {\n const isRootInput = props.id === 'root' && isObjectInputProps(props)\n\n if (!isRootInput) {\n return props.renderDefault(props)\n }\n\n const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(\n (field) => field.type.name,\n )\n\n const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))\n\n if (!hasExperiment) {\n return props.renderDefault(props)\n }\n\n const providerProps = {\n ...props,\n growthbookFieldPluginConfig: {\n ...pluginConfig,\n },\n }\n return GrowthbookProvider(providerProps)\n },\n },\n },\n }\n },\n)\n"],"names":["useSecrets","useState","useEffect","jsxs","Fragment","jsx","SettingsView","createContext","useContext","useMemo","value","definePlugin","baseFieldLevelExperiments","isObjectInputProps","flattenSchemaType"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/growthbook/Components/Secrets.tsx","../../src/growthbook/Components/GrowthbookContext.tsx","../../src/growthbook/utils.ts","../../src/growthbook/index.ts"],"sourcesContent":["import {SettingsView, useSecrets} from '@sanity/studio-secrets'\nimport {useEffect, useState} from 'react'\nimport type {ObjectInputProps} from 'sanity'\n\nimport {useGrowthbookContext} from './GrowthbookContext'\n\nexport const namespace = 'growthbook'\nexport const apiKeyName = 'apiKey'\n\nconst pluginConfigKeys = [\n {\n key: apiKeyName,\n title: 'Your secret API key',\n },\n]\n\nexport const Secrets = (props: ObjectInputProps) => {\n const {secrets, loading} = useSecrets(namespace) as {secrets: {apiKey: string}; loading: boolean}\n const {setSecret} = useGrowthbookContext()\n const [showSettings, setShowSettings] = useState<boolean>(false)\n\n useEffect(() => {\n if (loading) return undefined\n if (!secrets && !loading) {\n setSecret(undefined)\n // oxlint-disable-next-line react/react-compiler\n return setShowSettings(true)\n }\n setSecret(secrets.apiKey)\n return setShowSettings(false)\n }, [secrets, loading, setSecret])\n\n if (!showSettings) {\n return props.renderDefault(props)\n }\n return (\n <>\n <SettingsView\n title={'Growthbook secret'}\n namespace={namespace}\n keys={pluginConfigKeys}\n onClose={() => {\n setShowSettings(false)\n }}\n />\n {props.renderDefault(props)}\n </>\n )\n}\n","import {createContext, useContext, useMemo, useState} from 'react'\nimport type {ObjectInputProps} from 'sanity'\n\nimport type {GrowthbookContextProps, GrowthbookExperimentFieldPluginConfig} from '../types'\nimport {Secrets} from './Secrets'\n\nexport const GROWTHBOOK_CONFIG_DEFAULT = {\n baseUrl: 'https://api.growthbook.io/api/v1',\n}\n\nconst GrowthbookContext = createContext<GrowthbookContextProps>({\n setSecret: () => undefined,\n secret: undefined,\n})\n\nexport function useGrowthbookContext() {\n return useContext(GrowthbookContext)\n}\n\ntype GrowthbookProps = ObjectInputProps & {\n growthbookFieldPluginConfig: GrowthbookExperimentFieldPluginConfig\n}\n\nexport function GrowthbookProvider(props: GrowthbookProps) {\n const {growthbookFieldPluginConfig} = props\n const [secret, setSecret] = useState<string | undefined>()\n\n const context = useMemo(\n () => ({...growthbookFieldPluginConfig, secret, setSecret}),\n [growthbookFieldPluginConfig, secret, setSecret],\n )\n\n return (\n <GrowthbookContext.Provider value={context}>\n <Secrets {...props} />\n </GrowthbookContext.Provider>\n )\n}\n","import type {SanityClient} from 'sanity'\n\nimport type {ExperimentType, GrowthbookFeature, VariantType} from '../types'\nimport {apiKeyName, namespace} from './Components/Secrets'\nimport type {GrowthbookExperimentFieldPluginConfig} from './types'\n\nconst getBooleanConversion = (value: string) => {\n // control is false\n if (value === 'true') {\n return 'variant'\n } else if (value === 'false') {\n return 'control'\n }\n return value\n}\n\nexport const getExperiments = async ({\n client,\n environment,\n baseUrl,\n project,\n convertBooleans,\n tags,\n}: Omit<GrowthbookExperimentFieldPluginConfig, 'fields' | 'baseUrl'> & {\n client: SanityClient\n baseUrl: string\n}): Promise<ExperimentType[]> => {\n const query = `*[_id == 'secrets.${namespace}'][0].secrets.${apiKeyName}`\n\n const secret = await client.fetch(query) // secret is stored in the content lake using @sanity/studio-secrets\n if (!secret) return []\n\n const featureExperiments: ExperimentType[] = []\n let hasMore = true\n let offset = 0\n const url = new URL(`${baseUrl}/features`)\n if (project) {\n url.searchParams.set('projectId', project)\n }\n\n while (hasMore) {\n url.searchParams.set('offset', offset.toString())\n const response = await fetch(url, {\n headers: {\n Authorization: `Bearer ${secret}`,\n },\n })\n\n const {features, hasMore: responseHasMore, nextOffset} = await response.json()\n\n hasMore = responseHasMore\n offset = nextOffset\n if (!features) continue\n\n features.forEach((feature: GrowthbookFeature) => {\n if (feature.archived) {\n return undefined\n }\n if (tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag))) {\n return undefined\n }\n\n const experiments = feature.environments[environment]?.rules.filter(\n (experiment) => experiment.type === 'experiment-ref' || experiment.type === 'experiment',\n )\n\n if (!experiments) {\n return undefined\n }\n\n const variations: VariantType[] = []\n const uniqueValues = new Set<string>()\n\n experiments.forEach((experiment) => {\n experiment?.variations.forEach((variant) => {\n const value = convertBooleans ? getBooleanConversion(variant.value) : variant.value\n if (!uniqueValues.has(value)) {\n uniqueValues.add(value)\n variations.push({\n id: value,\n label: value,\n })\n }\n })\n })\n const value = {id: feature.id, label: feature.id, variants: variations}\n\n featureExperiments.push(value)\n return undefined\n })\n }\n const sortedFeatureExperiments = featureExperiments.sort((a, b) => a.id.localeCompare(b.id))\n return sortedFeatureExperiments\n}\n","import {definePlugin, isObjectInputProps} from 'sanity'\n\nimport {fieldLevelExperiments as baseFieldLevelExperiments} from '../fieldExperiments'\nimport {flattenSchemaType} from '../utils/flattenSchemaType'\nimport {GROWTHBOOK_CONFIG_DEFAULT, GrowthbookProvider} from './Components/GrowthbookContext'\nimport type {GrowthbookExperimentFieldPluginConfig} from './types'\nimport {getExperiments} from './utils'\n\nexport const fieldLevelExperiments = definePlugin<GrowthbookExperimentFieldPluginConfig>(\n (config) => {\n const pluginConfig = {...GROWTHBOOK_CONFIG_DEFAULT, ...config}\n const {fields, environment, project, convertBooleans, baseUrl, tags} = pluginConfig\n return {\n name: 'sanity-growthbook-personalistaion-plugin-field-level-experiments',\n plugins: [\n baseFieldLevelExperiments({\n fields,\n experiments: (client) =>\n getExperiments({client, environment, baseUrl, project, convertBooleans, tags}),\n }),\n ],\n\n form: {\n components: {\n input: (props) => {\n const isRootInput = props.id === 'root' && isObjectInputProps(props)\n\n if (!isRootInput) {\n return props.renderDefault(props)\n }\n\n const flatFieldTypeNames = flattenSchemaType(props.schemaType).map(\n (field) => field.type.name,\n )\n\n const hasExperiment = flatFieldTypeNames.some((name) => name.startsWith('experiment'))\n\n if (!hasExperiment) {\n return props.renderDefault(props)\n }\n\n const providerProps = {\n ...props,\n growthbookFieldPluginConfig: {\n ...pluginConfig,\n },\n }\n return GrowthbookProvider(providerProps)\n },\n },\n },\n }\n },\n)\n"],"names":["namespace","apiKeyName","pluginConfigKeys","key","title","Secrets","props","$","_c","secrets","loading","useSecrets","setSecret","useGrowthbookContext","showSettings","setShowSettings","useState","t0","t1","undefined","apiKey","useEffect","t2","renderDefault","Symbol","for","t3","t4","GROWTHBOOK_CONFIG_DEFAULT","baseUrl","GrowthbookContext","createContext","secret","useContext","GrowthbookProvider","growthbookFieldPluginConfig","context","getBooleanConversion","value","getExperiments","client","environment","project","convertBooleans","tags","query","fetch","featureExperiments","hasMore","offset","url","URL","searchParams","set","toString","response","headers","Authorization","features","responseHasMore","nextOffset","json","forEach","feature","archived","some","tag","includes","experiments","environments","rules","filter","experiment","type","variations","uniqueValues","Set","variant","has","add","push","id","label","variants","sort","a","b","localeCompare","fieldLevelExperiments","definePlugin","config","pluginConfig","fields","name","plugins","baseFieldLevelExperiments","form","components","input","isObjectInputProps","flattenSchemaType","schemaType","map","field","startsWith","providerProps"],"mappings":"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"}
|