Skip to content

Commit 9ea2bca

Browse files
authored
feat(node): Add ability to send cron monitor check ins (#8039)
1 parent 43d9fa9 commit 9ea2bca

File tree

9 files changed

+181
-15
lines changed

9 files changed

+181
-15
lines changed

packages/node/src/checkin.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import type { CheckIn, CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata } from '@sentry/types';
1+
import type { CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata, SerializedCheckIn } from '@sentry/types';
22
import { createEnvelope, dsnToString } from '@sentry/utils';
33

44
/**
55
* Create envelope from check in item.
66
*/
77
export function createCheckInEnvelope(
8-
checkIn: CheckIn,
8+
checkIn: SerializedCheckIn,
99
metadata?: SdkMetadata,
1010
tunnel?: string,
1111
dsn?: DsnComponents,
@@ -25,7 +25,7 @@ export function createCheckInEnvelope(
2525
return createEnvelope<CheckInEvelope>(headers, [item]);
2626
}
2727

28-
function createCheckInEnvelopeItem(checkIn: CheckIn): CheckInItem {
28+
function createCheckInEnvelopeItem(checkIn: SerializedCheckIn): CheckInItem {
2929
const checkInHeaders: CheckInItem[0] = {
3030
type: 'check_in',
3131
};

packages/node/src/client.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import type { Scope } from '@sentry/core';
22
import { addTracingExtensions, BaseClient, SDK_VERSION, SessionFlusher } from '@sentry/core';
3-
import type { Event, EventHint, Severity, SeverityLevel } from '@sentry/types';
4-
import { logger, resolvedSyncPromise } from '@sentry/utils';
3+
import type {
4+
CheckIn,
5+
Event,
6+
EventHint,
7+
MonitorConfig,
8+
SerializedCheckIn,
9+
Severity,
10+
SeverityLevel,
11+
} from '@sentry/types';
12+
import { logger, resolvedSyncPromise, uuid4 } from '@sentry/utils';
513
import * as os from 'os';
614
import { TextEncoder } from 'util';
715

16+
import { createCheckInEnvelope } from './checkin';
817
import { eventFromMessage, eventFromUnknownInput } from './eventbuilder';
918
import type { NodeClientOptions } from './types';
1019

@@ -138,6 +147,44 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
138147
);
139148
}
140149

150+
/**
151+
* Create a cron monitor check in and send it to Sentry.
152+
*
153+
* @param checkIn An object that describes a check in.
154+
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
155+
* to create a monitor automatically when sending a check in.
156+
*/
157+
public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): void {
158+
if (!this._isEnabled()) {
159+
__DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.');
160+
return;
161+
}
162+
163+
const options = this.getOptions();
164+
const { release, environment, tunnel } = options;
165+
166+
const serializedCheckIn: SerializedCheckIn = {
167+
check_in_id: uuid4(),
168+
monitor_slug: checkIn.monitorSlug,
169+
status: checkIn.status,
170+
duration: checkIn.duration,
171+
release,
172+
environment,
173+
};
174+
175+
if (monitorConfig) {
176+
serializedCheckIn.monitor_config = {
177+
schedule: monitorConfig.schedule,
178+
checkin_margin: monitorConfig.checkinMargin,
179+
max_runtime: monitorConfig.maxRuntime,
180+
timezone: monitorConfig.timezone,
181+
};
182+
}
183+
184+
const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn());
185+
void this._sendEnvelope(envelope);
186+
}
187+
141188
/**
142189
* @inheritDoc
143190
*/

packages/node/src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,16 @@ export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing';
5656

5757
export { NodeClient } from './client';
5858
export { makeNodeTransport } from './transports';
59-
export { defaultIntegrations, init, defaultStackParser, lastEventId, flush, close, getSentryRelease } from './sdk';
59+
export {
60+
defaultIntegrations,
61+
init,
62+
defaultStackParser,
63+
lastEventId,
64+
flush,
65+
close,
66+
getSentryRelease,
67+
captureCheckIn,
68+
} from './sdk';
6069
export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from './requestdata';
6170
export { deepReadDirSync } from './utils';
6271

