sanity Documentation

Sanity Studio – Rapidly configure content workspaces powered by structured content

6,229 stars TypeScript TypeScript 1 file · ~18,748 tokens #cms#headless#react#reactjs#real-time#realtime
RAW Doc

File: .agents/skills/playwright-best-practices/README.md

text
░█▀█░█░░░█▀█░█░█░█░█░█▀▄░▀█▀░█▀▀░█░█░▀█▀░░░█▀▄░█▀▀░█▀▀░▀█▀░░░█▀█░█▀▄░█▀█░█▀▀░▀█▀░▀█▀░█▀▀░█▀▀░█▀▀░
░█▀▀░█░░░█▀█░░█░░█▄█░█▀▄░░█░░█░█░█▀█░░█░░░░█▀▄░█▀▀░▀▀█░░█░░░░█▀▀░█▀▄░█▀█░█░░░░█░░░█░░█░░░█▀▀░▀▀█░
░▀░░░▀▀▀░▀░▀░░▀░░▀░▀░▀░▀░▀▀▀░▀▀▀░▀░▀░░▀░░░░▀▀░░▀▀▀░▀▀▀░░▀░░░░▀░░░▀░▀░▀░▀░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░

by currents.dev - The all-in-one Dashboard for Playwright Testing.

Playwright Best Practices Skill

A skill that gives the AI specialized guidance for writing, debugging, and maintaining Playwright tests in TypeScript. Use it in any repo where you work with Playwright so the assistant follows best practices for E2E, component, API, visual regression, accessibility, security, i18n, Electron, and browser extension testing.

Installation

bash
npx skills add https://github.com/currents-dev/playwright-best-practices-skill

The skill is activity-based: the AI is directed to the right reference depending on what you're doing, so you get focused advice without loading everything at once.

When the Skill Is Used

The skill triggers when the AI infers you need help with things like:

  • Writing new E2E, component, API, visual regression, or accessibility tests
  • Testing mobile/responsive layouts, touch gestures, or device emulation
  • Implementing file uploads/downloads, date/time mocking, or WebSocket testing
  • Handling OAuth popups, geolocation, permissions, or multi-tab flows
  • Testing iframes, canvas/WebGL, service workers, or PWA features
  • Testing Electron desktop apps or browser extensions
  • Internationalization (i18n), locales, RTL layouts, or date/number formats
  • Testing error states, offline mode, or network failure scenarios
  • Security testing (XSS, CSRF, authentication, authorization)
  • Performance testing with Web Vitals or Lighthouse
  • Reviewing or refactoring Playwright test code
  • Fixing flaky tests or debugging failures
  • Setting up CI/CD, test coverage, or global setup/teardown
  • Configuring projects, dependencies, parallel runs, or sharding

You don't have to mention "skill" or "Playwright best practices"; describe your task (e.g. "fix this flaky login test" or "add accessibility tests") and the AI will use the skill when it's relevant.

What's Inside

57 reference documents organized into 8 categories:

Core (`core/`)

Topic Reference Use for
Test structure test-suite-structure.md Structure, config, E2E/component/API/visual tests
Locators locators.md Selectors, robustness, avoiding brittle locators
Assertions & waiting assertions-waiting.md Expect APIs, auto-waiting, polling
Page Object Model page-object-model.md POM structure and patterns
Fixtures & hooks fixtures-hooks.md Setup, teardown, auth, custom fixtures
Test data test-data.md Factories, Faker, data-driven testing
Annotations annotations.md skip, fixme, slow, test steps
Configuration configuration.md playwright.config.ts options
Global setup global-setup.md globalSetup/Teardown, DB migrations
Projects projects-dependencies.md Project config, dependencies, filtering

Debugging (`debugging/`)

Topic Reference Use for
Debugging debugging.md Trace viewer, inspector, common issues
Flaky tests flaky-tests.md Detection, diagnosis, fixing, quarantine
Error testing error-testing.md Error boundaries, offline, network failures
Console errors console-errors.md Capturing and failing on JS errors

Testing Patterns (`testing-patterns/`)

Topic Reference Use for
Accessibility accessibility.md Axe-core, keyboard nav, ARIA, focus management
API testing api-testing.md REST API testing, request context
Component testing component-testing.md CT setup, mounting, props, mocking
Visual regression visual-regression.md Screenshot comparison, thresholds
File operations file-operations.md Upload, download basics
File upload/download file-upload-download.md Progress, cancellation, retry patterns
Forms validation forms-validation.md Form testing, validation states
Drag and drop drag-drop.md Drag-and-drop interactions
GraphQL testing graphql-testing.md GraphQL queries, mutations, mocking
Canvas/WebGL canvas-webgl.md Canvas testing, charts, WebGL, games
i18n i18n.md Locales, RTL, date/number formats
Electron electron.md Desktop apps, IPC, main/renderer process
Browser extensions browser-extensions.md Popup, background, content scripts, APIs
Security testing security-testing.md XSS, CSRF, auth security, authorization
Performance testing performance-testing.md Web Vitals, budgets, Lighthouse

Advanced (`advanced/`)

Topic Reference Use for
Authentication authentication.md Login flows, session storage, cookies
Auth flows authentication-flows.md MFA, password reset, complex auth
Mobile testing mobile-testing.md Device emulation, touch gestures, viewports
Clock mocking clock-mocking.md Date/time mocking, timezones, timers
Multi-context multi-context.md Popups, new tabs, OAuth flows
Multi-user multi-user.md Collaboration, RBAC, concurrent actions
Network advanced network-advanced.md GraphQL, HAR, request modification
Third-party third-party.md OAuth, payments, email/SMS mocking

Browser APIs (`browser-apis/`)

Topic Reference Use for
Browser APIs browser-apis.md Geolocation, permissions, clipboard, camera
WebSockets websockets.md Real-time testing, SSE, reconnection
iFrames iframes.md Cross-origin, nested, dynamic iframes
Service workers service-workers.md PWA, caching, offline, push notifications

Architecture (`architecture/`)

Topic Reference Use for
POM vs fixtures pom-vs-fixtures.md Choosing between patterns
Test architecture test-architecture.md Test type selection, structure
When to mock when-to-mock.md Mock vs real services decisions

Frameworks (`frameworks/`)

Topic Reference Use for
React react.md React-specific testing patterns
Angular angular.md Angular-specific testing
Vue vue.md Vue/Nuxt testing patterns
Next.js nextjs.md Next.js SSR/SSG testing

Infrastructure & CI/CD (`infrastructure-ci-cd/`)

Topic Reference Use for
CI/CD ci-cd.md Pipelines, general CI setup
GitHub Actions github-actions.md GitHub-specific workflows
GitLab CI gitlab.md GitLab-specific pipelines
Other providers other-providers.md CircleCI, Azure DevOps, Jenkins
Docker docker.md Container setup, Playwright images
Parallel/sharding parallel-sharding.md Sharding, parallel execution
Performance performance.md Parallel runs, optimization
Reporting reporting.md Test reporters, artifacts
Test coverage test-coverage.md V8 coverage, reports, thresholds, CI

The skill's SKILL.md maps your current activity to these references so the right content is used in context.

