File: .agents/skills/playwright-best-practices/README.md
░█▀█░█░░░█▀█░█░█░█░█░█▀▄░▀█▀░█▀▀░█░█░▀█▀░░░█▀▄░█▀▀░█▀▀░▀█▀░░░█▀█░█▀▄░█▀█░█▀▀░▀█▀░▀█▀░█▀▀░█▀▀░█▀▀░
░█▀▀░█░░░█▀█░░█░░█▄█░█▀▄░░█░░█░█░█▀█░░█░░░░█▀▄░█▀▀░▀▀█░░█░░░░█▀▀░█▀▄░█▀█░█░░░░█░░░█░░█░░░█▀▀░▀▀█░
░▀░░░▀▀▀░▀░▀░░▀░░▀░▀░▀░▀░▀▀▀░▀▀▀░▀░▀░░▀░░░░▀▀░░▀▀▀░▀▀▀░░▀░░░░▀░░░▀░▀░▀░▀░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░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
npx skills add https://github.com/currents-dev/playwright-best-practices-skillThe 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:
- The test suite measures the time it takes for the editor to respond to user input (e.g., typing in a field).
- This response time is then converted into a "frames per second" analogy to provide an intuitive understanding of performance.
- 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:
- Get a more comprehensive view of performance across various conditions.
- Identify inconsistencies or outliers in performance.
- 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:
- Isolation: Each test has its own schema and configuration, preventing interference between tests.
- Ease of Adding Tests: New tests can be added without affecting existing ones, making the suite more modular and maintainable.
- 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
# 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:3333Running 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.
HEADLESS=false pnpm run efps:testOn 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:
- Create a new folder in the
testsdirectory with your test name. - 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 thedefineEfpsTestfunction.
- If your test requires assets, add them to an
assetssubfolder. - Update the
testsarray inindex.tsto include your new test.
Example structure for a new test:
tests/
newtest/
assets/
sanity.config.ts
sanity.types.ts
newtest.tsCPU 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:
- Open Google Chrome DevTools.
- Go to the "Performance" tab.
- Click on "Load profile" and select the
.cpuprofilefile from theresultsdirectory.
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
- Workspace
- Documents
- Schema
- Perspectives
- Releases
- Tools
- Plugins
- Forms & Inputs
- Document Actions
- 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:
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:
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 workspaceWorkspace- The resolved workspace with all settingsSource- 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:
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
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
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:
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 typeSchemaType- Compiled schema type at runtimeObjectSchemaType- Compiled object/document type with fieldsSchema- The full compiled schema withget()andhas()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:
// Viewing a release scheduled for next month
perspectiveStack: ['rSummer2024', 'drafts']
// Resolution order: version → draft → publishedUsing Perspectives
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 queriesSystemBundle- 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
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:
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:
versions.<releaseId>.Example: versions.rSummer2024.article-123
Key Hooks
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
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
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":
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
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
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 optionsPluginOptions- Configuration a plugin can provideComposableOption<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
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:
defineField({
name: 'rating',
type: 'number',
components: {
input: StarRatingInput, // Custom component
},
})Patching System
Changes to documents are expressed as patches:
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:
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
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
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:
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:
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
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
- Session Tracking: Each browser session has a unique ID
- Location Reporting: The studio reports which document/field is focused
- Real-time Sync: Presence data syncs via Sanity's Bifur service
- Visual Indicators: Avatars and cursors show other users' locations
Using Presence
// 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:
interface FieldPresenceData {
element: HTMLElement | null
presence: FormNodePresence[]
maxAvatars: number // Limit visible avatars
}Source: packages/sanity/src/core/presence/types.ts
Quick Reference
Key Imports
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
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
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() hookKey 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.
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:
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:
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 withlocation: "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:
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:
// 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:
- HTTP POST (primary) -
POST /intake/batchvia the Sanity client - Beacon API (page unload) -
navigator.sendBeacon()to/intake/batchfor reliable delivery when the page is closing
Event Enrichment
Every event in a batch is enriched with a TelemetryContext object before sending:
// 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.
Consent
Telemetry is consent-gated. Before any events are sent, the studio checks the user's consent status:
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:
SANITY_STUDIO_DEBUG_TELEMETRY=trueIn debug mode:
- Consent is auto-granted
- Flush interval drops to 1 second
- Events are logged to
console.logwith[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 |
Search
| 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
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, })Log the event from your component:
typescriptimport {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}) } }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 --secretswill give you the CLI token provided you are logged insanity login), or by creating a project API token using https://sanity.io/manage.SANITY_E2E_PROJECT_ID: Project ID of the studioSANITY_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
shpnpm test:e2eRun all tests in specific directory, it runs relative to the
e2e/testsshpnpm test:e2e tests/default-layoutRun files that have my-spec or my-spec-2 in the file name
shpnpm test:e2e my-spec my-spec-2For help, run
shpnpm test:e2e --help
Other useful helper commands
- "e2e:dev": Starts the E2E studio using
sanity dev - "e2e:build": Runs
sanity buildon E2E studio - "e2e:codegen": Runs playwright codegen. Note: Requires the studio to be running. Run
pnpm e2e:devin another terminal first - "e2e:start": Runs
sanity previewon E2E studio (preview server, requires a build) - "e2e:preview": Runs
sanity build, thensanity previewon 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
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
commentdocument type containing:- A
commentfield (stringortext) for storing the comment content
- A
- Node.js v22.x for local development
Schema Requirements
This function expects your schema to include a comment document type with:
commentfield - Astringortextfield containing the comment content- The document type must be named
comment
Example schema definition:
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:
- Triggers on the on create event for
commenttype documents - Checks if the
commentfield is defined (using filter) - 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
- Get your Telegram bot token
Visit https://core.telegram.org/bots#how-do-i-create-a-bot for more information.
- Message
/newbotto @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
- 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
https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdatesThe response will be a JSON object containing your Chat ID.
- Prepare your environment variables
- Create a
.envfile in the root of your project - Add your bot token and chat ID to the
.envfile
TELEGRAM_BOT_TOKEN=<YOUR_BOT_TOKEN>
TELEGRAM_CHAT_ID=<YOUR_CHAT_ID>
STUDIO_URL=http://localhost:3333- Initialize the example
Run this if you haven't initialized blueprints:
npx sanity blueprints initYou'll be prompted to select your organization and Sanity studio.
Then run:
npx sanity blueprints add function --example telegram-notify- Add configuration to your blueprint
// 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},
}),
],
})- Install dependencies
npm installTesting 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:
npx sanity functions test telegram-notify --file document.json2. Interactive Development Mode
Start the development server for interactive testing:
npx sanity functions dev3. Test with Custom Data
Test with your own document data:
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:
# 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.jsonTesting 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
- Deploy your blueprint
From your project root, run:
npx sanity blueprints deployThis 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
- 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:
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:
npx sanity functions env list comment-telegram- 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
npx sanity functions logs comment-telegramRelated Examples
- Slack Notify - Send a Slack notification when a document is published
File: examples/functions/stale-products-analysis/README.md
Stale Products Analysis Function
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:
/* Detailed source-code truncated for AI context efficiency. */Implementation
Important: Run these commands from the root of your project (not inside the studio/ folder).
Initialize the example
For a new project:
bashnpx sanity blueprints init --example stale-products-analysisFor an existing project:
bashnpx sanity blueprints add function --example stale-products-analysisYou'll be prompted to select your organization and Sanity studio.
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}', }, }), ], })Install dependencies
Install dependencies in the project root:
bashnpm installAnd install function dependencies:
bashnpm install @sanity/functions cd functions/stale-products-analysis npm install cd ../..Add the required schema field
Follow the instructions in the "Compatible Templates" section above to add the
productAgeAnalysisfield to your page document schema.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:
# 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-tokenAlternative: Test with a real page from your dataset:
# 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-token2. Interactive Development Mode
Start the development server for interactive testing:
npx sanity functions devThis 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:
# 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-token4. Test with Real Document Data
The most reliable approach is to test with existing pages from your dataset:
# 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-token5. Enable Debugging
The function includes comprehensive logging. Check the output for:
// 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 queryto 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:
pagedocument type withmodulesfield (existing)productdocument type (existing)- Updated
pageschema withproductAgeAnalysisfield (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:
- Triggers on create/update events for page documents
- Extracts product references from grid modules
- Analyzes product ages based on creation and update dates
- Calculates age statistics and identifies stale products
- Updates the page with comprehensive age analysis data
Sample input document:
{
"_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
productAgeAnalysisfield 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:
// 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 updateAdd Additional Analysis Metrics
Extend the analysis to include more metrics:
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:
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:
// 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
_createdAtand_updatedAtfields
Analysis shows incorrect ages
- Cause: Product date fields contain invalid or missing data
- Solution: Check that product documents have proper
_createdAtand_updatedAttimestamps
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
Related Examples
- Auto-Tag Function - Automatically generate tags for content using AI
- Slack Notify Function - Send notifications when stale products are detected
- Product Mapping Function - Automatically organize products based on Shopify tags