Skip to content

feat(node): Add ability to send cron monitor check ins #8039

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/node/src/checkin.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,7 +25,7 @@ export function createCheckInEnvelope(
return createEnvelope<CheckInEvelope>(headers, [item]);
}

function createCheckInEnvelopeItem(checkIn: CheckIn): CheckInItem {
function createCheckInEnvelopeItem(checkIn: SerializedCheckIn): CheckInItem {
const checkInHeaders: CheckInItem[0] = {
type: 'check_in',
};
Expand Down
51 changes: 49 additions & 2 deletions packages/node/src/client.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -138,6 +147,44 @@ export class NodeClient extends BaseClient<NodeClientOptions> {
);
}

/**
* 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
*/
Expand Down
11 changes: 10 additions & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
21 changes: 20 additions & 1 deletion packages/node/src/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<NodeClient['captureCheckIn']> {
const client = getCurrentHub().getClient<NodeClient>();
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));

Expand Down
8 changes: 4 additions & 4 deletions packages/node/test/checkin.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CheckIn } from '@sentry/types';
import type { SerializedCheckIn } from '@sentry/types';

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

Expand Down Expand Up @@ -44,7 +44,7 @@ describe('CheckIn', () => {
duration: 10.0,
release: '1.0.0',
environment: 'production',
} as CheckIn,
} as SerializedCheckIn,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

giga l: you can simply do as const instead of as SerializedCheckIn here. TS is only complaining because status is interpreted as string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gonna keep this anyway because I prefer the explicitness of the type.

{
check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244',
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',
Expand All @@ -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',
Expand Down Expand Up @@ -98,7 +98,7 @@ describe('CheckIn', () => {
unit: 'minute',
},
},
} as CheckIn,
} as SerializedCheckIn,
{
check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244',
monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb',
Expand Down
67 changes: 67 additions & 0 deletions packages/node/test/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🫡

},
);

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', () => {
Expand Down
26 changes: 25 additions & 1 deletion packages/types/src/checkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<SerializedCheckIn['monitor_config']>;

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'];
}
4 changes: 2 additions & 2 deletions packages/types/src/envelope.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -79,7 +79,7 @@ export type SessionItem =
| BaseEnvelopeItem<SessionItemHeaders, Session>
| BaseEnvelopeItem<SessionAggregatesItemHeaders, SessionAggregates>;
export type ClientReportItem = BaseEnvelopeItem<ClientReportItemHeaders, ClientReport>;
export type CheckInItem = BaseEnvelopeItem<CheckInItemHeaders, CheckIn>;
export type CheckInItem = BaseEnvelopeItem<CheckInItemHeaders, SerializedCheckIn>;
type ReplayEventItem = BaseEnvelopeItem<ReplayEventItemHeaders, ReplayEvent>;
type ReplayRecordingItem = BaseEnvelopeItem<ReplayRecordingItemHeaders, ReplayRecordingData>;

Expand Down
2 changes: 1 addition & 1 deletion packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';