License

MIT


File: dev/efps/README.md

Editor "Frames per Second" — eFPS benchmarks

This folder contains a performance test suite for benchmarking the Sanity Studio editor and ensuring smooth performance. The suite is designed to run various tests and measure the editor's performance using the eFPS (editor Frames Per Second) metric.

Overview

The performance test suite is part of the Sanity Studio monorepo and is used to benchmark the editor's performance. It runs a series of tests on different document types and field configurations to measure the responsiveness and smoothness of the editing experience.

eFPS Metric

The eFPS (editor Frames Per Second) metric is used to quantify the performance of the Sanity Studio editor. Here's how it works:

  1. The test suite measures the time it takes for the editor to respond to user input (e.g., typing in a field).
  2. This response time is then converted into a "frames per second" analogy to provide an intuitive understanding of performance.
  3. The eFPS is calculated as: eFPS = 1000 / responseTime

We use the "frames per second" analogy because it helps us have a better intuition for what constitutes good or bad performance. Just like in video games or animations:

  • Higher eFPS values indicate smoother, more responsive performance.
  • Lower eFPS values suggest lag or sluggishness in the editor.

For example:

  • An eFPS of 60 or higher is generally considered very smooth.
  • An eFPS between 30-60 is acceptable but may show some lag.
  • An eFPS below 30 indicates noticeable performance issues.

Percentiles

The test suite reports eFPS values at different percentiles (p50, p75, and p90) for each run. Here's why we use percentiles and what they tell us:

  • p50 (50th percentile or median): This represents the typical performance. Half of the interactions were faster than this, and half were slower.
  • p75 (75th percentile): 75% of interactions were faster than this value. It gives us an idea of performance during slightly worse conditions.
  • p90 (90th percentile): 90% of interactions were faster than this value. This helps us understand performance during more challenging scenarios or edge cases.

Using percentiles allows us to:

  1. Get a more comprehensive view of performance across various conditions.
  2. Identify inconsistencies or outliers in performance.
  3. Ensure that we're not just optimizing for average cases but also for worst-case scenarios.

Test Structure

Each test in the suite has its own build. This approach offers several advantages:

  1. Isolation: Each test has its own schema and configuration, preventing interference between tests.
  2. Ease of Adding Tests: New tests can be added without affecting existing ones, making the suite more modular and maintainable.
  3. Accurate Profiling: Individual builds allow for more precise source maps, which leads to better profiling output and easier performance debugging.

Debugging

Locally

Make sure the following env vars are added to the root .env or .env.local

sh
# set to the sanity project id used for preview builds
SANITY_STUDIO_EFPS_EXPERIMENT_PROJECT_ID=
# set to sanity dataset used for preview builds (typically `production`)
SANITY_STUDIO_EFPS_EXPERIMENT_DATASET=production
# set to the api host used for preview builds (https://api.sanity.work if tests should run against staging, otherwise https://api.sanity.io)
SANITY_STUDIO_EFPS_EXPERIMENT_API_HOST=https://api.sanity.io
# set to the project id currently used for production builds in main
SANITY_STUDIO_EFPS_REFERENCE_PROJECT_ID=b8j69ts2
# set to the dataset currently used for production deployments from main (typically `production`)
SANITY_STUDIO_EFPS_REFERENCE_DATASET=production
# set to the api host used for production builds (https://api.sanity.work if production builds runs against staging, otherwise https://api.sanity.io)
SANITY_STUDIO_EFPS_REFERENCE_API_HOST=https://api.sanity.work
# Editor token for the staging project
EFPS_SANITY_TOKEN_STAGING=
# Editor token for the production project
EFPS_SANITY_TOKEN_PROD=
# Should be http://localhost:3333 locally
STUDIO_URL=http://localhost:3333

Running headful

Running the tests with the HEADLESS environment variable set to false will open Chrome and show you the tests as they are being executed.

bash
HEADLESS=false pnpm run efps:test

On CI

The eFPS GitHub workflow has a manual dispatch where you can run it from a selected branch and enable “Record video”. This adds screen recordings for the failing tests as artifacts on the workflow run which can be useful when debugging issues that may only happen on CI.

Run a manual dispatch here: https://github.com/sanity-io/sanity/actions/workflows/efps.yml

Adding a New Test

To add a new test to the suite:

  1. Create a new folder in the tests directory with your test name.
  2. Create the following files in your test folder:
    • sanity.config.ts: Define the Sanity configuration for your test.
    • sanity.types.ts: Define TypeScript types for your schema (if needed).
    • <testname>.ts: Implement your test using the defineEfpsTest function.
  3. If your test requires assets, add them to an assets subfolder.
  4. Update the tests array in index.ts to include your new test.

Example structure for a new test:

text
tests/
  newtest/
    assets/
    sanity.config.ts
    sanity.types.ts
    newtest.ts

CPU Profiles

The test suite generates CPU profiles for each test run. These profiles are remapped to the original source code, making them easier to analyze. To inspect a CPU profile:

  1. Open Google Chrome DevTools.
  2. Go to the "Performance" tab.
  3. Click on "Load profile" and select the .cpuprofile file from the results directory.

The mapped CPU profiles allow you to:

  • Identify performance bottlenecks in the original source code.
  • Analyze the time spent in different functions and components.
  • Optimize the areas of code that have the most significant impact on performance.

File: docs/CORE_CONCEPTS.md

Core Concepts

This document explains the key abstractions and mental models needed to understand and work with the Sanity Studio codebase. It is intended for developers contributing to the monorepo and for AI agents assisting with development tasks.

Table of Contents

  1. Workspace
  2. Documents
  3. Schema
  4. Perspectives
  5. Releases
  6. Tools
  7. Plugins
  8. Forms & Inputs
  9. Document Actions
  10. Presence

Workspace

A Workspace is the top-level configuration unit in Sanity Studio. It represents a complete studio environment with its own project, dataset, schema, and tools.

Key Concepts

  • Project ID & Dataset: Each workspace connects to a specific Sanity project and dataset
  • Base Path: URL path for the workspace (e.g., /myWorkspace)
  • Sources: A workspace can have multiple data sources (advanced use case)

Configuration

Workspaces are configured in sanity.config.ts:

typescript
import {defineConfig} from 'sanity'

export default defineConfig({
  name: 'default',
  title: 'My Studio',
  projectId: 'your-project-id',
  dataset: 'production',
  basePath: '/studio',

  // Schema types
  schema: {
    types: [/* ... */],
  },

  // Plugins
  plugins: [/* ... */],

  // Tools
  tools: [/* ... */],
})

Multi-Workspace Setup

For multiple workspaces, pass an array:

typescript
export default defineConfig([
  {
    name: 'production',
    basePath: '/production',
    projectId: 'xxx',
    dataset: 'production',
  },
  {
    name: 'staging',
    basePath: '/staging',
    projectId: 'xxx',
    dataset: 'staging',
  },
])

Key Types

  • WorkspaceOptions - Configuration input for a workspace
  • Workspace - The resolved workspace with all settings
  • Source - Data source within a workspace (project + dataset + schema)

