From 82deeb39ef771d72ce24ecf11bbe9075f24bf6cc Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 4 May 2023 12:32:28 +0200 Subject: [PATCH 1/4] feat(node): Add ability to send cron monitor check ins --- packages/node/src/checkin.ts | 13 +++++-- packages/node/src/client.ts | 53 +++++++++++++++++++++++++- packages/node/src/index.ts | 11 +++++- packages/node/src/sdk.ts | 21 ++++++++++- packages/node/test/checkin.test.ts | 8 ++-- packages/node/test/client.test.ts | 60 ++++++++++++++++++++++++++++++ packages/types/src/checkin.ts | 26 ++++++++++++- packages/types/src/envelope.ts | 4 +- packages/types/src/index.ts | 2 +- 9 files changed, 183 insertions(+), 15 deletions(-) diff --git a/packages/node/src/checkin.ts b/packages/node/src/checkin.ts index c2b56509e12a..f4856b25e3b0 100644 --- a/packages/node/src/checkin.ts +++ b/packages/node/src/checkin.ts @@ -1,11 +1,18 @@ -import type { CheckIn, CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata } from '@sentry/types'; +import type { + CheckIn, + 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 +32,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..f3b8316b7cf9 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 { dropUndefinedKeys, 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,46 @@ 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 = dropUndefinedKeys({ + check_in_id: uuid4(), + monitor_slug: checkIn.monitorSlug, + status: checkIn.status, + duration: checkIn.duration, + release, + environment, + }); + + if (monitorConfig) { + serializedCheckIn.monitor_config = dropUndefinedKeys({ + schedule: monitorConfig.schedule, + checkin_margin: monitorConfig.checkinMargin, + max_runtime: monitorConfig.maxRuntime, + timezone: monitorConfig.timezone, + }); + } + + const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); + + __DEBUG_BUILD__ && logger.warn('Sending checkin: ', checkIn); + 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..925e068fc7e0 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -280,6 +280,66 @@ describe('NodeClient', () => { expect(event.server_name).not.toEqual('bar'); }); }); + + describe('captureCheckIn', () => { + it('sends a checkIn envelope', () => { + const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar' }); + 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', + 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'; From ad47e87222d1c4e4f76e7a845ac8ed050e683a4d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 4 May 2023 14:05:14 +0200 Subject: [PATCH 2/4] remove unused type --- packages/node/src/checkin.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/node/src/checkin.ts b/packages/node/src/checkin.ts index f4856b25e3b0..2417736bcdae 100644 --- a/packages/node/src/checkin.ts +++ b/packages/node/src/checkin.ts @@ -1,11 +1,4 @@ -import type { - CheckIn, - CheckInEvelope, - CheckInItem, - DsnComponents, - SdkMetadata, - SerializedCheckIn, -} from '@sentry/types'; +import type { CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata, SerializedCheckIn } from '@sentry/types'; import { createEnvelope, dsnToString } from '@sentry/utils'; /** From 1cdfd9fd81bbc8dd227994e1f8a9cf3056634380 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 4 May 2023 14:07:09 +0200 Subject: [PATCH 3/4] remove dropUndefinedKeys --- packages/node/src/client.ts | 10 +++++----- packages/node/test/client.test.ts | 9 ++++++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index f3b8316b7cf9..f62c93a539b8 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -9,7 +9,7 @@ import type { Severity, SeverityLevel, } from '@sentry/types'; -import { dropUndefinedKeys, logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; +import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils'; import * as os from 'os'; import { TextEncoder } from 'util'; @@ -163,22 +163,22 @@ export class NodeClient extends BaseClient { const options = this.getOptions(); const { release, environment, tunnel } = options; - const serializedCheckIn: SerializedCheckIn = dropUndefinedKeys({ + const serializedCheckIn: SerializedCheckIn = { check_in_id: uuid4(), monitor_slug: checkIn.monitorSlug, status: checkIn.status, duration: checkIn.duration, release, environment, - }); + }; if (monitorConfig) { - serializedCheckIn.monitor_config = dropUndefinedKeys({ + 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()); diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index 925e068fc7e0..c29627accb2e 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -283,7 +283,12 @@ describe('NodeClient', () => { describe('captureCheckIn', () => { it('sends a checkIn envelope', () => { - const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar' }); + const options = getDefaultNodeClientOptions({ + dsn: PUBLIC_DSN, + serverName: 'bar', + release: '1.0.0', + environment: 'dev', + }); client = new NodeClient(options); // @ts-ignore accessing private method @@ -313,6 +318,8 @@ describe('NodeClient', () => { duration: 1222, monitor_slug: 'foo', status: 'ok', + release: '1.0.0', + environment: 'dev', monitor_config: { schedule: { type: 'crontab', From af4a4fb9687fc6c18732fa4af077f9fabb3ccbf2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 4 May 2023 14:08:45 +0200 Subject: [PATCH 4/4] remove warning --- packages/node/src/client.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index f62c93a539b8..fd0e94de595b 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -182,8 +182,6 @@ export class NodeClient extends BaseClient { } const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); - - __DEBUG_BUILD__ && logger.warn('Sending checkin: ', checkIn); void this._sendEnvelope(envelope); }