diff --git a/.gitignore b/.gitignore index b4a772505d0062..b46a4f61e2a759 100644 --- a/.gitignore +++ b/.gitignore @@ -91,6 +91,9 @@ public/mdx-images/* public/og-images/* !public/og-images/README.md +# Ignore LLM plans +/docs/plans/ + # yalc .yalc yalc.lock diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 7d3c950ed1d5a0..c5e6d24211a291 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -11,6 +11,9 @@ import {Home} from 'sentry-docs/components/home'; import {Include} from 'sentry-docs/components/include'; import {PageLoadMetrics} from 'sentry-docs/components/pageLoadMetrics'; import {PlatformContent} from 'sentry-docs/components/platformContent'; +import {SpecChangelog} from 'sentry-docs/components/specChangelog'; +import {isSpecStatus} from 'sentry-docs/components/specConstants'; +import {SpecMeta} from 'sentry-docs/components/specMeta'; import { DocNode, getCurrentPlatformOrGuide, @@ -47,23 +50,58 @@ export async function generateStaticParams() { export const dynamicParams = false; export const dynamic = 'force-static'; -const mdxComponentsWithWrapper = mdxComponents( - {Include, PlatformContent}, - ({children, frontMatter, nextPage, previousPage}) => ( - - {children} - - ) -); +function mdxComponentsForFrontMatter(frontMatter: Record) { + const specOverrides: Record = {}; + const changelog = Array.isArray(frontMatter.spec_changelog) + ? (frontMatter.spec_changelog as Array>) + .filter( + entry => entry.version != null && entry.date != null && entry.summary != null + ) + .map(entry => ({ + version: String(entry.version), + date: + entry.date instanceof Date + ? entry.date.toISOString().split('T')[0] + : String(entry.date), + summary: String(entry.summary), + })) + : undefined; + if (changelog && changelog.length > 0) { + specOverrides.SpecChangelog = function () { + return ; + }; + } + if (frontMatter.spec_version && isSpecStatus(frontMatter.spec_status)) { + const boundProps = { + version: String(frontMatter.spec_version), + status: frontMatter.spec_status, + }; + specOverrides.SpecMeta = function () { + return ; + }; + } + return mdxComponents( + {Include, PlatformContent, ...specOverrides}, + ({children, frontMatter: fm, nextPage, previousPage}) => ( + + {children} + + ) + ); +} function MDXLayoutRenderer({mdxSource, ...rest}) { const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]); - return ; + const components = useMemo( + () => mdxComponentsForFrontMatter(rest.frontMatter || {}), + [rest.frontMatter] + ); + return ; } export default async function Page(props: {params: Promise<{path?: string[]}>}) { diff --git a/develop-docs/sdk/telemetry/check-ins.mdx b/develop-docs/sdk/telemetry/check-ins.mdx index d9dfaa08cdee39..6d3a23aadbe322 100644 --- a/develop-docs/sdk/telemetry/check-ins.mdx +++ b/develop-docs/sdk/telemetry/check-ins.mdx @@ -1,86 +1,191 @@ --- -title: Check-Ins +title: Cron Check-Ins +spec_id: sdk/telemetry/check-ins +spec_version: 1.6.0 +spec_status: stable +spec_platforms: + - backend +spec_depends_on: + - id: sdk/foundations/data-model/envelopes + version: ">=1.0.0" +spec_changelog: + - version: 1.6.0 + date: 2025-09-18 + summary: Added hook requirements (beforeSend exclusion, optional beforeSendCheckIn) + - version: 1.5.0 + date: 2024-04-24 + summary: Added owner field to monitor_config + - version: 1.4.0 + date: 2023-12-13 + summary: Added failure_issue_threshold and recovery_threshold to monitor_config + - version: 1.3.0 + date: 2023-06-26 + summary: Added trace context support for linking check-ins to errors + - version: 1.2.0 + date: 2023-06-05 + summary: Added zero UUID support for updating most recent in_progress check-in + - version: 1.1.0 + date: 2023-04-18 + summary: Added monitor upsert support with schedule configuration + - version: 1.0.0 + date: 2023-03-21 + summary: Initial spec — basic check-in payload sidebar_order: 1 --- -## Cron Monitor Check-In Payload + -A check-in is an item in an envelope called `check_in`. It consists -of a JSON payload that looks roughly like this: + -```json -{ - "check_in_id": "83a7c03ed0a04e1b97e2e3b18d38f244", - "monitor_slug": "my-monitor", - "status": "in_progress", - "duration": 10.0, - "release": "1.0.0", - "environment": "production", - "contexts": { - "trace": { - "trace_id": "8f431b7aa08441bbbd5a0100fd91f9fe" - } - } -} +## Overview + +Cron check-ins allow Sentry to monitor the health of recurring jobs (cron jobs, scheduled tasks, periodic workers). An SDK sends check-in envelopes to report when a job starts, succeeds, or fails. Sentry uses this data to detect missed, late, and failing executions. + +Related specs: +- [Envelopes](/sdk/foundations/data-model/envelopes/) — transport format +- [Envelope Items](/sdk/foundations/data-model/envelope-items/) — `check_in` item type constraints + +--- + +## Concepts + +### Monitors and Check-Ins + +A **monitor** is a named entity in Sentry that tracks a recurring job. Each monitor is identified by a unique `monitor_slug`. Monitors can be created manually in Sentry or upserted by the SDK alongside check-in data. + +A **check-in** is a single status report for one execution of a monitored job, sent as a `check_in` envelope item. + +### Check-In Lifecycle + +Each job execution produces two check-ins that share the same `check_in_id`: + +``` +[job starts] → in_progress → [job ends] → ok / error ``` -The following fields exist: -`check_in_id` +- `in_progress` — the job has started +- `ok` — the job completed successfully +- `error` — the job failed + +Sentry uses the time between the `in_progress` and terminal (`ok`/`error`) check-ins to compute duration, and the absence of a terminal check-in within `max_runtime` to detect timeouts. + +### Monitor Upsert + +SDKs can send monitor configuration (schedule, thresholds, timezone) alongside a check-in. Sentry creates the monitor if it doesn't exist or updates it if it does. This eliminates the need for manual monitor setup. + +--- + +## Behavior -: **String, required**. Check-In ID (unique and client generated). +### Check-In Lifecycle - This may be provided as a empty UUID (128 bit zero value) to indicate to - Sentry that the checkin should update the most recent "in_progress" check-in. - If the most recent check-in is not in progress a new one will be created - instead. + -`monitor_slug` +SDKs **MUST** generate a unique `check_in_id` (UUID v4) when a job execution starts and send a check-in with status `in_progress`. -: **String, required**. The distinct slug of the monitor. +When the job completes, SDKs **MUST** send a second check-in with the same `check_in_id` and status `ok` (success) or `error` (failure). -`status` +SDKs **SHOULD** compute the `duration` field for terminal check-ins (`ok` or `error`) by measuring the elapsed time since the `in_progress` check-in was sent. -: **String, required**. The status of the check-in. Can be one of the following: - - `in_progress`: The check-in has started. - - `ok`: The check-in has completed successfully. - - `error`: The check-in has failed. + -`duration` +### Trace Context Linking -: _Number, optional_. The duration of the check-in in seconds. Will only take effect if the - status is `ok` or `error`. + -`release` +SDKs **SHOULD** include a `contexts.trace` object with a `trace_id` field to link the check-in to associated errors and traces. -: _String, optional_. The release. +If the job execution is part of an active trace, the SDK **SHOULD** use that trace's `trace_id`. Otherwise, the SDK **MAY** generate a new `trace_id` for correlation purposes. -`environment` + -: _String, optional_. The environment. +### Updating in_progress Check-Ins -`monitor_config` + -: _Object, optional_. A monitor configuration (defined below) that is stored with the - check-in in order to verify the state of the monitor at the time of the check-in. +SDKs **MAY** send a check-in with `check_in_id` set to an empty UUID (128-bit zero value, `00000000000000000000000000000000`) to indicate that Sentry **SHOULD** update the most recent `in_progress` check-in for the given monitor. -`contexts` +If no `in_progress` check-in exists, Sentry creates a new one. -: _Object, optional_. A dictionary of contextual information about the environment running - the check-in. Right now we only support the [trace context](/sdk/foundations/data-model/event-payloads/contexts/#trace-context) - and use the `trace_id` in order to link check-ins to associated errors. +This is useful for fire-and-forget patterns where the SDK does not retain the original `check_in_id`. -## Monitor upsert support + -In addition to sending check-in details, the SDK may also provide monitor -configuration, allowing monitors to be created or updated when sending -check-ins. +### Monitor Upsert + + +SDKs **MAY** include a `monitor_config` object in the check-in payload. When present, Sentry creates the monitor if it does not exist, or updates its configuration if it does. + +SDKs **SHOULD** only send `monitor_config` on the first check-in of a job execution (the `in_progress` check-in), not on every check-in. + + + +### Auto-Instrumentation + + + +SDKs **MAY** provide integrations that automatically instrument popular scheduling libraries. When available, these integrations **SHOULD**: + +1. Automatically discover scheduled jobs and derive `monitor_slug` values. +2. Send `in_progress` and terminal check-ins for each job execution. +3. Upsert monitor configuration based on the library's schedule definition. + +Known integrations include Celery Beat (Python), node-cron / cron / node-schedule (JavaScript), Sidekiq-Cron (Ruby), and Oban/Quantum (Elixir). + + + +--- + +## Wire Format + +### Check-In Payload + + + +A check-in is sent as a `check_in` envelope item containing a JSON object. ```json { "check_in_id": "83a7c03ed0a04e1b97e2e3b18d38f244", - "monitor_slug": "b7645b8e-b47d-4398-be9a-d16b0dac31cb", + "monitor_slug": "my-monitor", "status": "in_progress", + "duration": 10.0, + "release": "1.0.0", + "environment": "production", + "contexts": { + "trace": { + "trace_id": "8f431b7aa08441bbbd5a0100fd91f9fe" + } + } +} +``` + +| Field | Type | Required | Since | Description | +|-------|------|----------|-------|-------------| +| `check_in_id` | String | **REQUIRED** | 1.0.0 | Client-generated UUID identifying this job execution. **MAY** be an empty UUID (all zeros) to update the most recent `in_progress` check-in (since 1.2.0). | +| `monitor_slug` | String | **REQUIRED** | 1.0.0 | The distinct slug identifying the monitor. | +| `status` | String | **REQUIRED** | 1.0.0 | One of `in_progress`, `ok`, or `error`. | +| `duration` | Number | **OPTIONAL** | 1.0.0 | Duration of the job execution in seconds. Only takes effect for `ok` or `error` status. | +| `release` | String | **OPTIONAL** | 1.0.0 | The release version. | +| `environment` | String | **OPTIONAL** | 1.0.0 | The environment name. | +| `monitor_config` | Object | **OPTIONAL** | 1.1.0 | Monitor configuration for upsert. See [Monitor Configuration Payload](#monitor-configuration-payload). | +| `contexts` | Object | **OPTIONAL** | 1.3.0 | Contextual information. Currently supports [trace context](/sdk/foundations/data-model/event-payloads/contexts/#trace-context) with `trace_id` for linking check-ins to errors. | + +**Envelope constraints:** +- A `check_in` item **MUST** occur at most once per envelope. +- Maximum item size: 100 KiB. + + + +### Monitor Configuration Payload + + + +When included, the `monitor_config` object supports the following fields: + +```json +{ "monitor_config": { "schedule": { "type": "crontab", @@ -95,71 +200,195 @@ check-ins. } } ``` -The following fields exist under the `monitor_config` key: -`schedule` +| Field | Type | Required | Since | Description | +|-------|------|----------|-------|-------------| +| `schedule` | Object | **REQUIRED** | 1.1.0 | Schedule configuration. See [Schedule Configuration](#schedule-configuration). | +| `checkin_margin` | Number | **OPTIONAL** | 1.1.0 | Allowed margin in minutes after the expected check-in time before the monitor is considered missed. | +| `max_runtime` | Number | **OPTIONAL** | 1.1.0 | Allowed duration in minutes that a monitor may be `in_progress` before being considered failed due to timeout. | +| `failure_issue_threshold` | Number | **OPTIONAL** | 1.4.0 | Number of consecutive failed check-ins before an issue is created. | +| `recovery_threshold` | Number | **OPTIONAL** | 1.4.0 | Number of consecutive OK check-ins before an issue is resolved. | +| `timezone` | String | **OPTIONAL** | 1.1.0 | A [tz database](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string for the monitor's execution schedule timezone. | +| `owner` | String | **OPTIONAL** | 1.5.0 | An actor identifier string, e.g. `user:john@example.com` or `team:a-sentry-team`. | + + -: **Object, required**. [See schedule configuration](#schedule-configuration). +### Schedule Configuration -`checkin_margin` + + + +This configuration format differs slightly from what is accepted in the +monitors frontend APIs. + -: _Number, optional_. The allowed margin of minutes after the expected -check-in time that the monitor will not be considered missed for. +The `schedule` object **MUST** contain a `type` field set to either `crontab` or `interval`. -`max_runtime` +**Crontab schedule:** -: _Number, optional_. The allowed duration in minutes that the monitor -may be `in_progress` for before being considered failed due to timeout. +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | String | **REQUIRED** | `"crontab"` | +| `value` | String | **REQUIRED** | A crontab expression, e.g. `"0 * * * *"`. | -`failure_issue_threshold` +**Interval schedule:** -: _Number, optional_. The number of consecutive failed check-ins it takes -before an issue is created. +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | String | **REQUIRED** | `"interval"` | +| `value` | Number | **REQUIRED** | The interval value. | +| `unit` | String | **REQUIRED** | One of `year`, `month`, `week`, `day`, `hour`, `minute`. | -`recovery_threshold` + -: _Number, optional_. The number of consecutive OK check-ins it takes -before an issue is resolved. +--- -`timezone` +## Public API -: _String, optional_. A [`tz -database`](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) string -representing the timezone which the monitor's execution schedule is in. +### Capture Check-In -`owner` + -: _String, optional_. An actor identifier string. This looks like -`user:john@example.com` `team:a-sentry-team`. IDs can also be used but will -result in a poor DX. +SDKs **MUST** expose a top-level function to send a check-in: -### Schedule configuration +``` +captureCheckIn(checkIn, monitorConfig?) -> checkInId +``` - -This configuration format differs slightly from what is accepted in the -monitors frontend APIs. - +**Parameters:** +- `checkIn` — Object containing at minimum `monitorSlug` and `status`. For terminal check-ins, also includes `checkInId` (from the initial `in_progress` call) and optionally `duration`. +- `monitorConfig` — **OPTIONAL**. Monitor configuration for upsert (schedule, margins, thresholds). -`type` +**Returns:** The `checkInId` (string), so callers can correlate the `in_progress` and terminal check-ins. -: **String, required**. One of `crontab` or `interval`. +Naming **SHOULD** follow the SDK's language conventions: +- `captureCheckIn` (JavaScript, Java) +- `capture_check_in` (Python, Ruby, Elixir) +- `CaptureCheckIn` (Go, .NET) -#### Using `crontab` + -`value` +### Monitor Wrapper -: **String, required**. The crontab schedule string, e.g. `0 * * * *`. + -#### Using `interval` +SDKs **SHOULD** expose a higher-level wrapper that automatically sends `in_progress` and `ok`/`error` check-ins around a callback: -`value` +``` +withMonitor(monitorSlug, callback, monitorConfig?) -> callbackResult +``` + +The wrapper **MUST**: +1. Send an `in_progress` check-in before invoking the callback. +2. Send an `ok` check-in if the callback completes without error. +3. Send an `error` check-in if the callback throws or returns a failure. +4. Compute and include `duration` in the terminal check-in. + +SDKs **MAY** additionally provide language-idiomatic alternatives: +- **Decorators** (Python): `@monitor(monitor_slug='slug')` +- **Context managers** (Python): `with monitor(monitor_slug='slug'):` +- **Mixins** (Ruby): `include Sentry::Cron::MonitorCheckIns` +- **Annotations** (Java/Kotlin): framework-specific annotations + + + +### Hooks + + + +Check-in events **MUST NOT** go through the `beforeSend` hook. The `beforeSend` hook is reserved for error events. -: **Number, required**. The interval value. +SDKs **MAY** implement a dedicated `beforeSendCheckIn` hook that applies only to check-in events. + + + +--- + +## Examples + +### Basic manual check-in (two-step) + +``` +// Start +checkInId = captureCheckIn({ + monitorSlug: "data-sync", + status: "in_progress" +}) + +// ... execute job ... + +// Complete +captureCheckIn({ + monitorSlug: "data-sync", + checkInId: checkInId, + status: "ok", + duration: 12.5 +}) +``` -`unit` +Envelope payload for the first check-in: -: **String, required**. The interval unit. Should be one of `year`, `month`, `week`, `day`, `hour`, `minute`. +```json +{ + "check_in_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "monitor_slug": "data-sync", + "status": "in_progress" +} +``` + +Envelope payload for the terminal check-in: + +```json +{ + "check_in_id": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4", + "monitor_slug": "data-sync", + "status": "ok", + "duration": 12.5 +} +``` + +### Wrapper with monitor upsert + +``` +result = withMonitor("daily-report", () => { + return generateReport() +}, { + schedule: { type: "crontab", value: "0 2 * * *" }, + checkinMargin: 10, + maxRuntime: 60, + timezone: "America/New_York" +}) +``` + +The `in_progress` envelope payload includes the monitor config: + +```json +{ + "check_in_id": "f7e8d9c0b1a2f7e8d9c0b1a2f7e8d9c0", + "monitor_slug": "daily-report", + "status": "in_progress", + "monitor_config": { + "schedule": { "type": "crontab", "value": "0 2 * * *" }, + "checkin_margin": 10, + "max_runtime": 60, + "timezone": "America/New_York" + } +} +``` + +The terminal envelope payload omits the config: + +```json +{ + "check_in_id": "f7e8d9c0b1a2f7e8d9c0b1a2f7e8d9c0", + "monitor_slug": "daily-report", + "status": "ok", + "duration": 45.3 +} +``` + +--- -## Interaction with `beforeSend` +## Changelog -Check-in events in the SDK should **not** go through `beforeSend`. SDKs can optionally implement a dedicated `beforeSendCheckIn` hook that only applies to check-in events. + diff --git a/src/components/specChangelog.tsx b/src/components/specChangelog.tsx new file mode 100644 index 00000000000000..ebc4c7caa09636 --- /dev/null +++ b/src/components/specChangelog.tsx @@ -0,0 +1,45 @@ +type ChangelogEntry = { + date: string; + summary: string; + version: string; +}; + +type SpecChangelogProps = { + changelog: ChangelogEntry[]; +}; + +export function SpecChangelog({changelog}: SpecChangelogProps) { + if (!changelog || changelog.length === 0) { + return null; + } + + return ( +
+ + + + + + + + + + {changelog.map(entry => ( + + + + + + ))} + +
VersionDateSummary
+ + {entry.version} + + {entry.date}{entry.summary}
+
+ ); +} diff --git a/src/components/specConstants.ts b/src/components/specConstants.ts new file mode 100644 index 00000000000000..975d17e551a4d7 --- /dev/null +++ b/src/components/specConstants.ts @@ -0,0 +1,21 @@ +export const SPEC_STATUSES = [ + 'proposal', + 'draft', + 'candidate', + 'stable', + 'deprecated', +] as const; + +export type SpecStatus = (typeof SPEC_STATUSES)[number]; + +export function isSpecStatus(value: unknown): value is SpecStatus { + return typeof value === 'string' && SPEC_STATUSES.includes(value as SpecStatus); +} + +export const SPEC_STATUS_BADGE: Record = { + proposal: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300', + draft: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200', + candidate: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200', + stable: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200', + deprecated: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200', +}; diff --git a/src/components/specMeta.tsx b/src/components/specMeta.tsx new file mode 100644 index 00000000000000..6b1c1fe979485a --- /dev/null +++ b/src/components/specMeta.tsx @@ -0,0 +1,37 @@ +import {SPEC_STATUS_BADGE, type SpecStatus} from './specConstants'; + +type SpecMetaProps = { + status: SpecStatus; + version: string; +}; + +export function SpecMeta({version, status}: SpecMetaProps) { + const badgeClass = SPEC_STATUS_BADGE[status]; + + return ( +
+
+
+ Status + + {status} + +
+
+ Version + + {version} + + + (changelog) + +
+
+
+ ); +} diff --git a/src/components/specRfcAlert.tsx b/src/components/specRfcAlert.tsx new file mode 100644 index 00000000000000..666eeeda489678 --- /dev/null +++ b/src/components/specRfcAlert.tsx @@ -0,0 +1,11 @@ +import {Alert} from './alert'; + +export function SpecRfcAlert() { + return ( + + This document uses key words such as "MUST", "SHOULD", and "MAY" as defined in{' '} + RFC 2119 to indicate requirement + levels. + + ); +} diff --git a/src/components/specSection.tsx b/src/components/specSection.tsx new file mode 100644 index 00000000000000..1a4f1002a49f6a --- /dev/null +++ b/src/components/specSection.tsx @@ -0,0 +1,53 @@ +import type {ReactNode} from 'react'; + +import {SPEC_STATUS_BADGE, type SpecStatus} from './specConstants'; + +type SpecSectionProps = { + children: ReactNode; + id: string; + since: string; + status: SpecStatus; + superseded_by?: string; +}; + +const STATUS_CONFIG: Record = { + proposal: {label: 'Proposal', borderColor: 'border-l-gray-400 dark:border-l-gray-500'}, + draft: {label: 'Draft', borderColor: 'border-l-yellow-400 dark:border-l-yellow-500'}, + candidate: { + label: 'Candidate', + borderColor: 'border-l-blue-400 dark:border-l-blue-500', + }, + stable: {label: 'Stable', borderColor: 'border-l-green-400 dark:border-l-green-500'}, + deprecated: { + label: 'Deprecated', + borderColor: 'border-l-red-400 dark:border-l-red-500', + }, +}; + +export function SpecSection({ + id, + status, + since, + superseded_by, + children, +}: SpecSectionProps) { + const {label, borderColor} = STATUS_CONFIG[status]; + const badgeClass = SPEC_STATUS_BADGE[status]; + + return ( +
+
+ + {label} + + since {since} + {superseded_by && ( + + Superseded by {superseded_by} + + )} +
+
{children}
+
+ ); +} diff --git a/src/mdxComponents.ts b/src/mdxComponents.ts index 77a0414d16773b..e81c95501a718b 100644 --- a/src/mdxComponents.ts +++ b/src/mdxComponents.ts @@ -51,6 +51,10 @@ import {SdkApi} from './components/sdkApi'; import {SdkOption} from './components/sdkOption'; import {SignInNote} from './components/signInNote'; import {SmartLink} from './components/smartLink'; +import {SpecChangelog} from './components/specChangelog'; +import {SpecMeta} from './components/specMeta'; +import {SpecRfcAlert} from './components/specRfcAlert'; +import {SpecSection} from './components/specSection'; import { SplitLayout, SplitSection, @@ -119,6 +123,10 @@ export function mdxComponents( SandboxLink, CopyableCard, SignInNote, + SpecChangelog, + SpecMeta, + SpecRfcAlert, + SpecSection, SplitLayout, SplitSection, SplitSectionText,