diff --git a/packages/node/src/checkin.ts b/packages/node/src/checkin.ts index c2b56509e12a..2417736bcdae 100644 --- a/packages/node/src/checkin.ts +++ b/packages/node/src/checkin.ts @@ -1,11 +1,11 @@ -import type { CheckIn, CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata } from '@sentry/types'; +import type { CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata, SerializedCheckIn } from '@sentry/types'; import { createEnvelope, dsnToString } from '@sentry/utils'; /** * Create envelope from check in item. */ export function createCheckInEnvelope( - checkIn: CheckIn, + checkIn: SerializedCheckIn, metadata?: SdkMetadata, tunnel?: string, dsn?: DsnComponents, @@ -25,7 +25,7 @@ export function createCheckInEnvelope( return createEnvelope(headers, [item]); } -function createCheckInEnvelopeItem(checkIn: CheckIn): CheckInItem { +function createCheckInEnvelopeItem(checkIn: SerializedCheckIn): CheckInItem { const checkInHeaders: CheckInItem[0] = { type: 'check_in', }; diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index d0d0ae7424be..fd0e94de595b 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -1,10 +1,19 @@ import type { Scope } from '@sentry/core'; import { addTracingExtensions, BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core'; -import type { Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; -import { logger, resolvedSyncPromise } from '@sentry/utils'; +import type { + CheckIn, + Event, + EventHint, + MonitorConfig, + SerializedCheckIn, + Severity, + SeverityLevel, +} from '@sentry/types'; +import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; import * as os from 'os'; import { TextEncoder } from 'util'; +import { createCheckInEnvelope } from './checkin'; import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; import type { NodeClientOptions } from './types'; @@ -138,6 +147,44 @@ export class NodeClient extends BaseClient { ); } + /** + * Create a cron monitor check in and send it to Sentry. + * + * @param checkIn An object that describes a check in. + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + */ + public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): void { + if (!this._isEnabled()) { + __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); + return; + } + + const options = this.getOptions(); + const { release, environment, tunnel } = options; + + const serializedCheckIn: SerializedCheckIn = { + check_in_id: uuid4(), + monitor_slug: checkIn.monitorSlug, + status: checkIn.status, + duration: checkIn.duration, + release, + environment, + }; + + if (monitorConfig) { + serializedCheckIn.monitor_config = { + schedule: monitorConfig.schedule, + checkin_margin: monitorConfig.checkinMargin, + max_runtime: monitorConfig.maxRuntime, + timezone: monitorConfig.timezone, + }; + } + + const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); + void this._sendEnvelope(envelope); + } + /** * @inheritDoc */ diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6537b16aca70..b9e6202b9496 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -56,7 +56,16 @@ export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; export { NodeClient } from './client'; export { makeNodeTransport } from './transports'; -export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk'; +export { + defaultIntegrations, + init, + defaultStackParser, + lastEventId, + flush, + close, + getSentryRelease, + captureCheckIn, +} from './sdk'; export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from './requestdata'; export { deepReadDirSync } from './utils'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index d0a02c746247..d8bb6c25c989 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -6,7 +6,7 @@ import { initAndBind, Integrations as CoreIntegrations, } from '@sentry/core'; -import type { SessionStatus, StackParser } from '@sentry/types'; +import type { CheckIn, MonitorConfig, SessionStatus, StackParser } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ, @@ -262,6 +262,25 @@ export function getSentryRelease(fallback?: string): string | undefined { ); } +/** + * Create a cron monitor check in and send it to Sentry. + * + * @param checkIn An object that describes a check in. + * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want + * to create a monitor automatically when sending a check in. + */ +export function captureCheckIn( + checkIn: CheckIn, + upsertMonitorConfig?: MonitorConfig, +): ReturnType { + const client = getCurrentHub().getClient(); + if (client) { + return client.captureCheckIn(checkIn, upsertMonitorConfig); + } + + __DEBUG_BUILD__ && logger.warn('Cannot capture check in. No client defined.'); +} + /** Node.js stack parser */ export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModule)); diff --git a/packages/node/test/checkin.test.ts b/packages/node/test/checkin.test.ts index 4bd1003097dc..9fe59ad66971 100644 --- a/packages/node/test/checkin.test.ts +++ b/packages/node/test/checkin.test.ts @@ -1,4 +1,4 @@ -import type { CheckIn } from '@sentry/types'; +import type { SerializedCheckIn } from '@sentry/types'; import { createCheckInEnvelope } from '../src/checkin'; @@ -44,7 +44,7 @@ describe('CheckIn', () => { duration: 10.0, release: '1.0.0', environment: 'production', - } as CheckIn, + } as SerializedCheckIn, { check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', @@ -69,7 +69,7 @@ describe('CheckIn', () => { max_runtime: 30, timezone: 'America/Los_Angeles', }, - } as CheckIn, + } as SerializedCheckIn, { check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', @@ -98,7 +98,7 @@ describe('CheckIn', () => { unit: 'minute', }, }, - } as CheckIn, + } as SerializedCheckIn, { check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index a996b5408288..c29627accb2e 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -280,6 +280,73 @@ describe('NodeClient', () => { expect(event.server_name).not.toEqual('bar'); }); }); + + describe('captureCheckIn', () => { + it('sends a checkIn envelope', () => { + const options = getDefaultNodeClientOptions({ + dsn: PUBLIC_DSN, + serverName: 'bar', + release: '1.0.0', + environment: 'dev', + }); + client = new NodeClient(options); + + // @ts-ignore accessing private method + const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); + + client.captureCheckIn( + { monitorSlug: 'foo', status: 'ok', duration: 1222 }, + { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkinMargin: 2, + maxRuntime: 12333, + timezone: 'Canada/Eastern', + }, + ); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: expect.any(String), + duration: 1222, + monitor_slug: 'foo', + status: 'ok', + release: '1.0.0', + environment: 'dev', + monitor_config: { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkin_margin: 2, + max_runtime: 12333, + timezone: 'Canada/Eastern', + }, + }, + ], + ], + ]); + }); + + it('does not send a checkIn envelope if disabled', () => { + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); + client = new NodeClient(options); + + // @ts-ignore accessing private method + const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222 }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); + }); + }); }); describe('flush/close', () => { diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index a31a5632417c..67537e46d390 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -13,7 +13,7 @@ interface IntervalSchedule { type MonitorSchedule = CrontabSchedule | IntervalSchedule; // https://develop.sentry.dev/sdk/check-ins/ -export interface CheckIn { +export interface SerializedCheckIn { // Check-In ID (unique and client generated). check_in_id: string; // The distinct slug of the monitor. @@ -37,3 +37,27 @@ export interface CheckIn { timezone?: string; }; } + +export interface CheckIn { + // The distinct slug of the monitor. + monitorSlug: SerializedCheckIn['monitor_slug']; + // The status of the check-in. + status: SerializedCheckIn['status']; + // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + duration?: SerializedCheckIn['duration']; +} + +type SerializedMonitorConfig = NonNullable; + +export interface MonitorConfig { + schedule: MonitorSchedule; + // The allowed allowed margin of minutes after the expected check-in time that + // the monitor will not be considered missed for. + checkinMargin?: SerializedMonitorConfig['checkin_margin']; + // The allowed allowed duration in minutes that the monitor may be `in_progress` + // for before being considered failed due to timeout. + maxRuntime?: SerializedMonitorConfig['max_runtime']; + // A tz database string representing the timezone which the monitor's execution schedule is in. + // See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + timezone?: SerializedMonitorConfig['timezone']; +} diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index be146bdd6fc5..3bcb8e96da7d 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -1,4 +1,4 @@ -import type { CheckIn } from './checkin'; +import type { SerializedCheckIn } from './checkin'; import type { ClientReport } from './clientreport'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; @@ -79,7 +79,7 @@ export type SessionItem = | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; -export type CheckInItem = BaseEnvelopeItem; +export type CheckInItem = BaseEnvelopeItem; type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index f0a30806ed6e..1da1778d2012 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -106,4 +106,4 @@ export type { Instrumenter } from './instrumenter'; export type { HandlerDataFetch, HandlerDataXhr, SentryXhrData, SentryWrappedXMLHttpRequest } from './instrument'; export type { BrowserClientReplayOptions } from './browseroptions'; -export type { CheckIn } from './checkin'; +export type { CheckIn, MonitorConfig, SerializedCheckIn } from './checkin';