Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added growthbook field experiments as a plugin #13

Draft
wants to merge 36 commits into
base: canary
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d28d2cc
feat: added growthbook flied experiments as a plugin
jjburbridge Nov 28, 2024
db58178
Merge pull request #14 from sanity-io/canary-releases
jjburbridge Nov 28, 2024
bef137f
Merge branch 'main' of github.com:sanity-io/sanity-plugin-personalisa…
jjburbridge Nov 28, 2024
880eefd
chore: new workflow for doing pre-releases of any branch
jjburbridge Nov 28, 2024
9b70f1b
Merge pull request #15 from sanity-io/pre-release-workflow
jjburbridge Nov 28, 2024
48e09ae
chore: update pre-release cmd
jjburbridge Nov 28, 2024
733c6de
chore: fix typo
jjburbridge Nov 28, 2024
a978f4a
chore: escapre braces
jjburbridge Nov 28, 2024
4b11ca0
chore: escape the quotes
jjburbridge Nov 29, 2024
7f64e34
chore: fix the json for pre-release
jjburbridge Nov 29, 2024
365c493
chore: update comments
jjburbridge Nov 29, 2024
fd075dd
Merge pull request #16 from sanity-io/pre-release-workflow
jjburbridge Nov 29, 2024
37a66df
Merge branch 'main' of github.com:sanity-io/sanity-plugin-personalisa…
jjburbridge Nov 29, 2024
8d059d9
chore(release): 1.1.0-growthbook.1 [skip ci]
semantic-release-bot Nov 29, 2024
48a08bd
chore: updating readme to include more details
jjburbridge Nov 29, 2024
80d3905
chore: add more context in readme
jjburbridge Dec 4, 2024
b61cdce
fix: use onchagne from props rather than document operation for patch
jjburbridge Dec 9, 2024
dba609b
Merge pull request #19 from sanity-io/sa-110
jjburbridge Dec 9, 2024
6525aa1
chore(release): 1.1.0 [skip ci]
semantic-release-bot Dec 9, 2024
bca3b37
Merge branch 'main' of github.com:sanity-io/sanity-plugin-personalisa…
jjburbridge Dec 9, 2024
0e39192
chore: updated icon for adding experiment
jjburbridge Dec 10, 2024
827cf0f
chore: added gif show use of plugin
jjburbridge Dec 10, 2024
fcdb589
chore: included example of async function getting/mapping experiments…
jjburbridge Dec 10, 2024
b12953a
Merge pull request #20 from sanity-io/update-readme
jjburbridge Dec 17, 2024
e81af6e
chore(deps): update non-major
renovate[bot] Dec 17, 2024
1223115
Merge pull request #17 from sanity-io/renovate/non-major
jjburbridge Dec 18, 2024
4933440
chore(deps): lock file maintenance
renovate[bot] Dec 18, 2024
57c4cb4
Merge pull request #18 from sanity-io/renovate/lock-file-maintenance
jjburbridge Dec 18, 2024
eb40e0b
fix: get experiments from feature flags for growthbook and store valu…
jjburbridge Dec 19, 2024
e0cc75f
chore(release): 1.2.0-growthbook.1 [skip ci]
semantic-release-bot Dec 19, 2024
51a3a59
feat: added boolena conversion check
jjburbridge Dec 24, 2024
5d86b83
Merge branch 'growthbook' of github.com:sanity-io/sanity-plugin-perso…
jjburbridge Dec 24, 2024
57d3d9a
fix: remove unneeded comments
jjburbridge Jan 2, 2025
29e3e4c
Merge pull request #21 from sanity-io/release
jjburbridge Jan 2, 2025
8feac83
chore(release): 1.1.1 [skip ci]
semantic-release-bot Jan 2, 2025
3fcacb9
Merge branch 'main' of github.com:sanity-io/sanity-plugin-personalisa…
jjburbridge Jan 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .github/workflows/pre-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
name: Pre-Release

