Repository: sanity-io/sanity
Pull request #9475: feat(sanity): remove Actions API remote feature toggle and default to enabled
Author: login: juice49
Created: 2025-05-22T10:56:31Z | Merged: 2025-05-22T13:10:40Z

### Description

This branch removes the Actions API remote feature toggle and enables the Actions API integration by default. This will allow Content Lake to decommission the remote feature toggle API in the future.

The `__internal_serverDocumentActions.enabled` configuration option is still present and effective, but has been marked as deprecated. It will be removed in a separate PR along with the redundant Mutations API integration.

### What to review

The removal of the remote feature toggle.

### Testing

Tested in Studio with `__internal_serverDocumentActions.enabled` present and absent.

### Notes for release

The  `__internal_serverDocumentActions.enabled` configuration option has been deprecated and will be removed in a future release. **Most studios will not be impacted by this change**. If you're unable to remove this option from your configuration for any reason, please open a GitHub issue.



Diff:
diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts
index f3a7c518955a..f00035db32b8 100644
--- a/packages/sanity/src/core/config/types.ts
+++ b/packages/sanity/src/core/config/types.ts
@@ -526,6 +526,9 @@ export interface WorkspaceOptions extends SourceOptions {
    * @internal
    */
   __internal_serverDocumentActions?: {
+    /**
+     * @deprecated The Mutations API integration will be removed in a future release.
+     */
     enabled?: boolean
   }
 
diff --git a/packages/sanity/src/core/store/_legacy/datastores.ts b/packages/sanity/src/core/store/_legacy/datastores.ts
index 8c0af16230b9..eaba086a2fac 100644
--- a/packages/sanity/src/core/store/_legacy/datastores.ts
+++ b/packages/sanity/src/core/store/_legacy/datastores.ts
@@ -22,7 +22,6 @@ import {
 import {createDocumentStore, type DocumentStore, type LatencyReportEvent} from './document'
 import {DocumentDesynced} from './document/__telemetry__/documentOutOfSyncEvents.telemetry'
 import {HighListenerLatencyOccurred} from './document/__telemetry__/listenerLatency.telemetry'
-import {fetchFeatureToggle} from './document/document-pair/utils/fetchFeatureToggle'
 import {type OutOfSyncError} from './document/utils/sequentializeListenerEvents'
 import {createGrantsStore, type GrantsStore} from './grants'
 import {createHistoryStore, type HistoryStore} from './history'
@@ -149,11 +148,8 @@ export function useDocumentStore(): DocumentStore {
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean'
-      ? of(configFlag as boolean)
-      : fetchFeatureToggle(getClient(DEFAULT_STUDIO_CLIENT_OPTIONS))
-  }, [getClient, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
+  }, [workspace.__internal_serverDocumentActions?.enabled])
 
   const telemetry = useTelemetry()
 
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.test.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.test.ts
deleted file mode 100644
index 1613a9604db3..000000000000
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.test.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import {firstValueFrom, from} from 'rxjs'
-import {describe, expect, it, vi} from 'vitest'
-
-import {type ActionsFeatureToggle, mapResponse} from './fetchFeatureToggle'
-
-vi.mock('../../../../../version', () => ({
-  SANITY_VERSION: '3.47.0',
-}))
-
-describe('mapResponse', () => {
-  it('respects the `enabled` property', async () => {
-    const source = from<ActionsFeatureToggle[]>([
-      {
-        enabled: false,
-        compatibleStudioVersions: '>= 3',
-      },
-    ])
-
-    expect(await firstValueFrom(source.pipe(mapResponse()))).toBe(false)
-  })
-
-  it.each([
-    ['>= 2', true],
-    ['>= 3', true],
-    ['>= 4', false],
-
-    ['2', false],
-    ['3', true],
-    ['3.0.0', false],
-    ['4', false],
-
-    ['>= 2.13.13', true],
-    ['>= 3.13.13', true],
-    ['>= 4.13.13', false],
-
-    ['>= 3.46.9', true],
-    ['>= 3.47.0', true],
-    ['>= 3.47.1', false],
-
-    ['< 2 || 3.47.0', true],
-    ['< 2 || 3.45.0', false],
-  ])(
-    'respects the version range constraint specified in the `compatibleStudioVersions` property (%s)',
-    async (compatibleStudioVersions, expected) => {
-      const source = from<ActionsFeatureToggle[]>([
-        {
-          enabled: true,
-          compatibleStudioVersions,
-        },
-      ])
-
-      expect(await firstValueFrom(source.pipe(mapResponse()))).toBe(expected)
-    },
-  )
-})
diff --git a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts b/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts
deleted file mode 100644
index 6c6a9fa6e152..000000000000
--- a/packages/sanity/src/core/store/_legacy/document/document-pair/utils/fetchFeatureToggle.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import {type SanityClient} from '@sanity/client'
-import {map, type Observable, of, type OperatorFunction, ReplaySubject, timeout, timer} from 'rxjs'
-import {catchError, concatMap, share} from 'rxjs/operators'
-import {satisfies} from 'semver'
-
-import {SANITY_VERSION} from '../../../../../version'
-
-export interface ActionsFeatureToggle {
-  enabled: boolean
-  compatibleStudioVersions: string
-}
-
-const CACHE = new WeakMap<SanityClient, Observable<boolean>>()
-
-// How often to refresh the feature toggle
-const REFRESH_INTERVAL = 1000 * 120
-
-// Timer used to reset the observable when it completes or it's refcount drops to zero
-const RESET_TIMER = timer(REFRESH_INTERVAL)
-
-function createFeatureToggle(client: SanityClient) {
-  const {dataset} = client.config()
-
-  return timer(0, REFRESH_INTERVAL).pipe(
-    concatMap(() =>
-      client.observable
-        .request({
-          uri: `/data/actions/${dataset}`,
-        })
-        .pipe(
-          mapResponse(),
-          timeout({first: 2000, with: () => of(false)}),
-          catchError(() =>
-            // If we fail to fetch the feature toggle, we'll just assume it's disabled and fallback to legacy mutations
-            of(false),
-          ),
-        ),
-    ),
-    share({
-      // replay latest known state to new subscribers
-      connector: () => new ReplaySubject(1),
-      // this will typically be completed and unsubscribed from right after the answer is received, so we don't want to reset
-      resetOnComplete: () => RESET_TIMER,
-      // keep it alive for some time after the last subscriber unsubscribes
-      resetOnRefCountZero: () => RESET_TIMER,
-    }),
-  )
-}
-
-export function mapResponse(): OperatorFunction<ActionsFeatureToggle, boolean> {
-  return map<ActionsFeatureToggle, boolean>(
-    (res) =>
-      res.enabled &&
-      satisfies(SANITY_VERSION, res.compatibleStudioVersions, {
-        includePrerelease: true,
-      }),
-  )
-}
-
-export const fetchFeatureToggle = (client: SanityClient): Observable<boolean> => {
-  if (!CACHE.has(client)) {
-    CACHE.set(client, createFeatureToggle(client))
-  }
-  return CACHE.get(client)!
-}
diff --git a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
index 1f6a15f0e3ad..ef17542c9b1f 100644
--- a/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
+++ b/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts
@@ -16,7 +16,6 @@ import {
 } from '../../../util'
 import {useGrantsStore} from '../datastores'
 import {type DocumentStoreExtraOptions, snapshotPair} from '../document'
-import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle'
 import {type GrantsStore, type PermissionCheckResult} from './types'
 
 function getSchemaType(schema: Schema, typeName: string): SchemaType {
@@ -322,9 +321,8 @@ export function useDocumentPairPermissions({
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
-  }, [client, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
+  }, [workspace.__internal_serverDocumentActions?.enabled])
 
   return useDocumentPairPermissionsFromHookFactory(
     useMemo(
diff --git a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
index 656886509d6a..ba6eb032053b 100644
--- a/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
+++ b/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts
@@ -22,7 +22,6 @@ import {
 import {useClient} from '../../../hooks'
 import {DEFAULT_STUDIO_CLIENT_OPTIONS} from '../../../studioClient'
 import {remoteSnapshots, type RemoteSnapshotVersionEvent} from '../document'
-import {fetchFeatureToggle} from '../document/document-pair/utils/fetchFeatureToggle'
 
 interface UseTimelineControllerOpts {
   documentId: string
@@ -161,9 +160,8 @@ export function useTimelineStore({
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
-  }, [client, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
+  }, [workspace.__internal_serverDocumentActions?.enabled])
 
   /**
    * Fetch document snapshots and update the mutable controller.
diff --git a/packages/sanity/src/core/store/events/useEventsStore.ts b/packages/sanity/src/core/store/events/useEventsStore.ts
index f80d63bb147a..f345b8338d96 100644
--- a/packages/sanity/src/core/store/events/useEventsStore.ts
+++ b/packages/sanity/src/core/store/events/useEventsStore.ts
@@ -8,7 +8,6 @@ import {useReleasesStore} from '../../releases/store/useReleasesStore'
 import {RELEASES_STUDIO_CLIENT_OPTIONS} from '../../releases/util/releasesClient'
 import {useWorkspace} from '../../studio/workspace'
 import {getDocumentVariantType} from '../../util/getDocumentVariantType'
-import {fetchFeatureToggle} from '../_legacy/document/document-pair/utils/fetchFeatureToggle'
 import {createEventsStore} from './createEventsStore'
 import {getDocumentAtRevision} from './getDocumentAtRevision'
 import {
@@ -52,9 +51,8 @@ export function useEventsStore({
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
-  }, [client, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
+  }, [workspace.__internal_serverDocumentActions?.enabled])
   const schema = useSchema()
   const schemaType = schema.get(documentType) as ObjectSchemaType | undefined
   const isLiveEdit = Boolean(schemaType?.liveEdit)
diff --git a/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts b/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts
deleted file mode 100644
index 7c6196e4d060..000000000000
--- a/test/e2e/tests/document-actions/fetchFeatureToggle.spec.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import {expect} from '@playwright/test'
-
-import {mockActionsFeatureToggle} from '../../helpers/mockActionsFeatureToggle'
-import {test} from '../../studio-test'
-
-// This test is skipped because the feature toggle is disable by the use of `releases`, see https://github.com/sanity-io/sanity/blob/corel/packages/sanity/src/core/releases/plugin/index.ts#L61-L62
-// Re enable once the feature toggle is removed and we support serverDocumentActions with releases.
-test.skip('Actions API should be used if the feature toggle is enabled and the Studio version satisfies the `compatibleStudioVersions` constraint', async ({
-  page,
-  createDraftDocument,
-}) => {
-  await mockActionsFeatureToggle({
-    response: {
-      enabled: true,
-      compatibleStudioVersions: '>= 3',
-    },
-    page,
-  })
-
-  const actionsEditRequest = page.waitForResponse(
-    (response) =>
-      response.url().includes('/data/actions') && response.request().method() === 'POST',
-    {
-      timeout: 20_000,
-    },
-  )
-
-  const titleInput = page.getByTestId('field-title').getByTestId('string-input')
-
-  await createDraftDocument('/content/book')
-  await titleInput.fill('Test title')
-  const actionsEditResponse = await (await actionsEditRequest).json()
-
-  expect(actionsEditResponse).toEqual(
-    expect.objectContaining({
-      transactionId: expect.any(String),
-    }),
-  )
-})
-
-test('Actions API should not be used if the feature toggle is enabled, but the Studio version does not satisfy the `compatibleStudioVersions` constraint', async ({
-  page,
-  createDraftDocument,
-}) => {
-  await mockActionsFeatureToggle({
-    response: {
-      enabled: true,
-      compatibleStudioVersions: '< 3.0.0',
-    },
-    page,
-  })
-
-  const mutateEditRequest = page.waitForResponse(
-    (response) => response.url().includes('/data/mutate') && response.request().method() === 'POST',
-    {
-      timeout: 20_000,
-    },
-  )
-
-  const titleInput = page.getByTestId('field-title').getByTestId('string-input')
-
-  await createDraftDocument('/content/book')
-  await titleInput.fill('Test title')
-  const mutateEditResponse = await (await mutateEditRequest).json()
-
-  expect(mutateEditResponse).toEqual(
-    expect.objectContaining({
-      transactionId: expect.any(String),
-      results: [
-        {
-          id: expect.any(String),
-          operation: expect.any(String),
-        },
-      ],
-    }),
-  )
-})
-
-test('Actions API should not be used if the feature toggle is not enabled, regardless of whether the Studio version satisfies the `compatibleStudioVersions` constraint', async ({
-  page,
-  createDraftDocument,
-}) => {
-  await mockActionsFeatureToggle({
-    response: {
-      enabled: false,
-      compatibleStudioVersions: '>= 3',
-    },
-    page,
-  })
-
-  const mutateEditRequest = page.waitForResponse(
-    (response) => response.url().includes('/data/mutate') && response.request().method() === 'POST',
-    {
-      timeout: 20_000,
-    },
-  )
-
-  const titleInput = page.getByTestId('field-title').getByTestId('string-input')
-
-  await createDraftDocument('/content/book')
-  await titleInput.fill('Test title')
-  const mutateEditResponse = await (await mutateEditRequest).json()
-
-  expect(mutateEditResponse).toEqual(
-    expect.objectContaining({
-      transactionId: expect.any(String),
-      results: [
-        {
-          id: expect.any(String),
-          operation: expect.any(String),
-        },
-      ],
-    }),
-  )
-})


Reviews:
user: login: jordanl17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T11:40:52Z
user: login: jordanl17
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-22T11:41:19Z
user: login: juice49
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:22:48Z
user: login: pedrobonamin
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T12:52:52Z

Review comments:
diff_hunk: @@ -52,9 +51,8 @@ export function useEventsStore({
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
-  }, [client, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
path: packages/sanity/src/core/store/events/useEventsStore.ts
user: login: jordanl17
body: NIT: This PR is highlighting we are doing this same sort of thing in quite a number of places, Perhaps we abstract to a hook that we can share across them all
created_at: 2025-05-22T11:40:52Z
author_association: MEMBER
diff_hunk: @@ -52,9 +51,8 @@ export function useEventsStore({
 
   const serverActionsEnabled = useMemo(() => {
     const configFlag = workspace.__internal_serverDocumentActions?.enabled
-    // If it's explicitly set, let it override the feature toggle
-    return typeof configFlag === 'boolean' ? of(configFlag as boolean) : fetchFeatureToggle(client)
-  }, [client, workspace.__internal_serverDocumentActions?.enabled])
+    return typeof configFlag === 'boolean' ? of(configFlag) : of(true)
path: packages/sanity/src/core/store/events/useEventsStore.ts
user: login: juice49
body: Yep, it's true. But the next PR will remove all instances of this code, so I chose not to optimise it 🙂. Good catch, though.
created_at: 2025-05-22T12:22:48Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-22T10:56:38Z
body: [vc]: #qxeQJ6UD+NB+xBmoeBImR6l9A9ugXGL9CHUguimYerE=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZXN0LXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktc2FuZGJveC90ZXN0LXN0dWRpby8zeW92dDNZbVJzUnZvdmo5cURDdHR0cWVMZW9GIiwicHJldmlld1VybCI6InRlc3Qtc3R1ZGlvLWdpdC1mZWF0LXNhcHAtMjY4NGVuYWJsZS1hY3Rpb25zLWFwaS1ieS1kZWZhdWx0LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidGVzdC1zdHVkaW8tZ2l0LWZlYXQtc2FwcC0yNjg0ZW5hYmxlLWFjdGlvbnMtYXBpLWJ5LWRlZmF1bHQuc2FuaXR5LmRldiJ9fSx7Im5hbWUiOiJzdHVkaW8td29ya3Nob3AiLCJyb290RGlyZWN0b3J5IjpudWxsLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktc2FuZGJveC9zdHVkaW8td29ya3Nob3AvNzdCdDR2Y3ZCczhDSHZ0WkRjWGF5Y0RTZnk0MiIsIm5leHRDb21taXRTdGF0dXMiOiJJR05PUkVEIiwicHJldmlld1VybCI6InN0dWRpby13b3Jrc2hvcC1naXQtZmVhdC1zYXBwLTI2ODRlbmFibGUtYWN0aW9ucy1hcGktYnktZGVmYXVsdC5zYW5pdHkuZGV2In0seyJuYW1lIjoicGVyZm9ybWFuY2Utc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nhbml0eS1zYW5kYm94L3BlcmZvcm1hbmNlLXN0dWRpby81MnJWR29NenV3cXliZEREU0xQcmREQW9CWlFUIiwicHJldmlld1VybCI6InBlcmZvcm1hbmNlLXN0dWRpby1naXQtZmVhdC1zYXBwLTI2ODRlbmFibGUtYWN0aW9ucy1hcGktZWFmMGQ1LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGVyZm9ybWFuY2Utc3R1ZGlvLWdpdC1mZWF0LXNhcHAtMjY4NGVuYWJsZS1hY3Rpb25zLWFwaS1lYWYwZDUuc2FuaXR5LmRldiJ9fSx7Im5hbWUiOiJ0ZXN0LW5leHQtc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6ImRldi90ZXN0LW5leHQtc3R1ZGlvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvdGVzdC1uZXh0LXN0dWRpby9BSkVTOFpWeHl2aEZzQkx4NHVORXNKdWVWUGNvIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQifSx7Im5hbWUiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOiJkZXYvcGFnZS1idWlsZGluZy1zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvcGFnZS1idWlsZGluZy1zdHVkaW8vOFdwcnZZQXZrOUtSdVNwQnlQRUVtV2RhV3kxayIsInByZXZpZXdVcmwiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpby1naXQtZmVhdC1zYXBwLTI2ODRlbmFibGUtYWN0aW9ucy1hcC04NjU0YjIuc2FuaXR5LmRldiIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpby1naXQtZmVhdC1zYXBwLTI2ODRlbmFibGUtYWN0aW9ucy1hcC04NjU0YjIuc2FuaXR5LmRldiJ9fSx7Im5hbWUiOiJlMmUtc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6bnVsbCwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvZTJlLXN0dWRpby8yUTFWV3ZjUkQ1SHJjOFVreFR4SlV4ZTRIejJuIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **page-building-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/page-building-studio/8WprvYAvk9KRuSpByPEEmWdaWy1k)) | [Visit Preview](https://page-building-studio-git-feat-sapp-2684enable-actions-ap-8654b2.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/page-building-studio-git-feat-sapp-2684enable-actions-ap-8654b2.sanity.dev?via=pr-comment-feedback-link) | May 22, 2025 0:44am |
| **performance-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/performance-studio/52rVGoMzuwqybdDDSLPrdDAoBZQT)) | [Visit Preview](https://performance-studio-git-feat-sapp-2684enable-actions-api-eaf0d5.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/performance-studio-git-feat-sapp-2684enable-actions-api-eaf0d5.sanity.dev?via=pr-comment-feedback-link) | May 22, 2025 0:44am |
| **test-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/test-studio/3yovt3YmRsRvovj9qDCtttqeLeoF)) | [Visit Preview](https://test-studio-git-feat-sapp-2684enable-actions-api-by-default.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/test-studio-git-feat-sapp-2684enable-actions-api-by-default.sanity.dev?via=pr-comment-feedback-link) | May 22, 2025 0:44am |

<details><summary>3 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **e2e-studio** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/e2e-studio/2Q1VWvcRD5Hrc8UkxTxJUxe4Hz2n)) |  |  | May 22, 2025 0:44am |
| **studio-workshop** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/studio-workshop/77Bt4vcvBs8CHvtZDcXaycDSfy42)) | [Visit Preview](https://studio-workshop-git-feat-sapp-2684enable-actions-api-by-default.sanity.dev) |  | May 22, 2025 0:44am |
| **test-next-studio** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/test-next-studio/AJES8ZVxyvhFsBLx4uNEsJueVPco)) |  |  | May 22, 2025 0:44am |
</details>







author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T10:57:17Z
body: No changes to documentation
<!-- thollander/actions-comment-pull-request "docs-report" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-22T11:02:06Z
body: <h2>Coverage Report</h2>
<table> <thead> <tr> <th align="center">Status</th> <th align="left">Category</th> <th align="right">Percentage</th> <th align="right">Covered / Total</th> </tr> </thead> <tbody> <tr> <td align="center">🔵</td> <td align="left">Lines</td> <td align="right">43.39%</td> <td align="right">58398 / 134564</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Statements</td> <td align="right">43.39%</td> <td align="right">58398 / 134564</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Functions</td> <td align="right">48.15%</td> <td align="right">2925 / 6074</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Branches</td> <td align="right">79.44%</td> <td align="right">10838 / 13643</td> </tr> </tbody> </table><details><summary>File Coverage</summary><table> <thead> <tr> <th align="left">File</th> <th align="right">Stmts</th> <th align="right">Branches</th> <th align="right">Functions</th> <th align="right">Lines</th> <th align="left">Uncovered Lines</th> </tr> </thead> <tbody> <tr> <td align="left" colspan="6"><b>Changed Files</b></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/config/types.ts">packages/sanity/src/core/config/types.ts</a></td> <td align="right">0%</td> <td align="right">0%</td> <td align="right">0%</td> <td align="right">0%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/datastores.ts">packages/sanity/src/core/store/_legacy/datastores.ts</a></td> <td align="right">84.29%</td> <td align="right">88.46%</td> <td align="right">81.81%</td> <td align="right">84.29%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/datastores.ts#L158-L159" class="text-red">158-159</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/datastores.ts#L165-L172" class="text-red">165-172</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/datastores.ts#L272-L289" class="text-red">272-289</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/datastores.ts#L294-L312" class="text-red">294-312</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts">packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts</a></td> <td align="right">70.27%</td> <td align="right">52.77%</td> <td align="right">100%</td> <td align="right">70.27%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L25-L26" class="text-red">25-26</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L64-L74" class="text-red">64-74</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L77-L80" class="text-red">77-80</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L89-L105" class="text-red">89-105</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L127-L139" class="text-red">127-139</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L142-L157" class="text-red">142-157</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L160-L161" class="text-red">160-161</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L212-L213" class="text-red">212-213</a>, <a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/grants/documentPairPermissions.ts#L257-L260" class="text-red">257-260</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts">packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts</a></td> <td align="right">17.02%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">17.02%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/_legacy/history/useTimelineStore.ts#L94-L254" class="text-red">94-254</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/events/useEventsStore.ts">packages/sanity/src/core/store/events/useEventsStore.ts</a></td> <td align="right">10.84%</td> <td align="right">100%</td> <td align="right">0%</td> <td align="right">10.84%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/b6fb4f116078eaade7250da595de16a7720dc3b9/packages/sanity/src/core/store/events/useEventsStore.ts#L38-L220" class="text-red">38-220</a></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/sanity-io/sanity/actions/runs/15186910566>#35525</a> for commit <a href="https://github.com/sanity-io/sanity/commit/b6fb4f116078eaade7250da595de16a7720dc3b9">b6fb4f1</a> by the <a href="https://github.com/davelosert/vitest-coverage-report-action">Vitest Coverage Report Action</a></em>

<!-- vitest-coverage-report-marker-root -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-22T11:05:14Z
body: ### 🧪 [E2E Preview environment](https://e2e-studio-5mmzpdb9k.sanity.dev) 

<details>
<summary> 🔑 Environment Variables for Local Testing</summary>

This is the preview URL for the E2E tests: **https://e2e-studio-5mmzpdb9k.sanity.dev**

To run the E2E tests locally, you can use the following environment variables, then run `pnpm test:e2e --ui` to open the Playwright test runner.

💬 Remember to build the project first with `pnpm build:e2e`.

```
  SANITY_E2E_PROJECT_ID=ittbm412
  SANITY_E2E_BASE_URL=https://e2e-studio-5mmzpdb9k.sanity.dev
  SANITY_E2E_DATASET="update depending the project you want to test (pr-9475-chromium-15186910554 || pr-9475-firefox-15186910554 )"
  SANITY_E2E_DATASET_CHROMIUM=pr-9475-chromium-15186910554
  SANITY_E2E_DATASET_FIREFOX=pr-9475-firefox-15186910554
```

</details>
<!-- thollander/actions-comment-pull-request "e2e-preview-url" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-22T11:08:50Z
body: ### ⚡️ Editor Performance Report

Updated Thu, 22 May 2025 13:11:11 GMT


| Benchmark | reference<br/><sup>latency of `sanity@latest`</sup> | experiment<br/><sup>latency of this branch</sup> | Δ (%)<br/><sup>latency difference</sup> | |
| :-- | :-- | :-- | :-- | --- |
| article (title) | 26.7 efps (38ms) | 26.0 efps (39ms) | +1ms (+2.7%) | ✅ |
| article (body) | 30.6 efps (33ms) | 31.5 efps (32ms) | -1ms (-3.1%) | ✅ |
| article (string inside object) | 27.8 efps (36ms) | 28.2 efps (36ms) | -1ms (-1.4%) | ✅ |
| article (string inside array) | 23.8 efps (42ms) | 23.3 efps (43ms) | +1ms (+2.4%) | ✅ |
| recipe (name) | 29.4 efps (34ms) | 45.5 efps (22ms) | -12ms (-35.3%) | ✅ |
| recipe (description) | 31.7 efps (32ms) | 52.6 efps (19ms) | -13ms (-39.7%) | ✅ |
| recipe (instructions) | 76.9 efps (13ms) | 99.9+ efps (9ms) | -4ms (-/-%) | ✅ |
| synthetic (title) | 17.4 efps (58ms) | 17.5 efps (57ms) | -1ms (-0.9%) | ✅ |
| synthetic (string inside object) | 18.0 efps (56ms) | 17.5 efps (57ms) | +2ms (+2.7%) | ✅ |


> **efps** — editor "frames per second". The number of updates assumed to be possible within a second.
>
> Derived from input latency. `efps = 1000 / input_latency`

<details>

<summary><strong>Detailed information</strong></summary>

### 🏠 Reference result

The performance result of `sanity@latest`



| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
| --------- | ------: | --: | --: | --: | ------------: | ------------: |
| article (title) | 38ms | 41ms | 49ms | 161ms | 150ms | 11.6s |
| article (body) | 33ms | 39ms | 61ms | 244ms | 646ms | 9.7s |
| article (string inside object) | 36ms | 38ms | 42ms | 241ms | 232ms | 6.9s |
| article (string inside array) | 42ms | 43ms | 46ms | 69ms | 125ms | 6.8s |
| recipe (name) | 34ms | 37ms | 45ms | 71ms | 55ms | 9.0s |
| recipe (description) | 32ms | 36ms | 56ms | 107ms | 69ms | 6.7s |
| recipe (instructions) | 13ms | 16ms | 19ms | 33ms | 35ms | 6.1s |
| synthetic (title) | 58ms | 65ms | 79ms | 360ms | 1500ms | 15.1s |
| synthetic (string inside object) | 56ms | 61ms | 66ms | 529ms | 1520ms | 9.3s |


### 🧪 Experiment result

The performance result of this branch


| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
| --------- | ------: | --: | --: | --: | ------------: | ------------: |
| article (title) | 39ms | 43ms | 52ms | 162ms | 176ms | 10.4s |
| article (body) | 32ms | 38ms | 76ms | 227ms | 413ms | 9.1s |
| article (string inside object) | 36ms | 39ms | 42ms | 81ms | 106ms | 6.5s |
| article (string inside array) | 43ms | 46ms | 57ms | 99ms | 242ms | 7.1s |
| recipe (name) | 22ms | 25ms | 27ms | 47ms | 0ms | 7.3s |
| recipe (description) | 19ms | 20ms | 23ms | 32ms | 0ms | 4.8s |
| recipe (instructions) | 9ms | 11ms | 14ms | 34ms | 0ms | 5.3s |
| synthetic (title) | 57ms | 60ms | 63ms | 130ms | 784ms | 13.4s |
| synthetic (string inside object) | 57ms | 63ms | 71ms | 176ms | 1378ms | 8.9s |


### 📚 Glossary

> #### column definitions
>
> - **benchmark** — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
> - **latency** — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
> - **p75** — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
> - **p90** — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
> - **p99** — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
> - **blocking time** — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
> - **test duration** — how long the test run took to complete.

</details>

<!-- thollander/actions-comment-pull-request "efps-report" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-22T11:10:02Z
body: ### 📊 Playwright Test Report

**[Download Full E2E Report](https://github.com/sanity-io/sanity/actions/runs/15186910554/artifacts/3177039682)**

This report contains test results, including videos of failing tests.
<!-- thollander/actions-comment-pull-request "playwright-e2e-report" -->
author_association: CONTRIBUTOR