From 81e5401a619ad0cdccf6fbd21dbf00ab1f413cbb Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Tue, 4 Jul 2023 22:10:05 +0700 Subject: [PATCH 01/12] feat(crons): initial cron support --- check_in.go | 110 +++++++++++++++++++++++++++++++++ client.go | 16 +++++ hub.go | 19 ++++++ interfaces.go | 33 ++++++++++ marshal_test.go | 81 ++++++++++++++++++++++++ sentry.go | 6 ++ testdata/json/checkin/000.json | 8 +++ testdata/json/checkin/001.json | 18 ++++++ testdata/json/checkin/002.json | 17 +++++ 9 files changed, 308 insertions(+) create mode 100644 check_in.go create mode 100644 testdata/json/checkin/000.json create mode 100644 testdata/json/checkin/001.json create mode 100644 testdata/json/checkin/002.json diff --git a/check_in.go b/check_in.go new file mode 100644 index 000000000..6b6bc6a7e --- /dev/null +++ b/check_in.go @@ -0,0 +1,110 @@ +package sentry + +type CheckInStatus string + +const ( + CheckInStatusInProgress CheckInStatus = "in_progress" + CheckInStatusOK CheckInStatus = "ok" + CheckInStatusError CheckInStatus = "error" +) + +type checkInScheduleType string + +const ( + checkInScheduleTypeCrontab checkInScheduleType = "crontab" + checkInScheduleTypeInterval checkInScheduleType = "interval" +) + +type MonitorSchedule interface { + scheduleType() checkInScheduleType +} + +type crontabSchedule struct { + Type string `json:"type"` + Value string `json:"value"` +} + +func (c crontabSchedule) scheduleType() checkInScheduleType { + return checkInScheduleTypeCrontab +} + +// CrontabSchedule defines the MonitorSchedule with a cron format. +// Example: "8 * * * *". +func CrontabSchedule(scheduleString string) MonitorSchedule { + return crontabSchedule{ + Type: string(checkInScheduleTypeCrontab), + Value: scheduleString, + } +} + +type intervalSchedule struct { + Type string `json:"type"` + Value int64 `json:"value"` + Unit string `json:"unit"` +} + +func (i intervalSchedule) scheduleType() checkInScheduleType { + return checkInScheduleTypeInterval +} + +type MonitorScheduleUnit string + +const ( + MonitorScheduleUnitMinute MonitorScheduleUnit = "minute" + MonitorScheduleUnitHour MonitorScheduleUnit = "hour" + MonitorScheduleUnitDay MonitorScheduleUnit = "day" + MonitorScheduleUnitWeek MonitorScheduleUnit = "week" + MonitorScheduleUnitMonth MonitorScheduleUnit = "month" + MonitorScheduleUnitYear MonitorScheduleUnit = "year" +) + +// IntervalSchedule defines the MonitorSchedule with an interval format. +// +// Example: +// +// IntervalSchedule(1, sentry.MonitorScheduleUnitDay) +func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule { + return intervalSchedule{ + Type: string(checkInScheduleTypeInterval), + Value: value, + Unit: string(unit), + } +} + +type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout + Schedule MonitorSchedule `json:"schedule,omitempty"` + // The allowed allowed margin of minutes after the expected check-in time that + // the monitor will not be considered missed for. + CheckInMargin int64 `json:"check_in_margin,omitempty"` + // The allowed allowed duration in minutes that the monitor may be `in_progress` + // for before being considered failed due to timeout. + MaxRuntime int64 `json:"max_runtime,omitempty"` + // 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 string `json:"timezone,omitempty"` +} + +type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout + // The distinct slug of the monitor. + MonitorSlug string `json:"monitor_slug"` + // The status of the check-in. + Status CheckInStatus `json:"status"` + // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + Duration int64 `json:"duration,omitempty"` +} + +// serializedCheckIn is used by checkInMarshalJSON method on Event struct. +// See https://develop.sentry.dev/sdk/check-ins/ +type serializedCheckIn struct { //nolint: maligned + // Check-In ID (unique and client generated). + CheckInID string `json:"check_in_id"` + // The distinct slug of the monitor. + MonitorSlug string `json:"monitor_slug"` + // The status of the check-in. + Status CheckInStatus `json:"status"` + // The duration of the check-in in seconds. Will only take effect if the status is ok or error. + Duration int64 `json:"duration,omitempty"` + Release string `json:"release,omitempty"` + Environment string `json:"environment,omitempty"` + MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` +} diff --git a/client.go b/client.go index 41458d30b..32d620f98 100644 --- a/client.go +++ b/client.go @@ -413,6 +413,12 @@ func (client *Client) CaptureException(exception error, hint *EventHint, scope E return client.CaptureEvent(event, hint, scope) } +// CaptureCheckIn captures a check in. +func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID { + event := client.EventFromCheckIn(checkIn, monitorConfig) + return client.processEvent(event, nil, scope) +} + // CaptureEvent captures an event on the currently active client if any. // // The event must already be assembled. Typically code would instead use @@ -524,6 +530,16 @@ func (client *Client) EventFromException(exception error, level Level) *Event { return event } +// EventFromCheckIn creates a new Sentry event from the given `check_in` instance. +func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { + event := NewEvent() + event.CheckInID = uuid() + event.CheckIn = checkIn + event.MonitorConfig = monitorConfig + + return event +} + // reverse reverses the slice a in place. func reverse(a []Exception) { for i := len(a)/2 - 1; i >= 0; i-- { diff --git a/hub.go b/hub.go index ee252be44..8af673bf2 100644 --- a/hub.go +++ b/hub.go @@ -267,6 +267,25 @@ func (hub *Hub) CaptureException(exception error) *EventID { return eventID } +// CaptureCheckIn calls the method of a same nname on currently bound Client instance +// passing it a top-level Scope. +// Returns EventID if successfully, or nil if there's no Client available. +func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { + client, scope := hub.Client(), hub.Scope() + if client == nil { + return nil + } + + eventID := client.CaptureCheckIn(checkIn, monitorConfig, scope) + if eventID != nil { + hub.mu.Lock() + hub.lastEventID = *eventID + hub.mu.Unlock() + } + + return eventID +} + // AddBreadcrumb records a new breadcrumb. // // The total number of breadcrumbs that can be recorded are limited by the diff --git a/interfaces.go b/interfaces.go index 4a75ec276..4b1102887 100644 --- a/interfaces.go +++ b/interfaces.go @@ -22,6 +22,9 @@ const eventType = "event" const profileType = "profile" +// checkInType is the type of a check in event. +const checkInType = "check_in" + // Level marks the severity of the event. type Level string @@ -315,6 +318,12 @@ type Event struct { Spans []*Span `json:"spans,omitempty"` TransactionInfo *TransactionInfo `json:"transaction_info,omitempty"` + // The fields below are only relevant for crons/check ins + + CheckInID string `json:"check_in_id,omitempty"` + CheckIn *CheckIn `json:"check_in,omitempty"` + MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` + // The fields below are not part of the final JSON payload. sdkMetaData SDKMetaData @@ -375,6 +384,8 @@ func (e *Event) MarshalJSON() ([]byte, error) { // and a few type tricks. if e.Type == transactionType { return e.transactionMarshalJSON() + } else if e.Type == checkInType { + return e.checkInMarshalJSON() } return e.defaultMarshalJSON() } @@ -449,6 +460,28 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) { return json.Marshal(x) } +func (e *Event) checkInMarshalJSON() ([]byte, error) { + checkIn := serializedCheckIn{ + CheckInID: e.CheckInID, + MonitorSlug: e.CheckIn.MonitorSlug, + Status: e.CheckIn.Status, + Duration: e.CheckIn.Duration, + Release: e.Release, + Environment: e.Environment, + MonitorConfig: nil, + } + + if e.MonitorConfig != nil { + checkIn.MonitorConfig = &MonitorConfig{} + checkIn.MonitorConfig.Schedule = e.MonitorConfig.Schedule + checkIn.MonitorConfig.CheckInMargin = e.MonitorConfig.CheckInMargin + checkIn.MonitorConfig.MaxRuntime = e.MonitorConfig.MaxRuntime + checkIn.MonitorConfig.Timezone = e.MonitorConfig.Timezone + } + + return json.Marshal(checkIn) +} + // NewEvent creates a new Event. func NewEvent() *Event { event := Event{ diff --git a/marshal_test.go b/marshal_test.go index 88d02dfa7..a20cb8e85 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -125,6 +125,87 @@ func TestTransactionEventMarshalJSON(t *testing.T) { } } +func TestCheckInEventMarshalJSON(t *testing.T) { + tests := []*Event{ + { + Release: "1.0.0", + Environment: "dev", + Type: checkInType, + CheckInID: uuid(), + CheckIn: &CheckIn{ + MonitorSlug: "my-monitor", + Status: "ok", + Duration: 10, + }, + MonitorConfig: nil, + }, + { + Release: "1.0.0", + Environment: "dev", + Type: checkInType, + CheckInID: uuid(), + CheckIn: &CheckIn{ + MonitorSlug: "my-monitor", + Status: "ok", + Duration: 10, + }, + MonitorConfig: &MonitorConfig{ + Schedule: &intervalSchedule{ + Type: "interval", + Value: 1, + Unit: "day", + }, + CheckInMargin: 5, + MaxRuntime: 30, + Timezone: "America/Los_Angeles", + }, + }, + { + Release: "1.0.0", + Environment: "dev", + Type: checkInType, + CheckInID: uuid(), + CheckIn: &CheckIn{ + MonitorSlug: "my-monitor", + Status: "ok", + Duration: 10, + }, + MonitorConfig: &MonitorConfig{ + Schedule: &crontabSchedule{ + Type: "crontab", + Value: "0 * * * *", + }, + CheckInMargin: 5, + MaxRuntime: 30, + Timezone: "America/Los_Angeles", + }, + }, + } + + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetIndent("", " ") + for i, tt := range tests { + i, tt := i, tt + t.Run("", func(t *testing.T) { + defer buf.Reset() + err := enc.Encode(tt) + if err != nil { + t.Fatal(err) + } + path := filepath.Join("testdata", "json", "checkin", fmt.Sprintf("%03d.json", i)) + if *update { + WriteGoldenFile(t, path, buf.Bytes()) + } + got := buf.String() + want := ReadOrGenerateGoldenFile(t, path, buf.Bytes()) + if diff := cmp.Diff(want, got); diff != "" { + t.Fatalf("JSON mismatch (-want +got):\n%s", diff) + } + }) + } +} + func TestBreadcrumbMarshalJSON(t *testing.T) { tests := []*Breadcrumb{ // complete diff --git a/sentry.go b/sentry.go index fe8940b9d..df3c117dd 100644 --- a/sentry.go +++ b/sentry.go @@ -54,6 +54,12 @@ func CaptureException(exception error) *EventID { return hub.CaptureException(exception) } +// CaptureCheckIn captures a check in. +func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { + hub := CurrentHub() + return hub.CaptureCheckIn(checkIn, monitorConfig) +} + // CaptureEvent captures an event on the currently active client if any. // // The event must already be assembled. Typically code would instead use diff --git a/testdata/json/checkin/000.json b/testdata/json/checkin/000.json new file mode 100644 index 000000000..deaf91933 --- /dev/null +++ b/testdata/json/checkin/000.json @@ -0,0 +1,8 @@ +{ + "check_in_id": "c2f0ce1334c74564bf6631f6161173f5", + "monitor_slug": "my-monitor", + "status": "ok", + "duration": 10, + "release": "1.0.0", + "environment": "dev" +} diff --git a/testdata/json/checkin/001.json b/testdata/json/checkin/001.json new file mode 100644 index 000000000..65daac4fc --- /dev/null +++ b/testdata/json/checkin/001.json @@ -0,0 +1,18 @@ +{ + "check_in_id": "0cde0b4e725d4504b2d07c303c2a06d5", + "monitor_slug": "my-monitor", + "status": "ok", + "duration": 10, + "release": "1.0.0", + "environment": "dev", + "monitor_config": { + "schedule": { + "type": "interval", + "value": 1, + "unit": "day" + }, + "check_in_margin": 5, + "max_runtime": 30, + "timezone": "America/Los_Angeles" + } +} diff --git a/testdata/json/checkin/002.json b/testdata/json/checkin/002.json new file mode 100644 index 000000000..864d5432b --- /dev/null +++ b/testdata/json/checkin/002.json @@ -0,0 +1,17 @@ +{ + "check_in_id": "6fa9f5f4da114025b5bf681d63716f6d", + "monitor_slug": "my-monitor", + "status": "ok", + "duration": 10, + "release": "1.0.0", + "environment": "dev", + "monitor_config": { + "schedule": { + "type": "crontab", + "value": "0 * * * *" + }, + "check_in_margin": 5, + "max_runtime": 30, + "timezone": "America/Los_Angeles" + } +} From e0e2ccf8ea9a5daf9f961645da65b127973b5051 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 9 Jul 2023 12:25:44 +0700 Subject: [PATCH 02/12] test: set CheckInID to static const --- client_test.go | 57 ++++++++++++++++++++++++++++++++++ marshal_test.go | 6 ++-- testdata/json/checkin/001.json | 2 +- testdata/json/checkin/002.json | 2 +- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index 5afce8ae7..db3883186 100644 --- a/client_test.go +++ b/client_test.go @@ -326,6 +326,63 @@ func TestCaptureEventNil(t *testing.T) { } } +func TestCaptureCheckIn(t *testing.T) { + tests := []struct { + name string + checkIn *CheckIn + monitorConfig *MonitorConfig + }{ + { + name: "Nil CheckIn", + checkIn: nil, + monitorConfig: nil, + }, + { + name: "Nil MonitorConfig", + checkIn: &CheckIn{ + MonitorSlug: "cron", + Status: CheckInStatusOK, + Duration: 100, + }, + monitorConfig: nil, + }, + { + name: "Normal", + checkIn: &CheckIn{ + MonitorSlug: "cron", + Status: CheckInStatusInProgress, + Duration: 100, + }, + monitorConfig: &MonitorConfig{ + Schedule: IntervalSchedule(1, MonitorScheduleUnitHour), + CheckInMargin: 10, + MaxRuntime: 5000, + Timezone: "Asia/Singapore", + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + client, _, transport := setupClientTest() + client.CaptureCheckIn(tt.checkIn, tt.monitorConfig, nil) + if transport.lastEvent == nil { + t.Fatal("missing event") + } + + if diff := cmp.Diff(transport.lastEvent.CheckIn, tt.checkIn); diff != "" { + t.Errorf("CheckIn mismatch (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(transport.lastEvent.MonitorConfig, tt.monitorConfig); diff != "" { + t.Errorf("CheckIn mismatch (-want +got):\n%s", diff) + } + }) + + } +} + func TestSampleRateCanDropEvent(t *testing.T) { client, scope, transport := setupClientTest() client.options.SampleRate = 0.000000000000001 diff --git a/marshal_test.go b/marshal_test.go index a20cb8e85..30a45cd42 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -131,7 +131,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: uuid(), + CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", @@ -143,7 +143,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: uuid(), + CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", @@ -164,7 +164,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: uuid(), + CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", diff --git a/testdata/json/checkin/001.json b/testdata/json/checkin/001.json index 65daac4fc..64f133db7 100644 --- a/testdata/json/checkin/001.json +++ b/testdata/json/checkin/001.json @@ -1,5 +1,5 @@ { - "check_in_id": "0cde0b4e725d4504b2d07c303c2a06d5", + "check_in_id": "c2f0ce1334c74564bf6631f6161173f5", "monitor_slug": "my-monitor", "status": "ok", "duration": 10, diff --git a/testdata/json/checkin/002.json b/testdata/json/checkin/002.json index 864d5432b..f46a69ed9 100644 --- a/testdata/json/checkin/002.json +++ b/testdata/json/checkin/002.json @@ -1,5 +1,5 @@ { - "check_in_id": "6fa9f5f4da114025b5bf681d63716f6d", + "check_in_id": "c2f0ce1334c74564bf6631f6161173f5", "monitor_slug": "my-monitor", "status": "ok", "duration": 10, From 666c5e75c5110a486c023e19896c1823015600d3 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 10 Jul 2023 19:12:29 +0700 Subject: [PATCH 03/12] Update sentry.go Co-authored-by: Anton Ovchinnikov --- sentry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry.go b/sentry.go index df3c117dd..bb53823a4 100644 --- a/sentry.go +++ b/sentry.go @@ -54,7 +54,7 @@ func CaptureException(exception error) *EventID { return hub.CaptureException(exception) } -// CaptureCheckIn captures a check in. +// CaptureCheckIn captures a (cron) monitor check-in. func CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { hub := CurrentHub() return hub.CaptureCheckIn(checkIn, monitorConfig) From c68637b92d8fc3cbea3f682d0a6a0d642d84d346 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 10 Jul 2023 19:12:38 +0700 Subject: [PATCH 04/12] Update hub.go Co-authored-by: Anton Ovchinnikov --- hub.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hub.go b/hub.go index 8af673bf2..0930cf80d 100644 --- a/hub.go +++ b/hub.go @@ -267,9 +267,9 @@ func (hub *Hub) CaptureException(exception error) *EventID { return eventID } -// CaptureCheckIn calls the method of a same nname on currently bound Client instance +// CaptureCheckIn calls the method of the same name on currently bound Client instance // passing it a top-level Scope. -// Returns EventID if successfully, or nil if there's no Client available. +// Returns EventID if the event was captured successfully, or nil otherwise. func (hub *Hub) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *EventID { client, scope := hub.Client(), hub.Scope() if client == nil { From 91727412314518773e60997d43b4ba0c4566c05d Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 10 Jul 2023 19:15:25 +0700 Subject: [PATCH 05/12] Update check_in.go Co-authored-by: Anton Ovchinnikov --- check_in.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check_in.go b/check_in.go index 6b6bc6a7e..46e15035d 100644 --- a/check_in.go +++ b/check_in.go @@ -73,7 +73,7 @@ func IntervalSchedule(value int64, unit MonitorScheduleUnit) MonitorSchedule { type MonitorConfig struct { //nolint: maligned // prefer readability over optimal memory layout Schedule MonitorSchedule `json:"schedule,omitempty"` - // The allowed allowed margin of minutes after the expected check-in time that + // The allowed margin of minutes after the expected check-in time that // the monitor will not be considered missed for. CheckInMargin int64 `json:"check_in_margin,omitempty"` // The allowed allowed duration in minutes that the monitor may be `in_progress` From 3c9fc2dcdab8f37049c57b9a0599a32bc57bf37b Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 10 Jul 2023 19:15:33 +0700 Subject: [PATCH 06/12] Update check_in.go Co-authored-by: Anton Ovchinnikov --- check_in.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/check_in.go b/check_in.go index 46e15035d..70161cfd3 100644 --- a/check_in.go +++ b/check_in.go @@ -76,7 +76,7 @@ type MonitorConfig struct { //nolint: maligned // prefer readability over optima // The allowed margin of minutes after the expected check-in time that // the monitor will not be considered missed for. CheckInMargin int64 `json:"check_in_margin,omitempty"` - // The allowed allowed duration in minutes that the monitor may be `in_progress` + // The allowed duration in minutes that the monitor may be `in_progress` // for before being considered failed due to timeout. MaxRuntime int64 `json:"max_runtime,omitempty"` // A tz database string representing the timezone which the monitor's execution schedule is in. From cb77d962eb501b1ca00bd2a15eeaa7f69e33e85b Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Sun, 16 Jul 2023 08:17:29 +0700 Subject: [PATCH 07/12] feat: modify duration to time.Duration; add test for CrontabSchedule --- check_in.go | 11 ++++++++--- client.go | 2 +- client_test.go | 20 +++++++++++++++++--- interfaces.go | 13 +++++++------ marshal_test.go | 6 +++--- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/check_in.go b/check_in.go index 70161cfd3..87902f693 100644 --- a/check_in.go +++ b/check_in.go @@ -1,5 +1,7 @@ package sentry +import "time" + type CheckInStatus string const ( @@ -16,6 +18,9 @@ const ( ) type MonitorSchedule interface { + // scheduleType is a private method that must be implemented for monitor schedule + // implementation. It should never be called. This method is made for having + // specific private implementation of MonitorSchedule interface. scheduleType() checkInScheduleType } @@ -89,8 +94,8 @@ type CheckIn struct { //nolint: maligned // prefer readability over optimal memo MonitorSlug string `json:"monitor_slug"` // The status of the check-in. Status CheckInStatus `json:"status"` - // The duration of the check-in in seconds. Will only take effect if the status is ok or error. - Duration int64 `json:"duration,omitempty"` + // The duration of the check-in. Will only take effect if the status is ok or error. + Duration time.Duration `json:"duration,omitempty"` } // serializedCheckIn is used by checkInMarshalJSON method on Event struct. @@ -103,7 +108,7 @@ type serializedCheckIn struct { //nolint: maligned // The status of the check-in. Status CheckInStatus `json:"status"` // The duration of the check-in in seconds. Will only take effect if the status is ok or error. - Duration int64 `json:"duration,omitempty"` + Duration float64 `json:"duration,omitempty"` Release string `json:"release,omitempty"` Environment string `json:"environment,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` diff --git a/client.go b/client.go index 32d620f98..c25982f02 100644 --- a/client.go +++ b/client.go @@ -416,7 +416,7 @@ func (client *Client) CaptureException(exception error, hint *EventHint, scope E // CaptureCheckIn captures a check in. func (client *Client) CaptureCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig, scope EventModifier) *EventID { event := client.EventFromCheckIn(checkIn, monitorConfig) - return client.processEvent(event, nil, scope) + return client.CaptureEvent(event, nil, scope) } // CaptureEvent captures an event on the currently active client if any. diff --git a/client_test.go b/client_test.go index db3883186..fdca9fc6e 100644 --- a/client_test.go +++ b/client_test.go @@ -342,16 +342,16 @@ func TestCaptureCheckIn(t *testing.T) { checkIn: &CheckIn{ MonitorSlug: "cron", Status: CheckInStatusOK, - Duration: 100, + Duration: time.Second * 10, }, monitorConfig: nil, }, { - name: "Normal", + name: "IntervalSchedule", checkIn: &CheckIn{ MonitorSlug: "cron", Status: CheckInStatusInProgress, - Duration: 100, + Duration: time.Second * 10, }, monitorConfig: &MonitorConfig{ Schedule: IntervalSchedule(1, MonitorScheduleUnitHour), @@ -360,6 +360,20 @@ func TestCaptureCheckIn(t *testing.T) { Timezone: "Asia/Singapore", }, }, + { + name: "CronSchedule", + checkIn: &CheckIn{ + MonitorSlug: "cron", + Status: CheckInStatusInProgress, + Duration: time.Second * 10, + }, + monitorConfig: &MonitorConfig{ + Schedule: CrontabSchedule("40 * * * *"), + CheckInMargin: 10, + MaxRuntime: 5000, + Timezone: "Asia/Singapore", + }, + }, } for _, tt := range tests { diff --git a/interfaces.go b/interfaces.go index 4b1102887..4ef1666e9 100644 --- a/interfaces.go +++ b/interfaces.go @@ -465,18 +465,19 @@ func (e *Event) checkInMarshalJSON() ([]byte, error) { CheckInID: e.CheckInID, MonitorSlug: e.CheckIn.MonitorSlug, Status: e.CheckIn.Status, - Duration: e.CheckIn.Duration, + Duration: e.CheckIn.Duration.Seconds(), Release: e.Release, Environment: e.Environment, MonitorConfig: nil, } if e.MonitorConfig != nil { - checkIn.MonitorConfig = &MonitorConfig{} - checkIn.MonitorConfig.Schedule = e.MonitorConfig.Schedule - checkIn.MonitorConfig.CheckInMargin = e.MonitorConfig.CheckInMargin - checkIn.MonitorConfig.MaxRuntime = e.MonitorConfig.MaxRuntime - checkIn.MonitorConfig.Timezone = e.MonitorConfig.Timezone + checkIn.MonitorConfig = &MonitorConfig{ + Schedule: e.MonitorConfig.Schedule, + CheckInMargin: e.MonitorConfig.CheckInMargin, + MaxRuntime: e.MonitorConfig.MaxRuntime, + Timezone: e.MonitorConfig.Timezone, + } } return json.Marshal(checkIn) diff --git a/marshal_test.go b/marshal_test.go index 30a45cd42..6af7f9b09 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -135,7 +135,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", - Duration: 10, + Duration: time.Second * 10, }, MonitorConfig: nil, }, @@ -147,7 +147,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", - Duration: 10, + Duration: time.Second * 10, }, MonitorConfig: &MonitorConfig{ Schedule: &intervalSchedule{ @@ -168,7 +168,7 @@ func TestCheckInEventMarshalJSON(t *testing.T) { CheckIn: &CheckIn{ MonitorSlug: "my-monitor", Status: "ok", - Duration: 10, + Duration: time.Second * 10, }, MonitorConfig: &MonitorConfig{ Schedule: &crontabSchedule{ From 5ec9e0baa7b6823a791601adff774f4326dfc163 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Jul 2023 19:19:09 +0700 Subject: [PATCH 08/12] chore: resolve lint errors --- client_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/client_test.go b/client_test.go index fdca9fc6e..5f8ebaf46 100644 --- a/client_test.go +++ b/client_test.go @@ -393,7 +393,6 @@ func TestCaptureCheckIn(t *testing.T) { t.Errorf("CheckIn mismatch (-want +got):\n%s", diff) } }) - } } From 73d3bdd117c6bff947a9baa04d7bdc0f7e96e765 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Jul 2023 19:31:27 +0700 Subject: [PATCH 09/12] test: check in on hub --- hub_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/hub_test.go b/hub_test.go index 23ae3746f..ac46893c3 100644 --- a/hub_test.go +++ b/hub_test.go @@ -5,6 +5,7 @@ import ( "fmt" "sync" "testing" + "time" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -190,6 +191,18 @@ func TestLastEventIDUpdatesAfterCaptures(t *testing.T) { eventID := hub.CaptureEvent(&Event{Message: "wat"}) assertEqual(t, *eventID, hub.LastEventID()) + + checkInID := hub.CaptureCheckIn(&CheckIn{ + MonitorSlug: "job", + Status: CheckInStatusOK, + Duration: time.Second * 10, + }, &MonitorConfig{ + Schedule: CrontabSchedule("8 * * * *"), + CheckInMargin: 100, + MaxRuntime: 200, + Timezone: "Asia/Singapore", + }) + assertEqual(t, *checkInID, hub.LastEventID()) } func TestLastEventIDNotChangedForTransactions(t *testing.T) { From f674960491eed8d7f20fe3a034693a330ecac512 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Jul 2023 21:05:19 +0700 Subject: [PATCH 10/12] ref(cron): take check in id as an optional user input --- check_in.go | 2 ++ client.go | 16 ++++++++++++++-- client_test.go | 3 +++ interfaces.go | 3 +-- marshal_test.go | 6 +++--- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/check_in.go b/check_in.go index 87902f693..28b2b2e29 100644 --- a/check_in.go +++ b/check_in.go @@ -90,6 +90,8 @@ type MonitorConfig struct { //nolint: maligned // prefer readability over optima } type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout + // Check-In ID (unique and client generated) + ID string `json:"check_in_id"` // The distinct slug of the monitor. MonitorSlug string `json:"monitor_slug"` // The status of the check-in. diff --git a/client.go b/client.go index c25982f02..0027dbee3 100644 --- a/client.go +++ b/client.go @@ -533,8 +533,20 @@ func (client *Client) EventFromException(exception error, level Level) *Event { // EventFromCheckIn creates a new Sentry event from the given `check_in` instance. func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { event := NewEvent() - event.CheckInID = uuid() - event.CheckIn = checkIn + if checkIn != nil { + checkInID := uuid() + + if checkIn.ID != "" { + checkInID = checkIn.ID + } + + event.CheckIn = &CheckIn{ + ID: checkInID, + MonitorSlug: checkIn.MonitorSlug, + Status: checkIn.Status, + Duration: checkIn.Duration, + } + } event.MonitorConfig = monitorConfig return event diff --git a/client_test.go b/client_test.go index 5f8ebaf46..7815405a0 100644 --- a/client_test.go +++ b/client_test.go @@ -340,6 +340,7 @@ func TestCaptureCheckIn(t *testing.T) { { name: "Nil MonitorConfig", checkIn: &CheckIn{ + ID: "66e1a05b182346f2aee5fd7f0dc9b44e", MonitorSlug: "cron", Status: CheckInStatusOK, Duration: time.Second * 10, @@ -349,6 +350,7 @@ func TestCaptureCheckIn(t *testing.T) { { name: "IntervalSchedule", checkIn: &CheckIn{ + ID: "66e1a05b182346f2aee5fd7f0dc9b44e", MonitorSlug: "cron", Status: CheckInStatusInProgress, Duration: time.Second * 10, @@ -363,6 +365,7 @@ func TestCaptureCheckIn(t *testing.T) { { name: "CronSchedule", checkIn: &CheckIn{ + ID: "66e1a05b182346f2aee5fd7f0dc9b44e", MonitorSlug: "cron", Status: CheckInStatusInProgress, Duration: time.Second * 10, diff --git a/interfaces.go b/interfaces.go index 4ef1666e9..3019c517a 100644 --- a/interfaces.go +++ b/interfaces.go @@ -320,7 +320,6 @@ type Event struct { // The fields below are only relevant for crons/check ins - CheckInID string `json:"check_in_id,omitempty"` CheckIn *CheckIn `json:"check_in,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` @@ -462,7 +461,7 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) { func (e *Event) checkInMarshalJSON() ([]byte, error) { checkIn := serializedCheckIn{ - CheckInID: e.CheckInID, + CheckInID: e.CheckIn.ID, MonitorSlug: e.CheckIn.MonitorSlug, Status: e.CheckIn.Status, Duration: e.CheckIn.Duration.Seconds(), diff --git a/marshal_test.go b/marshal_test.go index 6af7f9b09..ecf1d8134 100644 --- a/marshal_test.go +++ b/marshal_test.go @@ -131,8 +131,8 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ + ID: "c2f0ce1334c74564bf6631f6161173f5", MonitorSlug: "my-monitor", Status: "ok", Duration: time.Second * 10, @@ -143,8 +143,8 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ + ID: "c2f0ce1334c74564bf6631f6161173f5", MonitorSlug: "my-monitor", Status: "ok", Duration: time.Second * 10, @@ -164,8 +164,8 @@ func TestCheckInEventMarshalJSON(t *testing.T) { Release: "1.0.0", Environment: "dev", Type: checkInType, - CheckInID: "c2f0ce1334c74564bf6631f6161173f5", CheckIn: &CheckIn{ + ID: "c2f0ce1334c74564bf6631f6161173f5", MonitorSlug: "my-monitor", Status: "ok", Duration: time.Second * 10, From 3c0381ee1fa94b4901d160e82396edcacef49577 Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Jul 2023 21:09:29 +0700 Subject: [PATCH 11/12] fix: EventID should be equal to CheckInID --- client.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 0027dbee3..73183dc29 100644 --- a/client.go +++ b/client.go @@ -533,9 +533,8 @@ func (client *Client) EventFromException(exception error, level Level) *Event { // EventFromCheckIn creates a new Sentry event from the given `check_in` instance. func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { event := NewEvent() + checkInID := uuid() if checkIn != nil { - checkInID := uuid() - if checkIn.ID != "" { checkInID = checkIn.ID } @@ -549,6 +548,9 @@ func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorC } event.MonitorConfig = monitorConfig + // EventID should be equal to CheckInID + event.EventID = EventID(checkInID) + return event } From 5c4322883ac6714e7d1173c32c4de5349c1ec42c Mon Sep 17 00:00:00 2001 From: Reinaldy Rafli Date: Mon, 17 Jul 2023 21:24:48 +0700 Subject: [PATCH 12/12] ref(cron): enable check in from previous CheckInID --- check_in.go | 2 +- client.go | 4 ++-- client_test.go | 28 ++++++++++++++++++++++++++++ interfaces.go | 2 +- 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/check_in.go b/check_in.go index 28b2b2e29..aca7e36b1 100644 --- a/check_in.go +++ b/check_in.go @@ -91,7 +91,7 @@ type MonitorConfig struct { //nolint: maligned // prefer readability over optima type CheckIn struct { //nolint: maligned // prefer readability over optimal memory layout // Check-In ID (unique and client generated) - ID string `json:"check_in_id"` + ID EventID `json:"check_in_id"` // The distinct slug of the monitor. MonitorSlug string `json:"monitor_slug"` // The status of the check-in. diff --git a/client.go b/client.go index 73183dc29..5f844ce67 100644 --- a/client.go +++ b/client.go @@ -533,7 +533,7 @@ func (client *Client) EventFromException(exception error, level Level) *Event { // EventFromCheckIn creates a new Sentry event from the given `check_in` instance. func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorConfig) *Event { event := NewEvent() - checkInID := uuid() + checkInID := EventID(uuid()) if checkIn != nil { if checkIn.ID != "" { checkInID = checkIn.ID @@ -549,7 +549,7 @@ func (client *Client) EventFromCheckIn(checkIn *CheckIn, monitorConfig *MonitorC event.MonitorConfig = monitorConfig // EventID should be equal to CheckInID - event.EventID = EventID(checkInID) + event.EventID = checkInID return event } diff --git a/client_test.go b/client_test.go index 7815405a0..29450430a 100644 --- a/client_test.go +++ b/client_test.go @@ -399,6 +399,34 @@ func TestCaptureCheckIn(t *testing.T) { } } +func TestCaptureCheckInExistingID(t *testing.T) { + client, _, _ := setupClientTest() + + monitorConfig := &MonitorConfig{ + Schedule: IntervalSchedule(1, MonitorScheduleUnitDay), + CheckInMargin: 30, + MaxRuntime: 30, + Timezone: "UTC", + } + + checkInID := client.CaptureCheckIn(&CheckIn{ + MonitorSlug: "cron", + Status: CheckInStatusInProgress, + Duration: time.Second, + }, monitorConfig, nil) + + checkInID2 := client.CaptureCheckIn(&CheckIn{ + ID: *checkInID, + MonitorSlug: "cron", + Status: CheckInStatusOK, + Duration: time.Minute, + }, monitorConfig, nil) + + if *checkInID != *checkInID2 { + t.Errorf("Expecting equivalent CheckInID: %s and %s", *checkInID, *checkInID2) + } +} + func TestSampleRateCanDropEvent(t *testing.T) { client, scope, transport := setupClientTest() client.options.SampleRate = 0.000000000000001 diff --git a/interfaces.go b/interfaces.go index 3019c517a..f87229474 100644 --- a/interfaces.go +++ b/interfaces.go @@ -461,7 +461,7 @@ func (e *Event) transactionMarshalJSON() ([]byte, error) { func (e *Event) checkInMarshalJSON() ([]byte, error) { checkIn := serializedCheckIn{ - CheckInID: e.CheckIn.ID, + CheckInID: string(e.CheckIn.ID), MonitorSlug: e.CheckIn.MonitorSlug, Status: e.CheckIn.Status, Duration: e.CheckIn.Duration.Seconds(),