diff --git a/packages/node/src/checkin.ts b/packages/node/src/checkin.ts new file mode 100644 index 000000000000..c2b56509e12a --- /dev/null +++ b/packages/node/src/checkin.ts @@ -0,0 +1,33 @@ +import type { CheckIn, CheckInEvelope, CheckInItem, DsnComponents, SdkMetadata } from '@sentry/types'; +import { createEnvelope, dsnToString } from '@sentry/utils'; + +/** + * Create envelope from check in item. + */ +export function createCheckInEnvelope( + checkIn: CheckIn, + metadata?: SdkMetadata, + tunnel?: string, + dsn?: DsnComponents, +): CheckInEvelope { + const headers: CheckInEvelope[0] = { + sent_at: new Date().toISOString(), + ...(metadata && + metadata.sdk && { + sdk: { + name: metadata.sdk.name, + version: metadata.sdk.version, + }, + }), + ...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }), + }; + const item = createCheckInEnvelopeItem(checkIn); + return createEnvelope(headers, [item]); +} + +function createCheckInEnvelopeItem(checkIn: CheckIn): CheckInItem { + const checkInHeaders: CheckInItem[0] = { + type: 'check_in', + }; + return [checkInHeaders, checkIn]; +} diff --git a/packages/node/test/checkin.test.ts b/packages/node/test/checkin.test.ts new file mode 100644 index 000000000000..da082207b00d --- /dev/null +++ b/packages/node/test/checkin.test.ts @@ -0,0 +1,61 @@ +import { createCheckInEnvelope } from '../src/checkin'; + +describe('userFeedback', () => { + test('creates user feedback envelope header', () => { + const envelope = createCheckInEnvelope( + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'in_progress', + }, + { + sdk: { + name: 'testSdkName', + version: 'testSdkVersion', + }, + }, + 'testTunnel', + { + host: 'testHost', + projectId: 'testProjectId', + protocol: 'http', + }, + ); + + expect(envelope[0]).toEqual({ + dsn: 'http://undefined@testHost/undefinedtestProjectId', + sdk: { + name: 'testSdkName', + version: 'testSdkVersion', + }, + sent_at: expect.any(String), + }); + }); + + test('creates user feedback envelope item', () => { + const envelope = createCheckInEnvelope({ + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + }); + + expect(envelope[1]).toEqual([ + [ + { + type: 'check_in', + }, + { + check_in_id: '83a7c03ed0a04e1b97e2e3b18d38f244', + monitor_slug: 'b7645b8e-b47d-4398-be9a-d16b0dac31cb', + status: 'ok', + duration: 10.0, + release: '1.0.0', + environment: 'production', + }, + ], + ]); + }); +}); diff --git a/packages/types/src/checkin.ts b/packages/types/src/checkin.ts new file mode 100644 index 000000000000..071afce9640d --- /dev/null +++ b/packages/types/src/checkin.ts @@ -0,0 +1,13 @@ +// https://develop.sentry.dev/sdk/check-ins/ +export interface CheckIn { + // Check-In ID (unique and client generated). + check_in_id: string; + // The distinct slug of the monitor. + monitor_slug: string; + // The status of the check-in. + status: 'in_progress' | 'ok' | 'error'; + // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + duration?: number; + release?: string; + environment?: string; +} diff --git a/packages/types/src/datacategory.ts b/packages/types/src/datacategory.ts index 3456ce2c757d..06f64c8525bb 100644 --- a/packages/types/src/datacategory.ts +++ b/packages/types/src/datacategory.ts @@ -1,7 +1,7 @@ // This type is used in various places like Client Reports and Rate Limit Categories // See: // - https://develop.sentry.dev/sdk/rate-limiting/#definitions -// - https://github.com/getsentry/relay/blob/10874b587bb676bd6d50ad42d507216513660082/relay-common/src/constants.rs#L97-L113 +// - https://github.com/getsentry/relay/blob/c3b339e151c1e548ede489a01c65db82472c8751/relay-common/src/constants.rs#L139-L152 // - https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload under `discarded_events` export type DataCategory = // Reserved and only used in edgecases, unlikely to be ever actually used @@ -21,4 +21,6 @@ export type DataCategory = // SDK internal event, like client_reports | 'internal' // Profile event type - | 'profile'; + | 'profile' + // Check-in event (monitor) + | 'monitor'; diff --git a/packages/types/src/envelope.ts b/packages/types/src/envelope.ts index 2234317ef8ce..be146bdd6fc5 100644 --- a/packages/types/src/envelope.ts +++ b/packages/types/src/envelope.ts @@ -1,3 +1,4 @@ +import type { CheckIn } from './checkin'; import type { ClientReport } from './clientreport'; import type { DsnComponents } from './dsn'; import type { Event } from './event'; @@ -31,7 +32,8 @@ export type EnvelopeItemType = | 'event' | 'profile' | 'replay_event' - | 'replay_recording'; + | 'replay_recording' + | 'check_in'; export type BaseEnvelopeHeaders = { [key: string]: unknown; @@ -68,6 +70,7 @@ type SessionAggregatesItemHeaders = { type: 'sessions' }; type ClientReportItemHeaders = { type: 'client_report' }; type ReplayEventItemHeaders = { type: 'replay_event' }; type ReplayRecordingItemHeaders = { type: 'replay_recording'; length: number }; +type CheckInItemHeaders = { type: 'check_in' }; export type EventItem = BaseEnvelopeItem; export type AttachmentItem = BaseEnvelopeItem; @@ -76,11 +79,13 @@ export type SessionItem = | BaseEnvelopeItem | BaseEnvelopeItem; export type ClientReportItem = BaseEnvelopeItem; +export type CheckInItem = BaseEnvelopeItem; type ReplayEventItem = BaseEnvelopeItem; type ReplayRecordingItem = BaseEnvelopeItem; export type EventEnvelopeHeaders = { event_id: string; sent_at: string; trace?: DynamicSamplingContext }; type SessionEnvelopeHeaders = { sent_at: string }; +type CheckInEnvelopeHeaders = BaseEnvelopeHeaders; type ClientReportEnvelopeHeaders = BaseEnvelopeHeaders; type ReplayEnvelopeHeaders = BaseEnvelopeHeaders; @@ -88,6 +93,7 @@ export type EventEnvelope = BaseEnvelope; export type ClientReportEnvelope = BaseEnvelope; export type ReplayEnvelope = [ReplayEnvelopeHeaders, [ReplayEventItem, ReplayRecordingItem]]; +export type CheckInEvelope = BaseEnvelope; -export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope; +export type Envelope = EventEnvelope | SessionEnvelope | ClientReportEnvelope | ReplayEnvelope | CheckInEvelope; export type EnvelopeItem = Envelope[1][number]; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 7768a73fb6da..f0a30806ed6e 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -30,6 +30,8 @@ export type { SessionEnvelope, SessionItem, UserFeedbackItem, + CheckInItem, + CheckInEvelope, } from './envelope'; export type { ExtendedError } from './error'; export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from './event'; @@ -104,3 +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'; diff --git a/packages/utils/src/envelope.ts b/packages/utils/src/envelope.ts index 705326b6c9ba..580a50d019e0 100644 --- a/packages/utils/src/envelope.ts +++ b/packages/utils/src/envelope.ts @@ -207,6 +207,7 @@ const ITEM_TYPE_TO_DATA_CATEGORY_MAP: Record = { profile: 'profile', replay_event: 'replay', replay_recording: 'replay', + check_in: 'monitor', }; /**