Source: packages/sanity/src/core/config/types.ts


Documents

Documents are the fundamental data units in Sanity. Every piece of content is stored as a JSON document with a unique ID and type.

Document Structure

All Sanity documents have these system fields:

typescript
interface SanityDocument {
  _id: string // Unique identifier
  _type: string // Schema type name
  _createdAt: string // ISO timestamp
  _updatedAt: string // ISO timestamp
  _rev: string // Revision ID for optimistic locking
}

Draft vs Published States

Sanity uses a draft/publish model for content management:

State ID Pattern Description
Published myDocument Live content visible to end users
Draft drafts.myDocument Work-in-progress changes
Version versions.<releaseId>.myDocument Content in a release

ID Utilities

typescript
import {
  getDraftId, // 'foo' → 'drafts.foo'
  getPublishedId, // 'drafts.foo' → 'foo'
  isDraftId, // Check if ID is a draft
  isPublishedId, // Check if ID is published
  isVersionId, // Check if ID is in a release
  getVersionId, // Get version ID for a release
} from 'sanity'

Live Edit

Documents with liveEdit: true in their schema skip the draft stage—edits are published immediately.

Source: packages/@sanity/types/src/documents/types.ts, packages/sanity/src/core/util/draftUtils.ts


Schema

The schema system defines the structure of your content. It determines what document types exist, what fields they contain, and how validation works.

Schema Types

Sanity supports these primitive and complex types:

Category Types
Primitives string, number, boolean, text
Date/Time date, datetime
Complex object, array, reference, image, file
Special slug, block (Portable Text), document

Defining Schema Types

typescript
import {defineType, defineField} from 'sanity'

export const article = defineType({
  name: 'article',
  title: 'Article',
  type: 'document',
  fields: [
    defineField({
      name: 'title',
      title: 'Title',
      type: 'string',
      validation: (rule) => rule.required().max(100),
    }),
    defineField({
      name: 'content',
      title: 'Content',
      type: 'array',
      of: [{type: 'block'}],
    }),
  ],
})

Validation

Validation rules are defined per-field using the Rule API:

typescript
validation: (rule) =>
  rule
    .required()
    .min(5)
    .max(100)
    .custom((value) => (value?.includes('bad') ? 'No bad words!' : true))

Schema Compilation

At runtime, schema definitions are compiled into SchemaType objects with resolved references and validation rules. The compiled schema is accessible via ConfigContext.schema.

Key Types

  • SchemaTypeDefinition - Input definition for a schema type
  • SchemaType - Compiled schema type at runtime
  • ObjectSchemaType - Compiled object/document type with fields
  • Schema - The full compiled schema with get() and has() methods

Source: packages/@sanity/types/src/schema/types.ts


Perspectives

Perspectives control which version of documents you see when querying content. They are fundamental to the draft/publish model and the releases feature.

Perspective Types

Perspective Description Document ID Pattern
published Only published documents myDoc
drafts Drafts layered on published drafts.myDoc → myDoc
<releaseId> Release version layered on drafts versions..myDoc → drafts.myDoc → myDoc

Perspective Stack

When viewing a release, perspectives are "stacked" chronologically:

typescript
// Viewing a release scheduled for next month
perspectiveStack: ['rSummer2024', 'drafts']
// Resolution order: version → draft → published

Using Perspectives

typescript
import {usePerspective} from 'sanity'

function MyComponent() {
  const {
    selectedPerspectiveName, // 'published' | 'drafts' | releaseId
    selectedReleaseId, // undefined for published/drafts
    perspectiveStack, // Array for client queries
  } = usePerspective()
}

Key Types

  • TargetPerspective - The selected perspective (system bundle or release document)
  • PerspectiveStack - Ordered array of perspective IDs for queries
  • SystemBundle - Built-in perspectives: 'drafts' | 'published'

Source: packages/sanity/src/core/perspective/types.ts


Releases

Releases (also called Content Releases) allow grouping document changes for coordinated publishing. They are Sanity's content versioning and scheduling system.

Release States

State Description
Active Being edited, not yet published
Scheduled Set to publish at a specific time
Published All documents have been published
Archived No longer active, kept for history

Release Types

typescript
const releaseTypes = ['asap', 'scheduled', 'undecided'] as const
  • ASAP: Publish as soon as ready (no fixed time)
  • Scheduled: Publish at a specific date/time
  • Undecided: Release timing not yet determined

Release Documents

Releases are stored as special system documents:

typescript
interface ReleaseDocument {
  _id: string // e.g., '_.releases.summer-launch'
  _type: 'system.release'
  metadata: {
    title: string
    description?: string
    releaseType: 'asap' | 'scheduled' | 'undecided'
    intendedPublishAt?: string // For scheduled releases
  }
  state: 'active' | 'published' | 'archived'
}

Version Documents

Documents in a release have IDs like:

text
versions.<releaseId>.

Example: versions.rSummer2024.article-123

Key Hooks

typescript
import {
  useActiveReleases, // Get all active releases
  useReleaseOperations, // Create, update, publish, archive
  useReleasesStore, // Full release state management
} from 'sanity'

Source: packages/sanity/src/core/releases/


Tools

Tools are top-level views or "apps" within Sanity Studio. They appear in the main navigation and have their own URL routes.

Built-in Tools

  • Desk (or Structure) - Document editing interface
  • Vision - GROQ query playground
  • Scheduled Publishing - View scheduled publishes

Tool Interface

typescript
interface Tool<Options = any> {
  name: string // URL segment (e.g., 'desk')
  title: string // Display name
  icon?: ComponentType // Navigation icon
  component: ComponentType<{tool: Tool<Options>}>
  options?: Options // Custom configuration
  router?: Router // URL routing

  // Intent handling
  canHandleIntent?: (intent, params, payload) => boolean
  getIntentState?: (intent, params, routerState, payload) => unknown
}

Creating a Custom Tool

typescript
import {definePlugin} from 'sanity'

export const myTool = definePlugin({
  name: 'my-tool',
  tools: [
    {
      name: 'analytics',
      title: 'Analytics',
      icon: ChartIcon,
      component: AnalyticsDashboard,
    },
  ],
})

Intent Handling

Tools can respond to intents like "edit" or "create":

typescript
canHandleIntent: (intent, params) => {
  if (intent === 'edit' && params.type === 'article') {
    return true
  }
  return false
}

Source: packages/sanity/src/core/config/types.ts (Tool interface)


Plugins

Plugins extend Sanity Studio's functionality. They can add schema types, tools, document actions, form inputs, and more.

Creating a Plugin

typescript
import {definePlugin} from 'sanity'

export const myPlugin = definePlugin<MyPluginOptions>((options) => ({
  name: 'my-plugin',

  // Add schema types
  schema: {
    types: [myCustomType],
  },

  // Add tools
  tools: [myTool],

  // Customize document handling
  document: {
    actions: (prev, context) => [...prev, myAction],
    badges: (prev, context) => [...prev, myBadge],
  },

  // Customize forms
  form: {
    components: {
      input: MyCustomInput,
    },
  },

  // Nest other plugins
  plugins: [otherPlugin()],
}))

Using Plugins