packages/node/src/sdk.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
initAndBind,
77
Integrations as CoreIntegrations,
88
} from '@sentry/core';
9-
import type { SessionStatus, StackParser } from '@sentry/types';
9+
import type { CheckIn, MonitorConfig, SessionStatus, StackParser } from '@sentry/types';
1010
import {
1111
createStackParser,
1212
GLOBAL_OBJ,
@@ -262,6 +262,25 @@ export function getSentryRelease(fallback?: string): string | undefined {
262262
);
263263
}
264264

265+
/**
266+
* Create a cron monitor check in and send it to Sentry.
267+
*
268+
* @param checkIn An object that describes a check in.
269+
* @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want
270+
* to create a monitor automatically when sending a check in.
271+
*/
272+
export function captureCheckIn(
273+
checkIn: CheckIn,
274+
upsertMonitorConfig?: MonitorConfig,
275+
): ReturnType<NodeClient['captureCheckIn']> {
276+
const client = getCurrentHub().getClient<NodeClient>();
277+
if (client) {
278+
return client.captureCheckIn(checkIn, upsertMonitorConfig);
279+
}
280+
281+
__DEBUG_BUILD__ && logger.warn('Cannot capture check in. No client defined.');
282+
}
283+
265284
/** Node.js stack parser */
266285
export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModule));
267286

packages/node/test/checkin.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CheckIn } from '@sentry/types';
1+
import type { SerializedCheckIn } from '@sentry/types';
22

33
import { createCheckInEnvelope } from '../src/checkin';
44

@@ -44,7 +44,7 @@ describe('CheckIn', () => {
4444
duration: 10.0,
4545
release: '1.0.0',
4646
environment: 'production',
47-
} as CheckIn,
47+
} as SerializedCheckIn,
4848
{
4949
check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244',
5050
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',
@@ -69,7 +69,7 @@ describe('CheckIn', () => {
6969
max_runtime: 30,
7070
timezone: 'America/Los_Angeles',
7171
},
72-
} as CheckIn,
72+
} as SerializedCheckIn,
7373
{
7474
check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244',
7575
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',
@@ -98,7 +98,7 @@ describe('CheckIn', () => {
9898
unit: 'minute',
9999
},
100100
},
101-
} as CheckIn,
101+
} as SerializedCheckIn,
102102
{
103103
check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244',
104104
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',

packages/node/test/client.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,73 @@ describe('NodeClient', () => {
280280
expect(event.server_name).not.toEqual('bar');
281281
});
282282
});
283+
284+
describe('captureCheckIn', () => {
285+
it('sends a checkIn envelope', () => {
286+
const options = getDefaultNodeClientOptions({
287+
dsn: PUBLIC_DSN,
288+
serverName: 'bar',
289+
release: '1.0.0',
290+
environment: 'dev',
291+
});
292+
client = new NodeClient(options);
293+
294+
// @ts-ignore accessing private method
295+
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope');
296+
297+
client.captureCheckIn(
298+
{ monitorSlug: 'foo', status: 'ok', duration: 1222 },
299+
{
300+
schedule: {
301+
type: 'crontab',
302+
value: '0 * * * *',
303+
},
304+
checkinMargin: 2,
305+
maxRuntime: 12333,
306+
timezone: 'Canada/Eastern',
307+
},
308+
);
309+
310+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
311+
expect(sendEnvelopeSpy).toHaveBeenCalledWith([
312+
expect.any(Object),
313+
[
314+
[
315+
expect.any(Object),
316+
{
317+
check_in_id: expect.any(String),
318+
duration: 1222,
319+
monitor_slug: 'foo',
320+
status: 'ok',
321+
release: '1.0.0',
322+
environment: 'dev',
323+
monitor_config: {
324+
schedule: {
325+
type: 'crontab',
326+
value: '0 * * * *',
327+
},
328+
checkin_margin: 2,
329+
max_runtime: 12333,
330+
timezone: 'Canada/Eastern',
331+
},
332+
},
333+
],
334+
],
335+
]);
336+
});
337+
338+
it('does not send a checkIn envelope if disabled', () => {
339+
const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false });
340+
client = new NodeClient(options);
341+
342+
// @ts-ignore accessing private method
343+
const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope');
344+
345+
client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222 });
346+
347+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0);
348+
});
349+
});
283350
});
284351