# Workflow name based on selected inputs. Fallback to default Github naming when expression evaluates to empty string
run-name: >-
${{
inputs.prerelease && inputs.test && format('Build {0} ➤ Test ➤ Publish to NPM', github.ref_name) ||
inputs.prerelease && !inputs.test && format('Build {0} ➤ Skip Tests ➤ Publish pre release to NPM', github.ref_name) ||
github.event_name == 'workflow_dispatch' && inputs.test && format('Build {0} ➤ Test', github.ref_name) ||
github.event_name == 'workflow_dispatch' && !inputs.test && format('Build {0} ➤ Skip Tests', github.ref_name) ||
''
}}

on:
workflow_dispatch:
inputs:
# test:
# description: Run tests
# required: true
# default: true
# type: boolean
prerelease:
description: create new pre release
required: true
default: false
type: boolean

concurrency:
# On PRs builds will cancel if new pushes happen before the CI completes, as it defines `github.head_ref` and gives it the name of the branch the PR wants to merge into
# Otherwise `github.run_id` ensures that you can quickly merge a queue of PRs without causing tests to auto cancel on any of the commits pushed to main.
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

permissions:
contents: read # for checkout

jobs:
build:
runs-on: ubuntu-latest
name: Lint & Build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: npm
node-version: lts/*
- run: yarn install --frozen-lockfile
# Linting can be skipped
- run: yarn lint
# if: github.event.inputs.test != 'false'
# But not the build script, as semantic-release will crash if this command fails so it makes sense to test it early
- run: yarn prepublishOnly --if-present

# Will add when I add tests
# test:
# needs: build
# # The test matrix can be skipped, in case a new release needs to be fast-tracked and tests are already passing on main
# if: github.event.inputs.test != 'false'
# runs-on: ${{ matrix.os }}
# name: Node.js ${{ matrix.node }} / ${{ matrix.os }}
# strategy:
# # A test failing on windows doesn't mean it'll fail on macos. It's useful to let all tests run to its completion to get the full picture
# fail-fast: false
# matrix:
# # Run the testing suite on each major OS with the latest LTS release of Node.js
# os: [macos-latest, ubuntu-latest, windows-latest]
# node: [lts/*]
# # It makes sense to also test the oldest, and latest, versions of Node.js, on ubuntu-only since it's the fastest CI runner
# include:
# - os: ubuntu-latest
# # Test the oldest LTS release of Node that's still receiving bugfixes and security patches, versions older than that have reached End-of-Life
# node: lts/-1
# - os: ubuntu-latest
# # Test the actively developed version that will become the latest LTS release next October
# node: current
# steps:
# # It's only necessary to do this for windows, as mac and ubuntu are sane OS's that already use LF
# - name: Set git to use LF
# if: matrix.os == 'windows-latest'
# run: |
# git config --global core.autocrlf false
# git config --global core.eol lf
# - uses: actions/checkout@v4
# - uses: actions/setup-node@v4
# with:
# cache: npm
# node-version: ${{ matrix.node }}
# - run: yarn install
# - run: npm test --if-present

release:
permissions:
id-token: write # to enable use of OIDC for npm provenance
needs: [build]
# only run if opt-in during workflow_dispatch
# add back in && needs.test.result != 'failure' && needs.test.result != 'cancelled'
if: always() && github.event.inputs.prerelease == 'true' && needs.build.result != 'failure'
runs-on: ubuntu-latest
name: Semantic release
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.ECOSPARK_APP_ID }}
private-key: ${{ secrets.ECOSPARK_APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
# Need to fetch entire commit history to
# analyze every commit since last release
fetch-depth: 0
# Uses generated token to allow pushing commits back
token: ${{ steps.app-token.outputs.token }}
# Make sure the value of GITHUB_TOKEN will not be persisted in repo's config
persist-credentials: false
- uses: actions/setup-node@v4
with:
cache: npm
node-version: lts/*
- run: yarn install --frozen-lockfile
- run: npm audit signatures
# add the current branch to prelease accepted list, then run release
- run: 'echo {\"extends\": \"@sanity/semantic-release-preset\",\"branches\": [\"main\", {\"name\": \"canary\", \"prerelease\": true}, {\"name\": \"${{github.ref_name}}\", \"prerelease\": true}]} > .releaserc.json && npx semantic-release'
# Don't allow interrupting the release step if the job is cancelled, as it can lead to an inconsistent state'
# e.g. git tags were pushed but it exited before `npm publish`
if: always()
env:
NPM_CONFIG_PROVENANCE: true
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
All notable changes to this project will be documented in this file. See
[Conventional Commits](https://conventionalcommits.org) for commit guidelines.

## [1.1.1](https://github.com/sanity-io/sanity-plugin-personalisation/compare/v1.1.0...v1.1.1) (2025-01-02)

### Bug Fixes

- remove unneeded comments ([57d3d9a](https://github.com/sanity-io/sanity-plugin-personalisation/commit/57d3d9a16ed39296ca5d28a9d997e6856798c143))

## [1.1.0](https://github.com/sanity-io/sanity-plugin-personalisation/compare/v1.0.3...v1.1.0) (2024-12-09)

### Features

- allow canary branch to make releases ([936068d](https://github.com/sanity-io/sanity-plugin-personalisation/commit/936068dd392074c62821f5ab2ba4bbcfb34a9489))

### Bug Fixes

- use onchagne from props rather than document operation for patch ([b61cdce](https://github.com/sanity-io/sanity-plugin-personalisation/commit/b61cdce12e470125fe70293bce983f48d091ade6))

## [1.0.3](https://github.com/sanity-io/sanity-plugin-personalisation/compare/v1.0.2...v1.0.3) (2024-11-26)

### Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Jon Burbridge
Copyright (c) 2025 Jon Burbridge

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
50 changes: 44 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,26 @@

> This is a **Sanity Studio v3** plugin.

This plugin allows users to add a/b testing experiemtnts on a field level basis
This plugin allows users to add a/b/n testing experiments to individual fields.

![image](./overview.gif)

For this plugin you need to defined the experiments you are running and the variations those experiments have. Each experiment needs to have an id, a label, and an array of variants that have an id and a label. You can either pass an array of experiments in the plugin config, or you can use and async function to retrieve the experiments and variants from an external service like growthbook, Amplitude, LaunchDarkly... You could even store the experiments in your sanity dataset.

Once configured you can query the values using the ids of the experiment and variant

- [@sanity/personalisation-plugin](#@sanity/personalisation-plugin)
- [Installation](#installation)
- [Usage](#usage)
- [Loading Experiments](#loading-experiments)
- [Using complex field configurations](#using-complex-field-configurations)
- [Validation of individual array items](#validation-of-individual-array-items)
- [Shape of stored data](#shape-of-stored-data)
- [Querying data](#querying-data)
- [License](#license)
- [Develop \& test](#develop--test)
- [Release new version](#release-new-version)
- [License](#license-1)

## Installation

Expand Down Expand Up @@ -62,10 +81,10 @@ export default defineConfig({

This will register two new fields to the schema., based on the setting passed intto `fields:`

- `experimentString` an Object field with `string` field called `default`, a `string` field called `experimentValue` and an array field of:
- `varirantsString` an object field with a `string` field called `value`, a string field called `variandId`, a `string` field called `experimentId`.
- `experimentString` an Object field with `string` field called `default`, a `string` field called `experimentId` and an array field of type:
- `varirantsString` an object field with a `string` field called `value`, a string field called `variantId`, a `string` field called `experimentId`.

Use them in your schema like this:
Use the experiment field in your schema like this:

```ts
//for Example in post.ts
Expand Down Expand Up @@ -120,7 +139,26 @@ Or an asynchronous function that returns an array of objects with an id and labe
```ts
experiments: async () => {
const response = await fetch('https://example.com/experiments')
return response.json()
const {externalExperiments} = await response.json()

const experiments: ExperimentType[] = externalExperiments?.map(
(experiment) => {
const experimentId = experiment.id
const experimentLabel = experiment.name
const variants = experiment.variations?.map((variant) => {
return {
id: variant.variationId,
label: variant.name,
}
})
return {
id: experimentId,
label: experimentLabel,
variants,
}
},
)
return experiments
}
```

Expand Down Expand Up @@ -162,7 +200,7 @@ export default defineConfig({

This would also create two new fields in your schema.

- `experimentFeaturedProduct` an Object field with `reference` field called `default`, a `string` field called `experimentId` and an array field of:
- `experimentFeaturedProduct` an Object field with `reference` field called `default`, a `string` field called `experimentId` and an array field of type:
- `variantFeaturedProduct` an object field with a `reference` field called `value`, a string field called `variandId`, a `string` field called `experimentId`.

Note that the name key in the field gets rewritten to value and is instead used to name the object field.
Expand Down
Binary file added overview.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 12 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sanity/personalisation-plugin",
"version": "1.0.3",
"version": "1.2.0-growthbook.1",
"description": "Plugin to help with personalisation, a/b testing when using Sanity",
"keywords": [
"sanity",
Expand Down Expand Up @@ -45,36 +45,38 @@
},
"dependencies": {
"@sanity/incompatible-plugin": "^1.0.4",
"@sanity/studio-secrets": "^3.0.0",
"@sanity/ui": "^2.8.19",
"@sanity/uuid": "^3.0.2",
"fast-deep-equal": "^3.1.3",
"react-icons": "^5.4.0",
"suspend-react": "^0.1.3"
},
"devDependencies": {
"@commitlint/cli": "^19.6.0",
"@commitlint/cli": "^19.6.1",
"@commitlint/config-conventional": "^19.6.0",
"@sanity/pkg-utils": "^6.11.12",
"@sanity/pkg-utils": "^6.12.0",
"@sanity/plugin-kit": "^4.0.18",
"@sanity/semantic-release-preset": "^5.0.0",
"@types/react": "^18.3.12",
"@typescript-eslint/eslint-plugin": "^8.16.0",
"@typescript-eslint/parser": "^8.16.0",
"@types/react": "^18.3.17",
"@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-sanity": "^7.1.3",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-hooks": "^5.1.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"prettier": "^3.4.0",
"prettier": "^3.4.2",
"prettier-plugin-packagejson": "^2.5.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sanity": "^3.64.3",
"sanity": "^3.67.1",
"semantic-release": "^24.2.0",
"styled-components": "^6.1.13",
"typescript": "^5.6.3"
"typescript": "^5.7.2"
},
"peerDependencies": {
"react": "^18",
Expand Down
13 changes: 8 additions & 5 deletions src/components/ExperimentContext.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import equal from 'fast-deep-equal'
import {createContext, useContext, useMemo} from 'react'
import {createContext, useContext, useMemo, useState} from 'react'
import {type ObjectInputProps, useClient, useWorkspace} from 'sanity'
import {suspend} from 'suspend-react'

Expand All @@ -16,9 +16,11 @@
export const ExperimentContext = createContext<ExperimentContextProps>({
...CONFIG_DEFAULT,
experiments: [],
setSecret: () => undefined,
secret: undefined,
})

export function useExperimentContext() {

Check warning on line 23 in src/components/ExperimentContext.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

Missing return type on function
return useContext(ExperimentContext)
}

Expand All @@ -26,8 +28,9 @@
experimentFieldPluginConfig: Required<FieldPluginConfig>
}

export function ExperimentProvider(props: ExperimentProps) {

Check warning on line 31 in src/components/ExperimentContext.tsx

View workflow job for this annotation

GitHub Actions / Lint & Build

Missing return type on function
const {experimentFieldPluginConfig} = props
const [secret, setSecret] = useState<string | undefined>()

const client = useClient({apiVersion: experimentFieldPluginConfig.apiVersion})
const workspace = useWorkspace()
Expand All @@ -39,17 +42,17 @@
// eslint-disable-next-line require-await
async () => {
if (typeof experimentFieldPluginConfig.experiments === 'function') {
return experimentFieldPluginConfig.experiments(client)
return experimentFieldPluginConfig.experiments(client, secret)
}
return experimentFieldPluginConfig.experiments
},
[workspace],
[workspace, secret],
{equal},
)

const context = useMemo(
() => ({...experimentFieldPluginConfig, experiments}),
[experimentFieldPluginConfig, experiments],
() => ({...experimentFieldPluginConfig, experiments, secret, setSecret}),
[experimentFieldPluginConfig, experiments, secret, setSecret],
)

return (
Expand Down
Loading
Loading