typescript
import {defineConfig} from 'sanity'
import {myPlugin} from './plugins/myPlugin'

export default defineConfig({
  // ...
  plugins: [myPlugin({option: 'value'})],
})

Plugin Composition

Plugins can compose other plugins, and configuration options are merged recursively. Later plugins can override earlier ones.

Key Types

  • Plugin<TOptions> - Function that returns plugin options
  • PluginOptions - Configuration a plugin can provide
  • ComposableOption<TValue, TContext> - Pattern for composable config

Source: packages/sanity/src/core/config/definePlugin.ts


Forms & Inputs

The form system renders editing interfaces for documents based on their schema. It handles state management, validation, patching, and real-time collaboration.

Form Architecture

text
Document Form
├── Form State (manages document value, validation, focus)
├── Field Components (one per schema field)
│   └── Input Components (type-specific editors)
└── Patch Channel (handles changes)

Input Components

Each schema type has a corresponding input component:

Schema Type Input Component
string StringInput
number NumberInput
boolean BooleanInput
array ArrayInput
object ObjectInput
reference ReferenceInput
image ImageInput
block PortableTextInput

Custom Inputs

You can create custom inputs in schema definitions:

typescript
defineField({
  name: 'rating',
  type: 'number',
  components: {
    input: StarRatingInput, // Custom component
  },
})

Patching System

Changes to documents are expressed as patches:

typescript
import {set, unset, insert, setIfMissing} from 'sanity'

// Set a value
onChange(set('New Title', ['title']))

// Insert into array
onChange(insert([{_key: 'abc', ...}], 'after', ['items', 0]))

// Unset a field
onChange(unset(['description']))

Form Props

Input components receive standardized props:

typescript
interface InputProps {
  value: unknown // Current field value
  schemaType: SchemaType // Field's schema type
  onChange: (patch: Patch) => void
  path: Path // Path to this field
  validation: ValidationMarker[]
  presence: FormNodePresence[]
  readOnly?: boolean
  // ... and more
}

Source: packages/sanity/src/core/form/


Document Actions

Document Actions are buttons and operations that appear in the document editor's action bar. They control publishing, deletion, duplication, and other document-level operations.

Built-in Actions

Action Description
publish Publish draft to live
unpublish Remove published version
delete Delete document entirely
duplicate Create a copy
discardChanges Revert draft to published
restore Restore from history

Action Component Structure

typescript
interface DocumentActionComponent {
  (props: DocumentActionProps): DocumentActionDescription | null
  action?: string // Identifier for replacement
}

interface DocumentActionDescription {
  label: string
  icon?: ComponentType
  tone?: 'primary' | 'positive' | 'caution' | 'critical'
  disabled?: boolean
  shortcut?: string
  onHandle?: () => void
  dialog?: DocumentActionDialogProps // Show confirmation/form
}

Creating Custom Actions

typescript
function MyPublishAction(props: DocumentActionProps) {
  const {draft, published} = props

  return {
    label: 'Super Publish',
    icon: RocketIcon,
    tone: 'positive',
    disabled: !draft,
    onHandle: async () => {
      // Custom publish logic
    },
  }
}

// Replace built-in publish action
export default defineConfig({
  document: {
    actions: (prev, context) =>
      prev.map((action) => (action.action === 'publish' ? MyPublishAction : action)),
  },
})

Action Context

Actions receive context about the document state:

typescript
interface DocumentActionProps {
  id: string // Document ID
  type: string // Schema type
  draft: SanityDocument | null
  published: SanityDocument | null
  liveEdit: boolean
  versionType: 'published' | 'draft' | 'version'
  releaseId?: string // If editing in a release
}

Dialogs

Actions can show dialogs for confirmation or additional input:

typescript
return {
  label: 'Delete',
  dialog: {
    type: 'confirm',
    message: 'Are you sure?',
    onConfirm: () => deleteDocument(),
    onCancel: () => setDialogOpen(false),
  },
}

Source: packages/sanity/src/core/config/document/actions.ts


Presence

Presence is Sanity's real-time collaboration system. It shows which users are viewing or editing a document and where their cursors are located.

Presence Data

typescript
interface FormNodePresence {
  user: User // Who is present
  path: Path // Where in the document
  sessionId: string // Browser session
  lastActiveAt: string // When last active
  selection?: EditorSelection // For text editors
}

How It Works

  1. Session Tracking: Each browser session has a unique ID
  2. Location Reporting: The studio reports which document/field is focused
  3. Real-time Sync: Presence data syncs via Sanity's Bifur service
  4. Visual Indicators: Avatars and cursors show other users' locations

Using Presence

typescript
// In input components
function MyInput(props: InputProps) {
  const {presence} = props

  return (
    
      
      {/* Input content */}
    
  )
}

Presence Regions

The presence system tracks "regions" in the form—areas where users can be present:

typescript
interface FieldPresenceData {
  element: HTMLElement | null
  presence: FormNodePresence[]
  maxAvatars: number // Limit visible avatars
}

Source: packages/sanity/src/core/presence/types.ts


Quick Reference

Key Imports

typescript
import {
  // Configuration
  defineConfig,
  definePlugin,
  defineType,
  defineField,

  // Document utilities
  getDraftId,
  getPublishedId,
  isDraftId,

  // Hooks
  useClient,
  useSchema,
  usePerspective,
  useActiveReleases,

  // Form
  set,
  unset,
  insert,

  // Types
  type SanityDocument,
  type SchemaType,
  type Tool,
  type Plugin,
} from 'sanity'

Directory Structure

text
packages/sanity/src/core/
├── config/           # Configuration types and utilities
├── form/             # Form system and inputs
├── perspective/      # Perspective management
├── presence/         # Real-time collaboration
├── releases/         # Content releases
├── schema/           # Schema compilation
├── store/            # State management
└── studio/           # Studio shell and UI

packages/@sanity/types/src/
├── documents/        # Document types
├── schema/           # Schema type definitions
├── validation/       # Validation types
└── ...

Further Reading


File: docs/TELEMETRY.md

Sanity Studio Telemetry

This document describes how frontend tracking and event sending works in the Sanity Studio.

Overview

Sanity Studio uses the @sanity/telemetry package to collect anonymized usage data and performance metrics. Events are batched, enriched with studio context, and sent to Sanity's intake API. All telemetry respects user consent and can be disabled.

Architecture

text
StudioProvider
  └── StudioTelemetryProvider          # Creates batched store, enriches events with context
        └── TelemetryProvider            # React context from @sanity/telemetry/react
              └── PerformanceTelemetryTracker   # Core Web Vitals + legacy INP
                    └── [Studio children]        # Components use useTelemetry() hook

Key Files

File Purpose
packages/sanity/src/core/studio/telemetry/StudioTelemetryProvider.tsx Main provider - creates the batched store, enriches events with context
packages/sanity/src/core/studio/telemetry/types.ts TelemetryContext interface definition
packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.ts Mounts Core Web Vitals and legacy INP tracking
packages/sanity/src/core/studio/telemetry/useWebVitalsTelemetry.ts Core Web Vitals via web-vitals/attribution
packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.ts Legacy INP v1 tracking
packages/sanity/src/core/studio/MaybeEnableErrorReporting.ts Consent check for error reporting

