Repository: sanity-io/sanity
Pull request #8963: feat(telemetry): track INP performance from the field
Author: login: bjoerge
Created: 2025-03-17T17:51:04Z | Merged: 2025-03-26T15:49:25Z

### Description
Collects [INP](https://web.dev/articles/inp) (Interaction to Next Paint) telemetry from the field.

This adds a performance observer that samples INP at most every minute. In addition to the INP duration, we also log the interaction event target. Example payload:

```json
{
  "target": "input#myNumberField.Input-sc-h62wco-4.evDwFo",
  "attrs": {"testId":"number-input"},
  "interaction": "keydown",
  "duration": 160
}
```


### What to review
- Does the event definition look sane?
- Is the sample rate of 1 minute acceptable?

### Notes for release
n/a


Diff:
diff --git a/packages/sanity/src/core/studio/StudioProvider.tsx b/packages/sanity/src/core/studio/StudioProvider.tsx
index ae502bf26321..e1565963a1cd 100644
--- a/packages/sanity/src/core/studio/StudioProvider.tsx
+++ b/packages/sanity/src/core/studio/StudioProvider.tsx
@@ -29,8 +29,8 @@ import {
 import {type StudioProps} from './Studio'
 import {StudioAnnouncementsProvider} from './studioAnnouncements/StudioAnnouncementsProvider'
 import {StudioErrorBoundary} from './StudioErrorBoundary'
-import {StudioTelemetryProvider} from './StudioTelemetryProvider'
 import {StudioThemeProvider} from './StudioThemeProvider'
+import {StudioTelemetryProvider} from './telemetry/StudioTelemetryProvider'
 import {WorkspaceLoader} from './workspaceLoader'
 import {WorkspacesProvider} from './workspaces'
 
diff --git a/packages/sanity/src/core/studio/__telemetry__/performance.telemetry.ts b/packages/sanity/src/core/studio/__telemetry__/performance.telemetry.ts
new file mode 100644
index 000000000000..66ec8c60e80d
--- /dev/null
+++ b/packages/sanity/src/core/studio/__telemetry__/performance.telemetry.ts
@@ -0,0 +1,19 @@
+import {defineEvent} from '@sanity/telemetry'
+
+export interface PerformanceINPMeasuredData {
+  target: string | null
+  attrs?: {
+    ui?: string
+    testId?: string
+  }
+  duration: number
+  interaction: string
+}
+
+export const PerformanceINPMeasured = defineEvent<PerformanceINPMeasuredData>({
+  name: 'Performance INP Measured',
+  // Sample at most every minute
+  maxSampleRate: 60_000,
+  version: 1,
+  description: 'Performance INP (Interaction to Next Paint) measurement happened',
+})
diff --git a/packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts b/packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts
new file mode 100644
index 000000000000..2a3b32991dbd
--- /dev/null
+++ b/packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts
@@ -0,0 +1,12 @@
+// Tracks performance metrics from the field
+import {type PropsWithChildren} from 'react'
+
+import {useMeasurePerformanceTelemetry} from './useMeasurePerformanceTelemetry'
+
+/**
+ * @internal
+ */
+export function PerformanceTelemetryTracker(props: PropsWithChildren) {
+  useMeasurePerformanceTelemetry()
+  return props.children
+}
diff --git a/packages/sanity/src/core/studio/StudioTelemetryProvider.tsx b/packages/sanity/src/core/studio/telemetry/StudioTelemetryProvider.tsx
similarity index 89%
rename from packages/sanity/src/core/studio/StudioTelemetryProvider.tsx
rename to packages/sanity/src/core/studio/telemetry/StudioTelemetryProvider.tsx
index 837c120ed3ac..fc2ab7ae67a0 100644
--- a/packages/sanity/src/core/studio/StudioTelemetryProvider.tsx
+++ b/packages/sanity/src/core/studio/telemetry/StudioTelemetryProvider.tsx
@@ -7,9 +7,10 @@ import {TelemetryProvider} from '@sanity/telemetry/react'
 import arrify from 'arrify'
 import {type ReactNode, useEffect, useMemo} from 'react'
 
-import {type Config} from '../config'
-import {useClient} from '../hooks'
-import {SANITY_VERSION} from '../version'
+import {type Config} from '../../config'
+import {useClient} from '../../hooks'
+import {SANITY_VERSION} from '../../version'
+import {PerformanceTelemetryTracker} from './PerformanceTelemetry'
 
 const sessionId = createSessionId()
 
@@ -18,7 +19,7 @@ const DEBUG_TELEMETRY = !!(
 )
 
 /* eslint-disable no-console */
-export const debugLoggingStore: CreateBatchedStoreOptions = {
+const debugLoggingStore: CreateBatchedStoreOptions = {
   // submit any pending events every <n> ms
   flushInterval: 1000,
 
@@ -105,5 +106,9 @@ export function StudioTelemetryProvider(props: {children: ReactNode; config: Con
     })
   }, [props.config, store.logger])
 
-  return <TelemetryProvider store={store}>{props.children}</TelemetryProvider>
+  return (
+    <TelemetryProvider store={store}>
+      <PerformanceTelemetryTracker>{props.children}</PerformanceTelemetryTracker>
+    </TelemetryProvider>
+  )
 }
diff --git a/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts b/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts
new file mode 100644
index 000000000000..2b66acf5edd2
--- /dev/null
+++ b/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts
@@ -0,0 +1,75 @@
+import {useTelemetry} from '@sanity/telemetry/react'
+import {useCallback, useEffect} from 'react'
+
+import {PerformanceINPMeasured} from '../__telemetry__/performance.telemetry'
+
+function getInterestingAttrs(node: Node | null): undefined | {ui?: string; testId?: string} {
+  if (!node || !(node instanceof Element)) {
+    return undefined
+  }
+  const ui = node.getAttribute('data-ui') || undefined
+  const testId = node.getAttribute('data-testid') || undefined
+  return ui || testId ? {ui, testId} : undefined
+}
+
+function getElementIdentifier(node: Node | null) {
+  if (!node) {
+    return null
+  }
+  if (!(node instanceof Element)) {
+    return node.nodeName
+  }
+  // Note: Deliberately using classList instead of className here since className isn't always a string
+  // See https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes
+  const {nodeName, classList, id} = node
+  return (
+    nodeName.toLowerCase() +
+    (id ? `#${id}` : '') +
+    (classList ? `.${classList.value.replaceAll(' ', '.')}` : '')
+  )
+}
+
+function isPerformanceEventTiming(entry: PerformanceEntry): entry is PerformanceEventTiming {
+  return entry.entryType === 'event'
+}
+
+/**
+ * @internal
+ */
+export function useMeasurePerformanceTelemetry() {
+  const telemetry = useTelemetry()
+  const onEvent = useCallback(
+    (list: PerformanceObserverEntryList, observer: PerformanceObserver) => {
+      const entries = list.getEntries()
+
+      let maxEntry: PerformanceEventTiming | undefined = undefined
+      for (const entry of entries) {
+        if (!isPerformanceEventTiming(entry)) continue
+        if (entry.duration > (maxEntry?.duration || 0)) {
+          maxEntry = entry
+        }
+      }
+      if (!maxEntry) {
+        return
+      }
+      telemetry.log(PerformanceINPMeasured, {
+        target: getElementIdentifier(maxEntry.target),
+        attrs: getInterestingAttrs(maxEntry.target),
+        interaction: maxEntry.name,
+        duration: maxEntry.duration,
+      })
+    },
+    [telemetry],
+  )
+  useEffect(() => {
+    if (!('PerformanceObserver' in globalThis)) {
+      return
+    }
+    const observer = new PerformanceObserver(onEvent)
+    observer.observe({type: 'event', buffered: true})
+    // eslint-disable-next-line consistent-return
+    return () => {
+      observer.disconnect()
+    }
+  }, [onEvent])
+}


Reviews:
user: login: stipsan
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T11:44:35Z
user: login: bjoerge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T11:49:57Z
user: login: bjoerge
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T12:40:05Z
user: login: stipsan
body: Brilliant 💖 
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T13:36:45Z
user: login: svirs
state: APPROVED
author_association: NONE
submitted_at: 2025-03-26T15:19:14Z

Review comments:
diff_hunk: @@ -0,0 +1,65 @@
+import {useTelemetry} from '@sanity/telemetry/react'
+import {useCallback, useEffect} from 'react'
+
+import {PerformanceINPMeasured} from '../__telemetry__/performance.telemetry'
+
+function getElementIdentifier(node: Node | null) {
+  if (!node) {
+    return null
+  }
+  if (!(node instanceof Element)) {
+    return node.nodeName
+  }
+  // Note: Deliberately using classList instead of className here since className isn't always a string
+  // See https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes
+  const {nodeName, classList, id} = node
+  return (
+    nodeName.toLowerCase() +
+    (id ? `#${id}` : '') +
+    (classList ? `.${classList.value.replaceAll(' ', '.')}` : '')
path: packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts
user: login: stipsan
body: I think I'd collect these two data attributes as well:
- `data-testid`
- `data-ui`

As the `classList` coming from `styled-components` can be botched/unusable if code minification is enabled (it is by default in `sanity build` and `next build`). And `id` sometimes might be using `useId()` or some other source that is difficult to trace back.

The `data-testid` is super high value since it's already used for E2E and gives useful context as to where and what the component is.
Same is true for `data-ui` as it's used by `@sanity/ui`, and we already know that primitives like `<Tooltip>`, `<Popover>` and `<Input>` are already hot paths.
created_at: 2025-03-26T11:44:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,65 @@
+import {useTelemetry} from '@sanity/telemetry/react'
+import {useCallback, useEffect} from 'react'
+
+import {PerformanceINPMeasured} from '../__telemetry__/performance.telemetry'
+
+function getElementIdentifier(node: Node | null) {
+  if (!node) {
+    return null
+  }
+  if (!(node instanceof Element)) {
+    return node.nodeName
+  }
+  // Note: Deliberately using classList instead of className here since className isn't always a string
+  // See https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes
+  const {nodeName, classList, id} = node
+  return (
+    nodeName.toLowerCase() +
+    (id ? `#${id}` : '') +
+    (classList ? `.${classList.value.replaceAll(' ', '.')}` : '')
path: packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts
user: login: bjoerge
body: Good point. This was inspired by how it's reported in the Performance devtools panel, but agree data-test-id and data-ui is valuable for us. I do wonder if we want id and classlist at all, but feels like _some_ hints is better than none? I'll add dataUi and testId as additional props.

<img width="480" alt="image" src="https://github.com/user-attachments/assets/fbf86efe-428b-4b60-b0e9-88a391b6210e" />
created_at: 2025-03-26T11:49:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,65 @@
+import {useTelemetry} from '@sanity/telemetry/react'
+import {useCallback, useEffect} from 'react'
+
+import {PerformanceINPMeasured} from '../__telemetry__/performance.telemetry'
+
+function getElementIdentifier(node: Node | null) {
+  if (!node) {
+    return null
+  }
+  if (!(node instanceof Element)) {
+    return node.nodeName
+  }
+  // Note: Deliberately using classList instead of className here since className isn't always a string
+  // See https://developer.mozilla.org/en-US/docs/Web/API/Element/className#notes
+  const {nodeName, classList, id} = node
+  return (
+    nodeName.toLowerCase() +
+    (id ? `#${id}` : '') +
+    (classList ? `.${classList.value.replaceAll(' ', '.')}` : '')
path: packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts
user: login: bjoerge
body: Added `data-testid` and `data-ui` in  055f9254bcd9aff65bec40eea1d6743030e7acfb.

Example:
```
{
  "target": "input#myNumberField.Input-sc-h62wco-4.evDwFo",
  "attrs": {"testId":"number-input"},
  "interaction":"keydown",
  "duration":688
}
```
Thanks for the suggestion!

btw I'm leaning towards keeping class for now. Might not be super useful at the moment, but that could change in the future.
created_at: 2025-03-26T12:40:05Z
author_association: MEMBER

PR comments:
user: login: vercel[bot]
created_at: 2025-03-17T17:51:09Z
body: [vc]: #t/0LRzURl9DvHY/7Weh0oaUrhpueBTeM4FTP0ObBE1Y=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ0ZXN0LXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOm51bGwsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS9zYW5pdHktc2FuZGJveC90ZXN0LXN0dWRpby9FemROTXFETGJlNkhwRDlLWGJQbVVGY3EzUFZnIiwicHJldmlld1VybCI6InRlc3Qtc3R1ZGlvLWdpdC1wZXJmb3JtYW5jZS10ZWxlbWV0cnkuc2FuaXR5LmRldiIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ0ZXN0LXN0dWRpby1naXQtcGVyZm9ybWFuY2UtdGVsZW1ldHJ5LnNhbml0eS5kZXYifX0seyJuYW1lIjoic3R1ZGlvLXdvcmtzaG9wIiwicm9vdERpcmVjdG9yeSI6bnVsbCwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvc3R1ZGlvLXdvcmtzaG9wL0RZREVIeVlHc3YyTno3NHlxUXZXdTJrUVVLRnQiLCJuZXh0Q29tbWl0U3RhdHVzIjoiSUdOT1JFRCIsInByZXZpZXdVcmwiOiJzdHVkaW8td29ya3Nob3AtZ2l0LXBlcmZvcm1hbmNlLXRlbGVtZXRyeS5zYW5pdHkuZGV2In0seyJuYW1lIjoicGVyZm9ybWFuY2Utc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Nhbml0eS1zYW5kYm94L3BlcmZvcm1hbmNlLXN0dWRpby9BRDhzZVhjekhDeVB4dGVtUmpoSkV5cUs0djVqIiwicHJldmlld1VybCI6InBlcmZvcm1hbmNlLXN0dWRpby1naXQtcGVyZm9ybWFuY2UtdGVsZW1ldHJ5LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGVyZm9ybWFuY2Utc3R1ZGlvLWdpdC1wZXJmb3JtYW5jZS10ZWxlbWV0cnkuc2FuaXR5LmRldiJ9fSx7Im5hbWUiOiJ0ZXN0LW5leHQtc3R1ZGlvIiwicm9vdERpcmVjdG9yeSI6ImRldi90ZXN0LW5leHQtc3R1ZGlvIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6IiJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvdGVzdC1uZXh0LXN0dWRpby9Fck1tVnVWamt1cXRYZDlGak02RjRaWUxMaEdRIiwibmV4dENvbW1pdFN0YXR1cyI6IklHTk9SRUQifSx7Im5hbWUiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOiJkZXYvcGFnZS1idWlsZGluZy1zdHVkaW8iLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vc2FuaXR5LXNhbmRib3gvcGFnZS1idWlsZGluZy1zdHVkaW8vMmJ0UTZlWjJmZGlSbWViUFgzWVFTZkVCZFQ4YSIsInByZXZpZXdVcmwiOiJwYWdlLWJ1aWxkaW5nLXN0dWRpby1naXQtcGVyZm9ybWFuY2UtdGVsZW1ldHJ5LnNhbml0eS5kZXYiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoicGFnZS1idWlsZGluZy1zdHVkaW8tZ2l0LXBlcmZvcm1hbmNlLXRlbGVtZXRyeS5zYW5pdHkuZGV2In19XX0=
**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/2btQ6eZ2fdiRmebPX3YQSfEBdT8a)) | [Visit Preview](https://page-building-studio-git-performance-telemetry.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/page-building-studio-git-performance-telemetry.sanity.dev?via=pr-comment-feedback-link) | Mar 26, 2025 0:52am |
| **performance-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/performance-studio/AD8seXczHCyPxtemRjhJEyqK4v5j)) | [Visit Preview](https://performance-studio-git-performance-telemetry.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/performance-studio-git-performance-telemetry.sanity.dev?via=pr-comment-feedback-link) | Mar 26, 2025 0:52am |
| **test-studio** | ✅ Ready ([Inspect](https://vercel.com/sanity-sandbox/test-studio/EzdNMqDLbe6HpD9KXbPmUFcq3PVg)) | [Visit Preview](https://test-studio-git-performance-telemetry.sanity.dev) | 💬 [**Add feedback**](https://vercel.live/open-feedback/test-studio-git-performance-telemetry.sanity.dev?via=pr-comment-feedback-link) | Mar 26, 2025 0:52am |

<details><summary>2 Skipped Deployments</summary>

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **studio-workshop** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/studio-workshop/DYDEHyYGsv2Nz74yqQvWu2kQUKFt)) | [Visit Preview](https://studio-workshop-git-performance-telemetry.sanity.dev) |  | Mar 26, 2025 0:52am |
| **test-next-studio** | ⬜️ Ignored ([Inspect](https://vercel.com/sanity-sandbox/test-next-studio/ErMmVuVjkuqtXd9FjM6F4ZYLLhGQ)) |  |  | Mar 26, 2025 0:52am |
</details>






author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-17T17:59:19Z
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.29%</td> <td align="right">56187 / 129770</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Statements</td> <td align="right">43.29%</td> <td align="right">56187 / 129770</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Functions</td> <td align="right">47.29%</td> <td align="right">2781 / 5880</td> </tr> <tr> <td align="center">🔵</td> <td align="left">Branches</td> <td align="right">79.71%</td> <td align="right">10480 / 13147</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/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/StudioProvider.tsx">packages/sanity/src/core/studio/StudioProvider.tsx</a></td> <td align="right">98.68%</td> <td align="right">50%</td> <td align="right">100%</td> <td align="right">98.68%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/StudioProvider.tsx#L101" class="text-red">101</a></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts">packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts</a></td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="right">100%</td> <td align="left"></td> </tr> <tr> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts">packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts</a></td> <td align="right">28.84%</td> <td align="right">50%</td> <td align="right">33.33%</td> <td align="right">28.84%</td> <td align="left"><a href="https://github.com/sanity-io/sanity/blob/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts#L6-L34" class="text-red">6-34</a>, <a href="https://github.com/sanity-io/sanity/blob/055f9254bcd9aff65bec40eea1d6743030e7acfb/packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts#L43-L60" class="text-red">43-60</a></td> </tr> </tbody> </table></details>
<em>Generated in workflow <a href=https://github.com/sanity-io/sanity/actions/runs/14083328707>#32885</a> for commit <a href="https://github.com/sanity-io/sanity/commit/055f9254bcd9aff65bec40eea1d6743030e7acfb">055f925</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-03-17T17:59:44Z
body: No changes to documentation
<!-- thollander/actions-comment-pull-request "docs-report" -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-17T18:08:20Z
body: ### ⚡️ Editor Performance Report

Updated Wed, 26 Mar 2025 15:49:57 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) | 18.2 efps (55ms) | 21.7 efps (46ms) | -9ms (-16.4%) | ✅ |
| article (body) | 63.7 efps (16ms) | 63.5 efps (16ms) | +0ms (-/-%) | ✅ |
| article (string inside object) | 19.4 efps (52ms) | 22.2 efps (45ms) | -7ms (-12.6%) | ✅ |
| article (string inside array) | 17.9 efps (56ms) | 20.2 efps (50ms) | -7ms (-11.6%) | ✅ |
| recipe (name) | 43.5 efps (23ms) | 41.7 efps (24ms) | +1ms (+4.3%) | ✅ |
| recipe (description) | 45.5 efps (22ms) | 47.6 efps (21ms) | -1ms (-4.5%) | ✅ |
| recipe (instructions) | 99.9+ efps (6ms) | 99.9+ efps (5ms) | -1ms (-/-%) | ✅ |
| synthetic (title) | 19.0 efps (53ms) | 16.4 efps (61ms) | +9ms (+16.2%) | ✅ |
| synthetic (string inside object) | 19.6 efps (51ms) | 16.7 efps (60ms) | +9ms (+17.6%) | ✅ |


> **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) | 55ms | 60ms | 68ms | 209ms | 1434ms | 12.2s |
| article (body) | 16ms | 18ms | 42ms | 178ms | 212ms | 5.8s |
| article (string inside object) | 52ms | 57ms | 68ms | 138ms | 1265ms | 9.6s |
| article (string inside array) | 56ms | 61ms | 68ms | 281ms | 1426ms | 9.3s |
| recipe (name) | 23ms | 25ms | 28ms | 48ms | 2ms | 7.7s |
| recipe (description) | 22ms | 23ms | 25ms | 38ms | 0ms | 5.0s |
| recipe (instructions) | 6ms | 8ms | 11ms | 40ms | 0ms | 3.5s |
| synthetic (title) | 53ms | 56ms | 70ms | 264ms | 1265ms | 13.3s |
| synthetic (string inside object) | 51ms | 55ms | 59ms | 269ms | 923ms | 8.6s |


### 🧪 Experiment result

The performance result of this branch


| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
| --------- | ------: | --: | --: | --: | ------------: | ------------: |
| article (title) | 46ms | 49ms | 56ms | 94ms | 335ms | 11.1s |
| article (body) | 16ms | 21ms | 34ms | 235ms | 294ms | 6.1s |
| article (string inside object) | 45ms | 48ms | 55ms | 251ms | 342ms | 7.7s |
| article (string inside array) | 50ms | 52ms | 57ms | 194ms | 692ms | 8.2s |
| recipe (name) | 24ms | 26ms | 29ms | 57ms | 0ms | 7.2s |
| recipe (description) | 21ms | 22ms | 24ms | 37ms | 0ms | 4.9s |
| recipe (instructions) | 5ms | 7ms | 11ms | 31ms | 0ms | 3.3s |
| synthetic (title) | 61ms | 66ms | 93ms | 326ms | 2216ms | 14.9s |
| synthetic (string inside object) | 60ms | 64ms | 82ms | 353ms | 1808ms | 9.7s |


### 📚 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-03-21T15:20:00Z
body: ### 📊 Playwright Test Report

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

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