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

```

░█▀█░█░░░█▀█░█░█░█░█░█▀▄░▀█▀░█▀▀░█░█░▀█▀░░░█▀▄░█▀▀░█▀▀░▀█▀░░░█▀█░█▀▄░█▀█░█▀▀░▀█▀░▀█▀░█▀▀░█▀▀░█▀▀░
░█▀▀░█░░░█▀█░░█░░█▄█░█▀▄░░█░░█░█░█▀█░░█░░░░█▀▄░█▀▀░▀▀█░░█░░░░█▀▀░█▀▄░█▀█░█░░░░█░░░█░░█░░░█▀▀░▀▀█░
░▀░░░▀▀▀░▀░▀░░▀░░▀░▀░▀░▀░▀▀▀░▀▀▀░▀░▀░░▀░░░░▀▀░░▀▀▀░▀▀▀░░▀░░░░▀░░░▀░▀░▀░▀░▀▀▀░░▀░░▀▀▀░▀▀▀░▀▀▀░▀▀▀░
```

by [currents.dev](https://currents.dev?utm_source=ai-skill) - 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:

```
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](#workspace)
2. [Documents](#documents)
3. [Schema](#schema)
4. [Perspectives](#perspectives)
5. [Releases](#releases)
6. [Tools](#tools)
7. [Plugins](#plugins)
8. [Forms & Inputs](#forms--inputs)
9. [Document Actions](#document-actions)
10. [Presence](#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:

```
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

```
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

```
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

- [Sanity Documentation](https://www.sanity.io/docs)
- [Schema Types Reference](https://www.sanity.io/docs/schema-types)
- [Plugin Development](https://www.sanity.io/docs/plugin-development)
- [GROQ Query Language](https://www.sanity.io/docs/groq)

---

## 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() 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:

```
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.

## 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=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       |

### 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

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](https://playwright.dev/docs/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](https://playwright.dev/docs/test-cli) 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](https://github.com/sanity-io/sanity/tree/main/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](https://core.telegram.org/bots) for more information.

- Message `/newbot` to [@BotFather on Telegram](https://t.me/botfather) to register your bot and receive its authentication token.
- Give your bot a name (e.g., "Sanity Content Notifications")
- Copy the bot token

2. **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>/getUpdates
```

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

3. **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
```

4. **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
```

5. **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},
    }),
  ],
})
```

6. **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

2. **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
```

4. **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
```

## Related Examples

- [Slack Notify](../slack-notify/README.md) - Send a Slack notification when a document is published

---

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

# Stale Products Analysis Function

[Explore all examples](https://github.com/sanity-io/sanity/tree/main/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:

```
/* 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

## Related Examples

- [Auto-Tag Function](../auto-tag/README.md) - Automatically generate tags for content using AI
- [Slack Notify Function](../slack-notify/README.md) - Send notifications when stale products are detected
- [Product Mapping Function](../product-mapping/README.md) - Automatically organize products based on Shopify tags