How Events Are Defined

Events are defined using defineEvent() from @sanity/telemetry. Each event has a name, version, description, and optional typed payload and sampling rate.

typescript
import {defineEvent} from '@sanity/telemetry'

interface DocumentPublishedInfo {
  publishedImmediately: boolean
  previouslyPublished: boolean
}

export const DocumentPublished = defineEvent<DocumentPublishedInfo>({
  name: 'Document Published',
  version: 1,
})

Optional Sampling

Events can specify a maxSampleRate (in milliseconds) to throttle high-frequency metrics:

typescript
export const PerformanceINPMeasuredV2 = defineEvent({
  name: 'Performance INP Measured',
  version: 2,
  maxSampleRate: 30_000, // At most once every 30 seconds
})

Event Definition Convention

Event definitions live in __telemetry__/ directories alongside the feature code that uses them:

text
src/
  core/
    comments/__telemetry__/comments.telemetry.ts
    canvas/__telemetry__/canvas.telemetry.ts
    releases/__telemetry__/releases.telemetry.ts
    tasks/__telemetry__/tasks.telemetry.ts
    form/__telemetry__/form.telemetry.ts
    studio/__telemetry__/performance.telemetry.ts
    ...
  structure/
    documentActions/__telemetry__/documentActions.telemetry.ts
    panes/document/__telemetry__/documentPanes.telemetry.ts
    panes/documentList/__telemetry__/documentListSearch.telemetry.ts
    ...

Standard property value sets

Some interactions happen on more than one UI surface or through more than one
trigger. Rather than baking that context into the event name (which fragments a
single action across many high-cardinality names), encode it as a property on a
generic event. For those properties to be aggregatable, the values must come
from a shared, low-cardinality vocabulary.

The value sets below are canonical. When instrumenting a new event, reuse an
existing value where one fits; only introduce a new value when no existing one
describes the surface or trigger, and coordinate additions with the Data /
Analytics team so downstream dbt models and Looker dashboards stay in sync.

Owner: Studio App team (SAPP), in coordination with Data / Analytics.

`location`

The UI surface an interaction happened on.

Value Description
document_pane The main document editor pane
array_list An array-of-objects list field
nested_object_dialog A nested object edit dialog or popover (tree editing)

`position`

Where in a collection an object was created or edited. Pairs with
location: "array_list".

Value Description
new Created via the array's primary add control
appended Inserted after an existing item
prepended Inserted before an existing item
nested An existing nested item opened for editing

`path`

How a navigation or open interaction was triggered.

Value Description
breadcrumb Via a breadcrumb control
close_button Via a dialog close button
keyboard_shortcut Via a keyboard shortcut

How Events Are Sent

React Hook

Components log events using the useTelemetry() hook from @sanity/telemetry/react:

typescript
import {useTelemetry} from '@sanity/telemetry/react'
import {DocumentPublished} from './__telemetry__/documentActions.telemetry'

function MyComponent() {
  const telemetry = useTelemetry()

  const handlePublish = () => {
    telemetry.log(DocumentPublished, {
      publishedImmediately: true,
      previouslyPublished: false,
    })
  }
}

Feature-Specific Telemetry Hooks

For features with multiple events, a dedicated hook encapsulates the telemetry logic:

typescript
// useCommentsTelemetry.ts
export function useCommentsTelemetry() {
  const telemetry = useTelemetry()

  return {
    linkCopied: () => telemetry.log(CommentLinkCopied),
    viewedFromLink: () => telemetry.log(CommentViewedFromLink),
    listViewChanged: () => telemetry.log(CommentListViewChanged),
  }
}

Batching and Transport

Events are not sent immediately. They are collected in a batched store and flushed periodically:

Setting Value
Flush interval 30 seconds (production)
Flush interval (debug) 1 second
Session ID Created once per page load via createSessionId()

Two delivery methods:

  1. HTTP POST (primary) - POST /intake/batch via the Sanity client
  2. Beacon API (page unload) - navigator.sendBeacon() to /intake/batch for reliable delivery when the page is closing

Event Enrichment

Every event in a batch is enriched with a TelemetryContext object before sending:

typescript
// Payload sent to /intake/batch
{
  projectId: "abc123",
  batch: [
    {
      // Original event data (name, version, data, timestamp, etc.)
      ...event,
      // Enrichment context
      context: {
        // Static (captured once)
        userAgent: "Mozilla/5.0...",
        screen: { density: 2, height: 1080, width: 1920, innerHeight: 900, innerWidth: 1600 },
        studioVersion: "5.18.0",
        reactVersion: "19.2.3",
        environment: "production",
        connection: { effectiveType: "4g", downlink: 10, rtt: 50, saveData: false },

        // Dynamic (updated on navigation)
        orgId: "org_xyz",
        activeTool: "desk",
        workspaceCount: 2,
        activeWorkspace: "default",
        activeProjectId: "abc123",
        activeDataset: "production",
        pluginCount: 12,
        schemaTypeCount: 84,
      }
    }
  ]
}

The context is stored in a useRef so that dynamic values (workspace, tool, org) can update without re-creating the batched store.
Workspace, plugin, and schema type counts are derived from the already-resolved Studio configuration. Connection quality uses the browser Network Information API when available. None of these fields add Sanity API requests.

Telemetry is consent-gated. Before any events are sent, the studio checks the user's consent status:

text
GET /intake/telemetry-status
→ { status: "granted" | "denied" }
  • If "granted": events are sent normally
  • If "denied": events are silently dropped

This check happens once when the StudioTelemetryProvider mounts (via the resolveConsent option on the batched store).

A separate consent check exists for error reporting (MaybeEnableErrorReporting), using the same endpoint with a different tag (telemetry-consent.error-reporting).

Debug Mode

Set the environment variable to log events to the console instead of sending them:

text
SANITY_STUDIO_DEBUG_TELEMETRY=true

In debug mode:

  • Consent is auto-granted
  • Flush interval drops to 1 second
  • Events are logged to console.log with [telemetry] prefix
  • No network requests are made

Event Categories

Performance (Core Web Vitals)

Tracked automatically via web-vitals/attribution library:

Event Metric Version
Performance LCP Measured Largest Contentful Paint v2
Performance FCP Measured First Contentful Paint v2
Performance CLS Measured Cumulative Layout Shift v2
Performance TTFB Measured Time to First Byte v2
Performance INP Measured Interaction to Next Paint v1 (legacy) + v2

Document Actions

Event When
Document Published Publish action completes
Publish Button Clicked Publish operation stages (started/completed/failed)
Publish Button Becomes Disabled - Started/Completed Publish button state transitions

Releases

Event When
Version Document Added to Release Document added to a release
Release Created/Deleted/Published Release lifecycle
Release Scheduled/Unscheduled Release scheduling
Release Archived/Unarchived Release archival
Release Reverted/Duplicated Release management
Release Description Set Release description usage - set at creation or edited in Studio (action, character count, contains-URL; never the content)
Release Link/ID/Title Copied Clipboard actions
Navigated to Releases Overview Navigation
Navigated to Scheduled Drafts Navigation

