diff --git a/packages/node/src/client.ts b/packages/node/src/client.ts index fd0e94de595b..af39f786ac3c 100644 --- a/packages/node/src/client.ts +++ b/packages/node/src/client.ts @@ -153,25 +153,30 @@ export class NodeClient extends BaseClient { * @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. + * @returns A string representing the id of the check in. */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): void { + public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig): string { + const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); if (!this._isEnabled()) { __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); - return; + return id; } const options = this.getOptions(); const { release, environment, tunnel } = options; const serializedCheckIn: SerializedCheckIn = { - check_in_id: uuid4(), + check_in_id: id, monitor_slug: checkIn.monitorSlug, status: checkIn.status, - duration: checkIn.duration, release, environment, }; + if (checkIn.status !== 'in_progress') { + serializedCheckIn.duration = checkIn.duration; + } + if (monitorConfig) { serializedCheckIn.monitor_config = { schedule: monitorConfig.schedule, @@ -183,6 +188,7 @@ export class NodeClient extends BaseClient { const envelope = createCheckInEnvelope(serializedCheckIn, this.getSdkMetadata(), tunnel, this.getDsn()); void this._sendEnvelope(envelope); + return id; } /** diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index d8bb6c25c989..d4e2df4ac5f9 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -13,6 +13,7 @@ import { logger, nodeStackLineParser, stackParserFromStackParserOptions, + uuid4, } from '@sentry/utils'; import { setNodeAsyncContextStrategy } from './async'; @@ -273,12 +274,17 @@ export function captureCheckIn( checkIn: CheckIn, upsertMonitorConfig?: MonitorConfig, ): ReturnType { + const capturedCheckIn = + checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn : { ...checkIn, checkInId: uuid4() }; + const client = getCurrentHub().getClient(); if (client) { - return client.captureCheckIn(checkIn, upsertMonitorConfig); + client.captureCheckIn(capturedCheckIn, upsertMonitorConfig); + } else { + __DEBUG_BUILD__ && logger.warn('Cannot capture check in. No client defined.'); } - __DEBUG_BUILD__ && logger.warn('Cannot capture check in. No client defined.'); + return capturedCheckIn.checkInId; } /** Node.js stack parser */ diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index c29627accb2e..ee5fd5bdd957 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -294,8 +294,8 @@ describe('NodeClient', () => { // @ts-ignore accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); - client.captureCheckIn( - { monitorSlug: 'foo', status: 'ok', duration: 1222 }, + const id = client.captureCheckIn( + { monitorSlug: 'foo', status: 'in_progress' }, { schedule: { type: 'crontab', @@ -314,10 +314,9 @@ describe('NodeClient', () => { [ expect.any(Object), { - check_in_id: expect.any(String), - duration: 1222, + check_in_id: id, monitor_slug: 'foo', - status: 'ok', + status: 'in_progress', release: '1.0.0', environment: 'dev', monitor_config: { @@ -333,6 +332,26 @@ describe('NodeClient', () => { ], ], ]); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'foo', + duration: 1222, + status: 'ok', + release: '1.0.0', + environment: 'dev', + }, + ], + ], + ]); }); it('does not send a checkIn envelope if disabled', () => { @@ -342,7 +361,7 @@ describe('NodeClient', () => { // @ts-ignore accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); - client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222 }); + client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); }); diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts index abd0265b62c4..f7c2595c66a5 100644 --- a/packages/node/test/sdk.test.ts +++ b/packages/node/test/sdk.test.ts @@ -1,5 +1,7 @@ +import { getCurrentHub } from '@sentry/core'; import type { Integration } from '@sentry/types'; +import type { NodeClient } from '../build/types'; import { init } from '../src/sdk'; import * as sdk from '../src/sdk'; @@ -90,3 +92,21 @@ describe('init()', () => { expect(newIntegration.setupOnce as jest.Mock).toHaveBeenCalledTimes(1); }); }); + +describe('captureCheckIn', () => { + it('always returns an id', () => { + const hub = getCurrentHub(); + const client = hub.getClient(); + expect(client).toBeDefined(); + + const captureCheckInSpy = jest.spyOn(client!, 'captureCheckIn'); + + // test if captureCheckIn returns an id even if client is not defined + hub.bindClient(undefined); + + expect(captureCheckInSpy).toHaveBeenCalledTimes(0); + expect(sdk.captureCheckIn({ monitorSlug: 'gogogo', status: 'in_progress' })).toBeTruthy(); + + hub.bindClient(client); + }); +}); diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts index 67537e46d390..a316c0c7a375 100644 --- a/packages/types/src/checkin.ts +++ b/packages/types/src/checkin.ts @@ -38,15 +38,26 @@ export interface SerializedCheckIn { }; } -export interface CheckIn { +interface InProgressCheckIn { // The distinct slug of the monitor. monitorSlug: SerializedCheckIn['monitor_slug']; // The status of the check-in. - status: SerializedCheckIn['status']; + status: 'in_progress'; +} + +export interface FinishedCheckIn { + // The distinct slug of the monitor. + monitorSlug: SerializedCheckIn['monitor_slug']; + // The status of the check-in. + status: 'ok' | 'error'; + // Check-In ID (unique and client generated). + checkInId: SerializedCheckIn['check_in_id']; // The duration of the check-in in seconds. Will only take effect if the status is ok or error. duration?: SerializedCheckIn['duration']; } +export type CheckIn = InProgressCheckIn | FinishedCheckIn; + type SerializedMonitorConfig = NonNullable; export interface MonitorConfig {