Back to Sanity

Sanity Studio Telemetry

docs/TELEMETRY.md

5.24.012.4 KB
Original Source

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

FilePurpose
packages/sanity/src/core/studio/telemetry/StudioTelemetryProvider.tsxMain provider - creates the batched store, enriches events with context
packages/sanity/src/core/studio/telemetry/types.tsTelemetryContext interface definition
packages/sanity/src/core/studio/telemetry/PerformanceTelemetry.tsMounts Core Web Vitals and legacy INP tracking
packages/sanity/src/core/studio/telemetry/useWebVitalsTelemetry.tsCore Web Vitals via web-vitals/attribution
packages/sanity/src/core/studio/telemetry/useMeasurePerformanceTelemetry.tsLegacy INP v1 tracking
packages/sanity/src/core/studio/MaybeEnableErrorReporting.tsConsent 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,
  description: 'User clicked the "Publish" button in the document pane',
})

Optional Sampling

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

typescript
export const PerformanceINPMeasuredV2 = defineEvent<INPMetricWithAttribution>({
  name: 'Performance INP Measured',
  version: 2,
  description: 'Interaction to Next Paint with attribution',
  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
    ...

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:

SettingValue
Flush interval30 seconds (production)
Flush interval (debug)1 second
Session IDCreated 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",

        // Dynamic (updated on navigation)
        orgId: "org_xyz",
        activeTool: "desk",
        activeWorkspace: "default",
        activeProjectId: "abc123",
        activeDataset: "production",
      }
    }
  ]
}

The context is stored in a useRef so that dynamic values (workspace, tool, org) can update without re-creating the batched store.

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:

EventMetricVersion
Performance LCP MeasuredLargest Contentful Paintv2
Performance FCP MeasuredFirst Contentful Paintv2
Performance CLS MeasuredCumulative Layout Shiftv2
Performance TTFB MeasuredTime to First Bytev2
Performance INP MeasuredInteraction to Next Paintv1 (legacy) + v2

Document Actions

EventWhen
Document PublishedPublish action completes
Publish Button ClickedPublish operation stages (started/completed/failed)
Publish Button Becomes Disabled - Started/CompletedPublish button state transitions

Releases

EventWhen
Version Document Added to ReleaseDocument added to a release
Release Created/Deleted/PublishedRelease lifecycle
Release Scheduled/UnscheduledRelease scheduling
Release Archived/UnarchivedRelease archival
Release Reverted/DuplicatedRelease management
Release Link/ID/Title CopiedClipboard actions
Navigated to Releases OverviewNavigation
Navigated to Scheduled DraftsNavigation

Comments

EventWhen
Comment Link CopiedComment link copied to clipboard
Comment Viewed From LinkComment opened via shared link
Comment List View ChangedView mode toggled

Tasks

EventWhen
Task Created/Duplicated/RemovedTask lifecycle
Task Status ChangedTask state changes
Task Link Copied/OpenedTask sharing
EventWhen
Recent Search ClickedUser clicks a recent search
Document List Load Time MeasuredSearch performance (sampled)

Canvas

EventWhen
Canvas OpenedCanvas opened
Canvas Link CTA Clicked/RedirectedCanvas link interactions
Canvas Unlink CTA Clicked/ApprovedCanvas unlinking

Form Interactions

EventWhen
Portable Text Input Expanded/CollapsedPTE editor state
Portable Text Invalid Value Ignore/ResolvePTE error handling
Created DraftNew draft creation

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,
      description: 'User interacted with my feature',
    })
    
  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.