Scheduled Drafts

Scheduled drafts (cardinality-one releases) emit their own events so their usage can be distinguished from content releases. Each is logged from useScheduleDraftOperations after its operation succeeds.

Event When Payload
Scheduled Draft Created A draft is scheduled for publishing none
Scheduled Draft Rescheduled A scheduled draft's publish time is changed { fromPaused }
Scheduled Draft Cancelled A scheduled draft is cancelled { keptAsDraft }

Comments

Event When
Comment Link Copied Comment link copied to clipboard
Comment Viewed From Link Comment opened via shared link
Comment List View Changed View mode toggled

Tasks

Event When
Task Created/Duplicated/Removed Task lifecycle
Task Status Changed Task state changes
Task Link Copied/Opened Task sharing
Event When
Recent Search Clicked User clicks a recent search
Document List Load Time Measured Search performance (sampled)

Canvas

Event When
Canvas Opened Canvas opened
Canvas Link CTA Clicked/Redirected Canvas link interactions
Canvas Unlink CTA Clicked/Approved Canvas unlinking

Form Interactions

Event When
Portable Text Input Expanded/Collapsed PTE editor state
Portable Text Invalid Value Ignore/Resolve PTE error handling
Created Draft New draft creation

Divergences

A divergence session starts on the first Inspected Divergence event for a document and lasts while the document stays open with unresolved divergences. Each studio pane, browser tab, and device tracks its own session, so one user can hold parallel sessions for the same document. Both Inspected Divergence and Acted On Divergence carry the session id, so BigQuery can join the resolution funnel per session.

Event When
Inspected Divergence User views a divergence in a single node
Acted On Divergence User resolves a divergence. Payload carries action: 'take-upstream-value' | 'mark-resolved'
Workspace Features Observed Fires once per active workspace, re-emitting on workspace switch, with a low-cardinality snapshot of the resolved feature flags (releases, tasks, scheduled drafts/publishing, media library, canvas, variants, document group inventory, events API, announcements, drafts, partial indexing, direct uploads, search strategy, advanced version control). Values are read straight off the resolved workspace; an absent field means the studio default applies.

Other

  • Copy/Paste - Document ID and URL copied
  • Upsell dialogs - Free trial and feature upsell interactions
  • Studio announcements - Announcement views and interactions
  • Request permission dialogs - Permission request flows
  • Document out-of-sync - Divergence and conflict events
  • Document pair loading - Loading performance metrics
  • Listener latency - Real-time listener performance
  • Nested object editing - Tree-editing interactions
  • Draft live edit banner - Banner interactions
  • Focus events - Document panel focus tracking

Adding a New Tracked Event

  1. Create the event definition in a __telemetry__/ directory alongside your feature:

    typescript
    // src/core/myFeature/__telemetry__/myFeature.telemetry.ts
    import {defineEvent} from '@sanity/telemetry'
    
    interface MyEventData {
      actionType: string
    }
    
    export const MyFeatureUsed = defineEvent<MyEventData>({
      name: 'My Feature Used',
      version: 1,
    })
  2. Log the event from your component:

    typescript
    import {useTelemetry} from '@sanity/telemetry/react'
    import {MyFeatureUsed} from './__telemetry__/myFeature.telemetry'
    
    function MyFeature() {
      const telemetry = useTelemetry()
    
      const handleAction = (type: string) => {
        telemetry.log(MyFeatureUsed, {actionType: type})
      }
    }
  3. For features with multiple events, consider creating a dedicated telemetry hook (e.g., useMyFeatureTelemetry()) to encapsulate all event logging for that feature.

Testing

The telemetry provider is tested via mocks in:
packages/sanity/src/core/studio/telemetry/__tests__/StudioTelemetryProvider.test.tsx

In unit tests, @sanity/telemetry and @sanity/telemetry/react are mocked. Components that use useTelemetry() will get a no-op logger by default.


File: e2e/README.md

E2E Testing in the Studio

Required Env Variables

The tests expects to find the below env variables. Either define it in your shell, or add it to the .env.local file in the repository root.

  • SANITY_E2E_SESSION_TOKEN: Before you get started with writing and running tests, you need to get hold of a token - either using your own Sanity user token (sanity debug --secrets will give you the CLI token provided you are logged in sanity login), or by creating a project API token using https://sanity.io/manage.
  • SANITY_E2E_PROJECT_ID: Project ID of the studio
  • SANITY_E2E_DATASET: Dataset name of the studio

Make sure that the project that you are running in the e2e tests allows the origin of your local studio (this is default http://localhost:3339) in https://sanity.io/manage > your project > API > Add CORS origin

Running tests

To run E2E tests run the following commands from the root of the project

  • Run all the tests

    sh
    pnpm test:e2e
  • Run all tests in specific directory, it runs relative to the e2e/tests

    sh
    pnpm test:e2e tests/default-layout
  • Run files that have my-spec or my-spec-2 in the file name

    sh
    pnpm test:e2e my-spec my-spec-2
  • For help, run

    sh
    pnpm test:e2e --help

Other useful helper commands

  • "e2e:dev": Starts the E2E studio using sanity dev
  • "e2e:build": Runs sanity build on E2E studio
  • "e2e:codegen": Runs playwright codegen. Note: Requires the studio to be running. Run pnpm e2e:dev in another terminal first
  • "e2e:start": Runs sanity preview on E2E studio (preview server, requires a build)
  • "e2e:preview": Runs sanity build, then sanity preview on E2E studio

For more useful commands, see the Playwright Command Line documentation.

Running tests from your code editor

You can run your tests in your editor with the help of some useful editor plugins/extensions. For example, you can download Playwright Test for VSCode from Microsoft to show and run your tests in VSCode.


File: examples/functions/telegram-notify/README.md

Telegram notifications when comments are submitted

Explore all examples

Problem

Content authors need to respond quickly to comments on their content.

Solution

This Sanity Function sends a Telegram notification when a comment is posted, with a helpful link to open the comment in your Sanity Studio.

Benefits

  • Simplifies content operations by providing a one-click way to enter the Studio
  • Improves content velocity by allowing authors to respond to comments quickly

Requirements

  • A Sanity project with Functions enabled
  • A schema with a comment document type containing:
    • A comment field (string or text) for storing the comment content
  • Node.js v22.x for local development

Schema Requirements

This function expects your schema to include a comment document type with:

  • comment field - A string or text field containing the comment content
  • The document type must be named comment

Example schema definition:

ts
import {defineType} from 'sanity'

export const comment = defineType({
  name: 'comment',
  title: 'Comment',
  type: 'document',
  fields: [
    {
      name: 'comment',
      title: 'Comment',
      type: 'text', // or 'string'
      validation: (rule) => rule.required(),
    },
    // Add other fields as needed
  ],
})

Usage Example

When a new comment type document is created, the function automatically:

  1. Triggers on the on create event for comment type documents
  2. Checks if the comment field is defined (using filter)
  3. Sends a Telegram notification with a link to the comment in the Sanity Studio

Result: Content authors get a notification with a link to the comment in the Sanity Studio.

Implementation

  1. Get your Telegram bot token

Visit https://core.telegram.org/bots#how-do-i-create-a-bot for more information.

  • Message /newbot to @BotFather on Telegram to register your bot and receive its authentication token.
  • Give your bot a name (e.g., "Sanity Content Notifications")
  • Copy the bot token
  1. Get your Telegram chat ID
  • Open a chat with your bot in Telegram
  • Send any message to your bot
  • Open the following URL in your browser, replacing <YOUR_BOT_TOKEN> with your bot token
text
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates

The response will be a JSON object containing your Chat ID.

  1. Prepare your environment variables
  • Create a .env file in the root of your project
  • Add your bot token and chat ID to the .env file
env
TELEGRAM_BOT_TOKEN=<YOUR_BOT_TOKEN>
TELEGRAM_CHAT_ID=<YOUR_CHAT_ID>
STUDIO_URL=http://localhost:3333
  1. Initialize the example

Run this if you haven't initialized blueprints:

bash
npx sanity blueprints init

You'll be prompted to select your organization and Sanity studio.

Then run:

bash
npx sanity blueprints add function --example telegram-notify
  1. Add configuration to your blueprint
ts
// sanity.blueprint.ts
import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'
import process from 'node:process'

const {TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID} = process.env
if (!TELEGRAM_BOT_TOKEN || !TELEGRAM_CHAT_ID) {
  throw new Error('TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set')
}

export default defineBlueprint({
  // ...all other settings
  resources: [
    //...all other functions
    defineDocumentFunction({
      name: 'comment-telegram',
      event: {
        on: ['create'],
        filter: '_type == "comment" && defined(comment)',
        projection: '{_id, comment}',
      },
      env: {TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID},
    }),
  ],
})
  1. Install dependencies
bash
npm install

Testing the function locally

You can test the telegram-notify function locally using the Sanity CLI before deploying:

1. Basic Function Test

Test with the included sample document:

bash
npx sanity functions test telegram-notify --file document.json

2. Interactive Development Mode

Start the development server for interactive testing:

bash
npx sanity functions dev

3. Test with Custom Data

Test with your own document data:

bash
npx sanity functions test telegram-notify --data '{
  "_type": "comment",
  "_id": "test-comment-123",
  "comment": "Loved this article, great read!"
}'

