@sanity/personalization-plugin 2.2.0 → 2.3.0-growthbook.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/README.md CHANGED
@@ -25,6 +25,8 @@ Once configured you can query the values using the ids of the experiment and var
25
25
  - [Release new version](#release-new-version)
26
26
  - [License](#license-1)
27
27
 
28
+ For Specific information about the growthbookFieldLevel export see its [readme](/growthbook.md)
29
+
28
30
  ## Installation
29
31
 
30
32
  ```sh
@@ -0,0 +1,15 @@
1
+ import {FieldDefinition} from 'sanity'
2
+ import {Plugin as Plugin_2} from 'sanity'
3
+
4
+ export declare const fieldLevelExperiments: Plugin_2<GrowthbookABConfig>
5
+
6
+ declare type GrowthbookABConfig = {
7
+ fields: (string | FieldDefinition)[]
8
+ environment: string
9
+ baseUrl?: string
10
+ project?: string
11
+ convertBooleans?: boolean
12
+ tags?: string[]
13
+ }
14
+
15
+ export {}
@@ -0,0 +1,15 @@
1
+ import {FieldDefinition} from 'sanity'
2
+ import {Plugin as Plugin_2} from 'sanity'
3
+
4
+ export declare const fieldLevelExperiments: Plugin_2<GrowthbookABConfig>
5
+
6
+ declare type GrowthbookABConfig = {
7
+ fields: (string | FieldDefinition)[]
8
+ environment: string
9
+ baseUrl?: string
10
+ project?: string
11
+ convertBooleans?: boolean
12
+ tags?: string[]
13
+ }
14
+
15
+ export {}
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: !0 });
3
+ var sanity = require("sanity"), index = require("../index.js"), jsxRuntime = require("react/jsx-runtime"), react = require("react"), studioSecrets = require("@sanity/studio-secrets");
4
+ const namespace = "growthbook", pluginConfigKeys = [
5
+ {
6
+ key: "apiKey",
7
+ title: "Your secret API key"
8
+ }
9
+ ], Secrets = (props) => {
10
+ const { secrets, loading } = studioSecrets.useSecrets(namespace), { setSecret } = useGrowthbookContext(), [showSettings, setShowSettings] = react.useState(!1);
11
+ return react.useEffect(() => {
12
+ if (!loading)
13
+ return !secrets && !loading ? (setSecret(void 0), setShowSettings(!0)) : (setSecret(secrets.apiKey), setShowSettings(!1));
14
+ }, [secrets, loading, setSecret]), showSettings ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
15
+ /* @__PURE__ */ jsxRuntime.jsx(
16
+ studioSecrets.SettingsView,
17
+ {
18
+ title: "Growthbook secret",
19
+ namespace,
20
+ keys: pluginConfigKeys,
21
+ onClose: () => {
22
+ setShowSettings(!1);
23
+ }
24
+ }
25
+ ),
26
+ props.renderDefault(props)
27
+ ] }) : props.renderDefault(props);
28
+ }, GROWTHBOOK_CONFIG_DEFAULT = {
29
+ baseUrl: "https://api.growthbook.io/api/v1"
30
+ }, GrowthbookContext = react.createContext({
31
+ setSecret: () => {
32
+ },
33
+ secret: void 0
34
+ });
35
+ function useGrowthbookContext() {
36
+ return react.useContext(GrowthbookContext);
37
+ }
38
+ function GrowthbookProvider(props) {
39
+ const { growthbookFieldPluginConfig } = props, [secret, setSecret] = react.useState(), context = react.useMemo(
40
+ () => ({ ...growthbookFieldPluginConfig, secret, setSecret }),
41
+ [growthbookFieldPluginConfig, secret, setSecret]
42
+ );
43
+ return /* @__PURE__ */ jsxRuntime.jsx(GrowthbookContext.Provider, { value: context, children: /* @__PURE__ */ jsxRuntime.jsx(Secrets, { ...props }) });
44
+ }
45
+ const getBooleanConversion = (value) => value === "true" ? "variant" : value === "false" ? "control" : value, getExperiments = async ({
46
+ client,
47
+ environment,
48
+ baseUrl,
49
+ project,
50
+ convertBooleans,
51
+ tags
52
+ }) => {
53
+ const query = `*[_id == 'secrets.${namespace}'][0].secrets.${pluginConfigKeys[0].key}`, secret = await client.fetch(query);
54
+ if (!secret) return [];
55
+ const featureExperiments = [];
56
+ let hasMore = !0, offset = 0;
57
+ const url = new URL(`${baseUrl}/features`);
58
+ for (project && url.searchParams.set("projectId", project); hasMore; ) {
59
+ url.searchParams.set("offset", offset.toString());
60
+ const response = await fetch(url, {
61
+ headers: {
62
+ Authorization: `Bearer ${secret}`
63
+ }
64
+ }), { features, hasMore: responseHasMore, nextOffset } = await response.json();
65
+ hasMore = responseHasMore, offset = nextOffset, features && features.forEach((feature) => {
66
+ if (feature.archived || tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag)))
67
+ return;
68
+ const experiments = feature.environments[environment]?.rules.filter(
69
+ (experiment) => experiment.type === "experiment-ref" || experiment.type === "experiment"
70
+ );
71
+ if (!experiments)
72
+ return;
73
+ const variations = [], uniqueValues = /* @__PURE__ */ new Set();
74
+ experiments.forEach((experiment) => {
75
+ experiment?.variations.forEach((variant) => {
76
+ const value2 = convertBooleans ? getBooleanConversion(variant.value) : variant.value;
77
+ uniqueValues.has(value2) || (uniqueValues.add(value2), variations.push({
78
+ id: value2,
79
+ label: value2
80
+ }));
81
+ });
82
+ });
83
+ const value = { id: feature.id, label: feature.id, variants: variations };
84
+ featureExperiments.push(value);
85
+ });
86
+ }
87
+ return featureExperiments.sort((a, b) => a.id.localeCompare(b.id));
88
+ }, fieldLevelExperiments = sanity.definePlugin((config) => {
89
+ const pluginConfig = { ...GROWTHBOOK_CONFIG_DEFAULT, ...config }, { fields, environment, project, convertBooleans, baseUrl, tags } = pluginConfig;
90
+ return {
91
+ name: "sanity-growthbook-personalistaion-plugin-field-level-experiments",
92
+ plugins: [
93
+ index.fieldLevelExperiments({
94
+ fields,
95
+ experiments: (client) => getExperiments({ client, environment, baseUrl, project, convertBooleans, tags })
96
+ })
97
+ ],
98
+ form: {
99
+ components: {
100
+ input: (props) => {
101
+ if (!(props.id === "root" && sanity.isObjectInputProps(props)) || !index.flattenSchemaType(props.schemaType).map(
102
+ (field) => field.type.name
103
+ ).some((name) => name.startsWith("experiment")))
104
+ return props.renderDefault(props);
105
+ const providerProps = {
106
+ ...props,
107
+ growthbookFieldPluginConfig: {
108
+ ...pluginConfig
109
+ }
110
+ };
111
+ return GrowthbookProvider(providerProps);
112
+ }
113
+ }
114
+ }
115
+ };
116
+ });
117
+ exports.fieldLevelExperiments = fieldLevelExperiments;
118
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 {GrowthbookABConfig, GrowthbookContextProps} 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: GrowthbookABConfig\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 {GrowthbookABConfig} 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<GrowthbookABConfig, '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 {GrowthbookABConfig} from './types'\nimport {getExperiments} from './utils'\n\nexport const fieldLevelExperiments = definePlugin<GrowthbookABConfig>((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"],"names":["useSecrets","useState","useEffect","jsxs","Fragment","jsx","SettingsView","createContext","useContext","useMemo","value","definePlugin","baseFieldLevelExperiments","isObjectInputProps","flattenSchemaType"],"mappings":"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"}
@@ -0,0 +1,122 @@
1
+ import { definePlugin, isObjectInputProps } from "sanity";
2
+ import { fieldLevelExperiments as fieldLevelExperiments$1, flattenSchemaType } from "../index.mjs";
3
+ import { jsxs, Fragment, jsx } from "react/jsx-runtime";
4
+ import { useState, useEffect, createContext, useMemo, useContext } from "react";
5
+ import { useSecrets, SettingsView } from "@sanity/studio-secrets";
6
+ const namespace = "growthbook", pluginConfigKeys = [
7
+ {
8
+ key: "apiKey",
9
+ title: "Your secret API key"
10
+ }
11
+ ], Secrets = (props) => {
12
+ const { secrets, loading } = useSecrets(namespace), { setSecret } = useGrowthbookContext(), [showSettings, setShowSettings] = useState(!1);
13
+ return useEffect(() => {
14
+ if (!loading)
15
+ return !secrets && !loading ? (setSecret(void 0), setShowSettings(!0)) : (setSecret(secrets.apiKey), setShowSettings(!1));
16
+ }, [secrets, loading, setSecret]), showSettings ? /* @__PURE__ */ jsxs(Fragment, { children: [
17
+ /* @__PURE__ */ jsx(
18
+ SettingsView,
19
+ {
20
+ title: "Growthbook secret",
21
+ namespace,
22
+ keys: pluginConfigKeys,
23
+ onClose: () => {
24
+ setShowSettings(!1);
25
+ }
26
+ }
27
+ ),
28
+ props.renderDefault(props)
29
+ ] }) : props.renderDefault(props);
30
+ }, GROWTHBOOK_CONFIG_DEFAULT = {
31
+ baseUrl: "https://api.growthbook.io/api/v1"
32
+ }, GrowthbookContext = createContext({
33
+ setSecret: () => {
34
+ },
35
+ secret: void 0
36
+ });
37
+ function useGrowthbookContext() {
38
+ return useContext(GrowthbookContext);
39
+ }
40
+ function GrowthbookProvider(props) {
41
+ const { growthbookFieldPluginConfig } = props, [secret, setSecret] = useState(), context = useMemo(
42
+ () => ({ ...growthbookFieldPluginConfig, secret, setSecret }),
43
+ [growthbookFieldPluginConfig, secret, setSecret]
44
+ );
45
+ return /* @__PURE__ */ jsx(GrowthbookContext.Provider, { value: context, children: /* @__PURE__ */ jsx(Secrets, { ...props }) });
46
+ }
47
+ const getBooleanConversion = (value) => value === "true" ? "variant" : value === "false" ? "control" : value, getExperiments = async ({
48
+ client,
49
+ environment,
50
+ baseUrl,
51
+ project,
52
+ convertBooleans,
53
+ tags
54
+ }) => {
55
+ const query = `*[_id == 'secrets.${namespace}'][0].secrets.${pluginConfigKeys[0].key}`, secret = await client.fetch(query);
56
+ if (!secret) return [];
57
+ const featureExperiments = [];
58
+ let hasMore = !0, offset = 0;
59
+ const url = new URL(`${baseUrl}/features`);
60
+ for (project && url.searchParams.set("projectId", project); hasMore; ) {
61
+ url.searchParams.set("offset", offset.toString());
62
+ const response = await fetch(url, {
63
+ headers: {
64
+ Authorization: `Bearer ${secret}`
65
+ }
66
+ }), { features, hasMore: responseHasMore, nextOffset } = await response.json();
67
+ hasMore = responseHasMore, offset = nextOffset, features && features.forEach((feature) => {
68
+ if (feature.archived || tags && feature.tags && !feature.tags.some((tag) => tags.includes(tag)))
69
+ return;
70
+ const experiments = feature.environments[environment]?.rules.filter(
71
+ (experiment) => experiment.type === "experiment-ref" || experiment.type === "experiment"
72
+ );
73
+ if (!experiments)
74
+ return;
75
+ const variations = [], uniqueValues = /* @__PURE__ */ new Set();
76
+ experiments.forEach((experiment) => {
77
+ experiment?.variations.forEach((variant) => {
78
+ const value2 = convertBooleans ? getBooleanConversion(variant.value) : variant.value;
79
+ uniqueValues.has(value2) || (uniqueValues.add(value2), variations.push({
80
+ id: value2,
81
+ label: value2
82
+ }));
83
+ });
84
+ });
85
+ const value = { id: feature.id, label: feature.id, variants: variations };
86
+ featureExperiments.push(value);
87
+ });
88
+ }
89
+ return featureExperiments.sort((a, b) => a.id.localeCompare(b.id));
90
+ }, fieldLevelExperiments = definePlugin((config) => {
91
+ const pluginConfig = { ...GROWTHBOOK_CONFIG_DEFAULT, ...config }, { fields, environment, project, convertBooleans, baseUrl, tags } = pluginConfig;
92
+ return {
93
+ name: "sanity-growthbook-personalistaion-plugin-field-level-experiments",
94
+ plugins: [
95
+ fieldLevelExperiments$1({
96
+ fields,
97
+ experiments: (client) => getExperiments({ client, environment, baseUrl, project, convertBooleans, tags })
98
+ })
99
+ ],
100
+ form: {
101
+ components: {
102
+ input: (props) => {
103
+ if (!(props.id === "root" && isObjectInputProps(props)) || !flattenSchemaType(props.schemaType).map(
104
+ (field) => field.type.name
105
+ ).some((name) => name.startsWith("experiment")))
106
+ return props.renderDefault(props);
107
+ const providerProps = {
108
+ ...props,
109
+ growthbookFieldPluginConfig: {
110
+ ...pluginConfig
111
+ }
112
+ };
113
+ return GrowthbookProvider(providerProps);
114
+ }
115
+ }
116
+ }
117
+ };
118
+ });
119
+ export {
120
+ fieldLevelExperiments
121
+ };
122
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","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 {GrowthbookABConfig, GrowthbookContextProps} 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: GrowthbookABConfig\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 {GrowthbookABConfig} 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<GrowthbookABConfig, '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 {GrowthbookABConfig} from './types'\nimport {getExperiments} from './utils'\n\nexport const fieldLevelExperiments = definePlugin<GrowthbookABConfig>((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"],"names":["value","baseFieldLevelExperiments"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -42,7 +42,9 @@ export declare const fieldLevelExperiments: Plugin_2<FieldPluginConfig>
42
42
 
43
43
  export declare type FieldPluginConfig = {
44
44
  fields: (string | FieldDefinition)[]
45
- experiments: ExperimentType[] | ((client: SanityClient) => Promise<ExperimentType[]>)
45
+ experiments:
46
+ | ExperimentType[]
47
+ | ((client: SanityClient, secret?: string) => Promise<ExperimentType[]>)
46
48
  apiVersion?: string
47
49
  experimentNameOverride?: string
48
50
  variantNameOverride?: string
@@ -56,6 +58,194 @@ export declare type FieldPluginConfig = {
56
58
  */
57
59
  export declare function flattenSchemaType(schemaType: SchemaType): ObjectFieldWithPath[]
58
60
 
61
+ export declare type GrowthbookExperiment = {
62
+ id: string
63
+ dateCreated: string
64
+ dateUpdated: string
65
+ name: string
66
+ project: string
67
+ hypothesis: string
68
+ description: string
69
+ tags: [string]
70
+ owner: string
71
+ archived: boolean
72
+ status: string
73
+ autoRefresh: boolean
74
+ hashAttribute: string
75
+ fallbackAttribute: string
76
+ hashVersion: number
77
+ disableStickyBucketing: boolean
78
+ bucketVersion: number
79
+ minBucketVersion: number
80
+ variations: [
81
+ {
82
+ variationId: string
83
+ key: string
84
+ name: string
85
+ description: string
86
+ screenshots: [string]
87
+ },
88
+ ]
89
+ phases: [
90
+ {
91
+ name: string
92
+ dateStarted: string
93
+ dateEnded: string
94
+ reasonForStopping: string
95
+ seed: string
96
+ coverage: 0
97
+ trafficSplit: [
98
+ {
99
+ variationId: string
100
+ weight: 0
101
+ },
102
+ ]
103
+ namespace: {
104
+ namespaceId: string
105
+ range: []
106
+ }
107
+ targetingCondition: string
108
+ savedGroupTargeting: [
109
+ {
110
+ matchType: string
111
+ savedGroups: [string]
112
+ },
113
+ ]
114
+ },
115
+ ]
116
+ settings: {
117
+ datasourceId: string
118
+ assignmentQueryId: string
119
+ experimentId: string
120
+ segmentId: string
121
+ queryFilter: string
122
+ inProgressConversions: string
123
+ attributionModel: string
124
+ statsEngine: string
125
+ regressionAdjustmentEnabled: boolean
126
+ goals: [
127
+ {
128
+ metricId: string
129
+ overrides: {
130
+ delayHours: 0
131
+ windowHours: 0
132
+ window: string
133
+ winRiskThreshold: 0
134
+ loseRiskThreshold: 0
135
+ }
136
+ },
137
+ ]
138
+ secondaryMetrics: [
139
+ {
140
+ metricId: string
141
+ overrides: {
142
+ delayHours: 0
143
+ windowHours: 0
144
+ window: string
145
+ winRiskThreshold: 0
146
+ loseRiskThreshold: 0
147
+ }
148
+ },
149
+ ]
150
+ guardrails: [
151
+ {
152
+ metricId: string
153
+ overrides: {
154
+ delayHours: 0
155
+ windowHours: 0
156
+ window: string
157
+ winRiskThreshold: 0
158
+ loseRiskThreshold: 0
159
+ }
160
+ },
161
+ ]
162
+ activationMetric: {
163
+ metricId: string
164
+ overrides: {
165
+ delayHours: 0
166
+ windowHours: 0
167
+ window: string
168
+ winRiskThreshold: 0
169
+ loseRiskThreshold: 0
170
+ }
171
+ }
172
+ }
173
+ resultSummary: {
174
+ status: string
175
+ winner: string
176
+ conclusions: string
177
+ releasedVariationId: string
178
+ excludeFromPayload: boolean
179
+ }
180
+ }
181
+
182
+ export declare type GrowthbookFeature = {
183
+ id: string
184
+ dateCreated: string
185
+ dateUpdated: string
186
+ archived: boolean
187
+ description: string
188
+ owner: string
189
+ project: string
190
+ valueType: string
191
+ defaultValue: string
192
+ tags: string[]
193
+ environments: {
194
+ [key: string]: {
195
+ enabled: boolean
196
+ defaultValue: string
197
+ rules: {
198
+ description: string
199
+ condition: string
200
+ savedGroupTargeting: {
201
+ matchType: string
202
+ savedGroups: string[]
203
+ }[]
204
+ id: string
205
+ enabled: boolean
206
+ type: string
207
+ value: string
208
+ variations: {
209
+ value: string
210
+ variationId: string
211
+ }[]
212
+ }[]
213
+ definition: string
214
+ draft: {
215
+ enabled: boolean
216
+ defaultValue: string
217
+ rules: {
218
+ description: string
219
+ condition: string
220
+ savedGroupTargeting: {
221
+ matchType: string
222
+ savedGroups: string[]
223
+ }[]
224
+ id: string
225
+ enabled: boolean
226
+ type: string
227
+ value: string
228
+ variations: {
229
+ value: string
230
+ variationId: string
231
+ }[]
232
+ }[]
233
+ definition: string
234
+ }
235
+ }
236
+ }
237
+ prerequisites: {
238
+ parentId: string
239
+ parentCondition: string
240
+ }[]
241
+ revision: {
242
+ version: number
243
+ comment: string
244
+ date: string
245
+ publishedBy: string
246
+ }
247
+ }
248
+
59
249
  export declare type ObjectFieldWithPath = ObjectField<SchemaType> & {
60
250
  path: Path
61
251
  }