285352
describe('flush/close', () => {

packages/types/src/checkin.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ interface IntervalSchedule {
1313
type MonitorSchedule = CrontabSchedule | IntervalSchedule;
1414

1515
// https://develop.sentry.dev/sdk/check-ins/
16-
export interface CheckIn {
16+
export interface SerializedCheckIn {
1717
// Check-In ID (unique and client generated).
1818
check_in_id: string;
1919
// The distinct slug of the monitor.
@@ -37,3 +37,27 @@ export interface CheckIn {
3737
timezone?: string;
3838
};
3939
}
40+
41+
export interface CheckIn {
42+
// The distinct slug of the monitor.
43+
monitorSlug: SerializedCheckIn['monitor_slug'];
44+
// The status of the check-in.
45+
status: SerializedCheckIn['status'];
46+
// The duration of the check-in in seconds. Will only take effect if the status is ok or error.
47+
duration?: SerializedCheckIn['duration'];
48+
}
49+
50+
type SerializedMonitorConfig = NonNullable<SerializedCheckIn['monitor_config']>;
51+
52+
export interface MonitorConfig {
53+
schedule: MonitorSchedule;
54+
// The allowed allowed margin of minutes after the expected check-in time that
55+
// the monitor will not be considered missed for.
56+
checkinMargin?: SerializedMonitorConfig['checkin_margin'];
57+
// The allowed allowed duration in minutes that the monitor may be `in_progress`
58+
// for before being considered failed due to timeout.
59+
maxRuntime?: SerializedMonitorConfig['max_runtime'];
60+
// A tz database string representing the timezone which the monitor's execution schedule is in.
61+
// See: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
62+
timezone?: SerializedMonitorConfig['timezone'];
63+
}

packages/types/src/envelope.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CheckIn } from './checkin';
1+
import type { SerializedCheckIn } from './checkin';
22
import type { ClientReport } from './clientreport';
33
import type { DsnComponents } from './dsn';
44
import type { Event } from './event';
@@ -79,7 +79,7 @@ export type SessionItem =
7979
| BaseEnvelopeItem<SessionItemHeaders, Session>
8080
| BaseEnvelopeItem<SessionAggregatesItemHeaders, SessionAggregates>;
8181
export type ClientReportItem = BaseEnvelopeItem<ClientReportItemHeaders, ClientReport>;
82-
export type CheckInItem = BaseEnvelopeItem<CheckInItemHeaders, CheckIn>;
82+
export type CheckInItem = BaseEnvelopeItem<CheckInItemHeaders, SerializedCheckIn>;
8383
type ReplayEventItem = BaseEnvelopeItem<ReplayEventItemHeaders, ReplayEvent>;
8484
type ReplayRecordingItem = BaseEnvelopeItem<ReplayRecordingItemHeaders, ReplayRecordingData>;
8585

packages/types/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,4 +106,4 @@ export type { Instrumenter } from './instrumenter';
106106
export type { HandlerDataFetch, HandlerDataXhr, SentryXhrData, SentryWrappedXMLHttpRequest } from './instrument';
107107

108108
export type { BrowserClientReplayOptions } from './browseroptions';
109-
export type { CheckIn } from './checkin';
109+
export type { CheckIn, MonitorConfig, SerializedCheckIn } from './checkin';

0 commit comments

Comments
 (0)