4. Test with Real Document Data

Capture a real document from your dataset:

bash
# Export a real document for testing
npx sanity documents get "your-comment-id" > document.json

# Test with the real document
npx sanity functions test telegram-notify --file document.json

Testing Tips

  • Use Node.js v22.x locally to match production runtime
  • Test with real Telegram tokens - The function requires valid credentials to work
  • Check function logs in CLI output for debugging

Deploying your function

Once you've tested your function locally and are satisfied with its behavior, you can deploy it to production.

Important: Make sure you have the Deploy Studio permission for your Sanity project before attempting to deploy.

Prerequisites for deployment

  • Sanity CLI v3.92.0 or later
  • Deploy Studio permissions for your Sanity project
  • Node.js v22.x (matches production runtime)
  • Valid Telegram bot token and chat ID

Deploy to production

  1. Deploy your blueprint

From your project root, run:

bash
npx sanity blueprints deploy

This command will:

  • Package your function code
  • Upload it to Sanity's infrastructure
  • Configure the event triggers for new comment creation
  • Make your telegram-notify function live in production
  • Deploy .env file in the root directory if it exists
  1. If not using an .env file, add environment variables manually

After deployment, if you're not using a .env file in your projects root, you need to add your Telegram credentials as environment variables:

bash
npx sanity functions env add comment-telegram TELEGRAM_BOT_TOKEN "your-telegram-bot-token-here"
npx sanity functions env add comment-telegram TELEGRAM_CHAT_ID "your-chat-id-here"
npx sanity functions env add comment-telegram STUDIO_URL "https://your-studio.sanity.studio"

You can verify the environment variables were added successfully:

bash
npx sanity functions env list comment-telegram
  1. Verify deployment

After deployment, you can verify your function is active by:

  • Creating a new comment and confirming Telegram notifications are sent
  • Monitoring function logs in the CLI
bash
npx sanity functions logs comment-telegram
  • Slack Notify - Send a Slack notification when a document is published

File: examples/functions/stale-products-analysis/README.md

Stale Products Analysis Function

Explore all examples

Problem

E-commerce teams need to monitor the freshness of products featured on their pages to ensure they're showcasing current and relevant items. Manually tracking product ages and identifying stale content across page modules is time-consuming and often overlooked, leading to outdated product displays that can negatively impact user experience and sales.

Solution

This Sanity Function automatically analyzes the age of products referenced in page modules whenever a page is updated. It calculates how long products have been in the system, identifies stale products (older than 30 days), and stores this analysis data directly on the page document for easy monitoring and reporting.

Benefits

  • Automated freshness monitoring by calculating product ages automatically
  • Stale content identification by flagging products older than 30 days
  • Data-driven decisions with comprehensive age analysis and statistics
  • Page optimization by providing insights to keep featured content fresh
  • Scalable monitoring that works across all page product references

Compatible Templates

This function is built to be compatible with the Sanity E-commerce Shopify template. It works specifically well with page documents that have modules containing product references in a grid layout structure. Install this template with: npm create sanity@latest -- --template "shopify"

Adding required schema field to your project

You'll need to add a productAgeAnalysis field to your page document schema:

text
/* Detailed source-code truncated for AI context efficiency. */

Implementation

Important: Run these commands from the root of your project (not inside the studio/ folder).

  1. Initialize the example

    For a new project:

    bash
    npx sanity blueprints init --example stale-products-analysis

    For an existing project:

    bash
    npx sanity blueprints add function --example stale-products-analysis

    You'll be prompted to select your organization and Sanity studio.

  2. Add configuration to your blueprint

    ts
    // sanity.blueprint.ts
    import {defineBlueprint, defineDocumentFunction} from '@sanity/blueprints'
    
    export default defineBlueprint({
      resources: [
        defineDocumentFunction({
          name: 'stale-products-analysis',
          memory: 1,
          timeout: 30,
          src: './functions/stale-products-analysis',
          event: {
            on: ['create', 'update'],
            filter:
              "_type == 'page' && (delta::changedAny(modules) || (delta::operation() == 'create' && defined(modules)))",
            projection: '{_id, _type, modules}',
          },
        }),
      ],
    })
  3. Install dependencies

    Install dependencies in the project root:

    bash
    npm install

    And install function dependencies:

    bash
    npm install @sanity/functions
    cd functions/stale-products-analysis
    npm install
    cd ../..
  4. Add the required schema field

    Follow the instructions in the "Compatible Templates" section above to add the productAgeAnalysis field to your page document schema.

  5. Deploy your schema

    From the studio folder, deploy your updated schema:

    bash
    # From the studio/ folder (adjust path as needed for template structure)
    cd studio
    npx sanity schema deploy
    cd ..

Testing the function locally

You can test the stale-products-analysis function locally using the Sanity CLI before deploying it to production.

Important: Document functions require that the document ID used in testing actually exists in your dataset. The examples below show how to work with real document IDs.

1. Basic Function Test

Since document functions require the document ID to exist in your dataset, create a test page first:

bash
# From the studio/ folder, create a test page with product references
cd studio
cat > test-page.json << EOF
{
  "_type": "page",
  "title": "Test Page with Products",
  "modules": [
    {
      "_type": "grid",
      "_key": "test-grid",
      "items": [
        {
          "_type": "productReference",
          "_key": "test-product-ref",
          "productWithVariant": {
            "product": {
              "_ref": "existing-product-id",
              "_type": "reference"
            }
          }
        }
      ]
    }
  ]
}
EOF

npx sanity documents create test-page.json --replace

# Back to project root for function testing
cd ..
npx sanity functions test stale-products-analysis --file studio/test-page.json --dataset production --with-user-token

Alternative: Test with a real page from your dataset:

bash
# From the studio/ folder, find and export an existing page
cd studio
npx sanity documents query "*[_type == 'page'][0]" > ../real-page.json

# Back to project root for function testing
cd ..
npx sanity functions test stale-products-analysis --file real-page.json --dataset production --with-user-token

2. Interactive Development Mode

Start the development server for interactive testing:

bash
npx sanity functions dev

This opens an interactive playground where you can test functions with custom data.

3. Test with Custom Data

For custom data testing, you still need to use a real document ID that exists in your dataset:

bash
# From the studio/ folder, create or find a real document ID
cd studio
REAL_DOC_ID=$(npx sanity documents query "*[_type == 'page'][0]._id" | tr -d '"')

# Create a temporary JSON file with custom data in project root
cd ..
cat > test-custom-page.json << EOF
{
  "_type": "page",
  "_id": "$REAL_DOC_ID",
  "title": "Custom Test Page",
  "modules": [
    {
      "_type": "grid",
      "_key": "custom-grid",
      "items": [
        {
          "_type": "productReference",
          "_key": "custom-product-ref",
          "productWithVariant": {
            "product": {
              "_ref": "existing-product-id",
              "_type": "reference"
            }
          }
        }
      ]
    }
  ]
}
EOF

# Test with the custom data file
npx sanity functions test stale-products-analysis --file test-custom-page.json --dataset production --with-user-token

4. Test with Real Document Data

The most reliable approach is to test with existing pages from your dataset:

bash
# From the studio/ folder, find and export a page that has product modules
cd studio
npx sanity documents query "*[_type == 'page' && defined(modules)][0]" > ../test-real-page.json

# Back to project root for function testing
cd ..
npx sanity functions test stale-products-analysis --file test-real-page.json --dataset production --with-user-token

5. Enable Debugging

The function includes comprehensive logging. Check the output for:

typescript
// Function logs include:
console.log('📄 Page Product Age Analysis Function called at', new Date().toISOString())
console.log('🔍 Analyzing product ages for page:', _id)
console.log(
  '📦 Found products:',
  uniqueProducts.map((p) => p._id),
)
console.log('📈 Product age analysis:', {totalProducts, oldProducts, averageAge})

Testing Tips

  • Use real document IDs - Document functions require IDs that exist in your dataset
  • Query for test documents - Use npx sanity documents query to find suitable test documents
  • Use Node.js v22.x locally to match production runtime
  • Test with pages that have product references to see the full functionality
  • Check function logs for detailed processing information
  • Verify analysis results in your Sanity Studio after function execution
  • Create test content - If you don't have suitable pages, create some test pages with product references first

Requirements

  • A Sanity project with Functions enabled
  • The Sanity E-commerce template with schema types:
    • page document type with modules field (existing)
    • product document type (existing)
    • Updated page schema with productAgeAnalysis field (add using schema above)
  • Page documents with modules containing product references in grid layout
  • Node.js v22.x for local development

Usage Example

When a page document is created or updated, the function automatically:

  1. Triggers on create/update events for page documents
  2. Extracts product references from grid modules
  3. Analyzes product ages based on creation and update dates
  4. Calculates age statistics and identifies stale products
  5. Updates the page with comprehensive age analysis data

Sample input document:

json
{
  "_type": "page",
  "_id": "page-123",
  "title": "Page with Product Grid",
  "modules": [
    {
      "_type": "grid",
      "items": [
        {
          "_type": "productReference",
          "productWithVariant": {
            "product": {
              "_ref": "product-123",
              "_type": "reference"
            }
          }
        }
      ]
    }
  ]
}

Result:

  • Analyzes all referenced products for their age
  • Flags products older than 30 days as stale
  • Stores analysis data in productAgeAnalysis field on page
  • Provides statistics like total products, old products count, and average age

Customization

Modify Stale Product Threshold

Update the age threshold by modifying the stale product logic:

typescript
// Change the stale product threshold from 30 to 60 days
const isOld = createdAgeInDays > 60

// Or use a different calculation based on last update
const isOld = updatedAgeInDays > 14 // 2 weeks since last update

Add Additional Analysis Metrics

Extend the analysis to include more metrics:

typescript
const productAgeAnalysis = uniqueProducts.map((product) => {
  // ... existing logic

  return {
    // ... existing fields
    isVeryOld: createdAgeInDays > 90, // Flag very old products
    category: getProductCategory(product), // Add category analysis
    lastModifiedBy: product.lastModifiedBy, // Track who last modified
    stalenessScore: calculateStalenessScore(createdAgeInDays, updatedAgeInDays),
  }
})

Process Different Module Types

Extend the function to analyze products in different module types:

typescript
page.modules?.forEach((module) => {
  // Handle different module types
  if (module._type === 'grid' && module.items) {
    // ... existing grid logic
  } else if (module._type === 'carousel' && module.products) {
    // Handle carousel modules with direct product references
    module.products.forEach((productRef) => {
      if (productRef._ref) {
        // Add to products array
      }
    })
  }
})

Custom Age Calculations

Implement different age calculation strategies:

typescript
// Weight creation vs update dates differently
const weightedAge = createdAgeInDays * 0.7 + updatedAgeInDays * 0.3

// Use different thresholds for different product types
const getAgeThreshold = (product: ProductWithDates) => {
  if (product.store?.productType === 'seasonal') return 14 // 2 weeks for seasonal
  if (product.store?.productType === 'evergreen') return 90 // 3 months for evergreen
  return 30 // Default 30 days
}

Troubleshooting

Common Issues

Error: "Page not found"

  • Cause: The page document doesn't exist in the dataset
  • Solution: Ensure the page document exists and has the correct _id

Error: "No product references found in page modules"

  • Cause: The page doesn't have grid modules with product references
  • Solution: Add product references to your page modules or check the module structure

Function processes page but no analysis data appears

  • Cause: Products referenced in modules don't exist or lack required date fields
  • Solution: Ensure referenced products exist and have valid _createdAt and _updatedAt fields

Analysis shows incorrect ages

  • Cause: Product date fields contain invalid or missing data
  • Solution: Check that product documents have proper _createdAt and _updatedAt timestamps

Function times out during execution

  • Cause: Page has too many product references or complex module structure
  • Solution: Increase the timeout value in your blueprint configuration or optimize the query