appsignal 4.0.3 → 4.0.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41324be9bb4bd42bfcee997c778b0ae1eb0a1a773e044503eab2466779f3bcb7
4
- data.tar.gz: 1ab95f249d94eedba7e86e36d3f74d217bd57deed236e6b352b190dc34007eb5
3
+ metadata.gz: 91e5aad04da2524d3d2f4bd983ecd76cf1a33380a1bc2a6bf8aab1bd4d91db5b
4
+ data.tar.gz: c7d0582debd5c6d9ed29f2239a4baeed92ebdad46606c423e7537495b4dea3ab
5
5
  SHA512:
6
- metadata.gz: f8e8e89b583dc585b83fcfc70378f5090e0854d7c19b2f375a68739fc1325bcd184d8ab678fdb68af55e32d81d60ecbb764d3768f0d2c02ea6d1a974b808018c
7
- data.tar.gz: d25b331756ceafe336d5b430577fb98da0696f5f92613a47d89f30007b910a4057cf981ddb1d841d295cfdb31f48d69bb8fecb1e7071d933dba47b1160d701bf
6
+ metadata.gz: a482b32ffa5d9ddc805a65507ab4d526f9c299969c86123743e8f0c4830af6f8abd3eecd805cb7c4eec6abcd474a2f97704ac9f5129825b7958245cc973ff6db
7
+ data.tar.gz: c0bf6a6ba6454fee105c6890db3154e03c0ad64c6b5ab325fb0186edf99066e923d7b6b4043ea4d77428155fbc58371ece1309517109437edc8cc265ffda554a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 4.0.4
4
+
5
+ _Published on 2024-08-29._
6
+
7
+ ### Changed
8
+
9
+ - Send check-ins concurrently. When calling `Appsignal::CheckIn.cron`, instead of blocking the current thread while the check-in events are sent, schedule them to be sent in a separate thread.
10
+
11
+ When shutting down your application manually, call `Appsignal.stop` to block until all scheduled check-ins have been sent.
12
+
13
+ (patch [46d4ca74](https://github.com/appsignal/appsignal-ruby/commit/46d4ca74f4c188cc011653ed23969ad7ec770812))
14
+
15
+ ### Fixed
16
+
17
+ - Make our Rack BodyWrapper behave like a Rack BodyProxy. If a method doesn't exist on our BodyWrapper class, but it does exist on the body, behave like the Rack BodyProxy and call the method on the wrapped body. (patch [e2376305](https://github.com/appsignal/appsignal-ruby/commit/e23763058a3fb980f1054e9c1eaf7e0f25f75666))
18
+ - Do not report `SignalException` errors from our `at_exit` error reporter. (patch [3ba3ce31](https://github.com/appsignal/appsignal-ruby/commit/3ba3ce31ee3f3e84665c9f2f18d488c689cff6c2))
19
+
3
20
  ## 4.0.3
4
21
 
5
22
  _Published on 2024-08-26._
@@ -3,15 +3,6 @@
3
3
  module Appsignal
4
4
  module CheckIn
5
5
  class Cron
6
- class << self
7
- # @api private
8
- def transmitter
9
- @transmitter ||= Appsignal::Transmitter.new(
10
- "#{Appsignal.config[:logging_endpoint]}/check_ins/json"
11
- )
12
- end
13
- end
14
-
15
6
  # @api private
16
7
  attr_reader :identifier, :digest
17
8
 
@@ -21,11 +12,11 @@ module Appsignal
21
12
  end
22
13
 
23
14
  def start
24
- transmit_event("start")
15
+ CheckIn.scheduler.schedule(event("start"))
25
16
  end
26
17
 
27
18
  def finish
28
- transmit_event("finish")
19
+ CheckIn.scheduler.schedule(event("finish"))
29
20
  end
30
21
 
31
22
  private
@@ -39,29 +30,6 @@ module Appsignal
39
30
  :check_in_type => "cron"
40
31
  }
41
32
  end
42
-
43
- def transmit_event(kind)
44
- unless Appsignal.active?
45
- Appsignal.internal_logger.debug(
46
- "AppSignal not active, not transmitting cron check-in event"
47
- )
48
- return
49
- end
50
-
51
- response = self.class.transmitter.transmit(event(kind))
52
-
53
- if response.code.to_i >= 200 && response.code.to_i < 300
54
- Appsignal.internal_logger.debug(
55
- "Transmitted cron check-in `#{identifier}` (#{digest}) #{kind} event"
56
- )
57
- else
58
- Appsignal.internal_logger.error(
59
- "Failed to transmit cron check-in #{kind} event: status code was #{response.code}"
60
- )
61
- end
62
- rescue => e
63
- Appsignal.internal_logger.error("Failed to transmit cron check-in #{kind} event: #{e}")
64
- end
65
33
  end
66
34
  end
67
35
  end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module CheckIn
5
+ class Scheduler
6
+ INITIAL_DEBOUNCE_SECONDS = 0.1
7
+ BETWEEN_TRANSMISSIONS_DEBOUNCE_SECONDS = 10
8
+
9
+ def initialize
10
+ # The mutex is used to synchronize access to the events array, the
11
+ # waker thread and the main thread, as well as queue writes
12
+ # (which depend on the events array) and closes (so they do not
13
+ # happen at the same time that an event is added to the scheduler)
14
+ @mutex = Mutex.new
15
+ # The transmitter thread will be started when an event is first added.
16
+ @thread = nil
17
+ @queue = Thread::Queue.new
18
+ # Scheduled events that have not been sent to the transmitter thread
19
+ # yet. A copy of this array is pushed to the queue by the waker thread
20
+ # after it has awaited the debounce period.
21
+ @events = []
22
+ # The waker thread is used to schedule debounces. It will be started
23
+ # when an event is first added.
24
+ @waker = nil
25
+ # For internal testing purposes.
26
+ @transmitted = 0
27
+ end
28
+
29
+ def schedule(event)
30
+ unless Appsignal.active?
31
+ Appsignal.internal_logger.debug(
32
+ "Cannot transmit #{describe([event])}: AppSignal is not active"
33
+ )
34
+ return
35
+ end
36
+
37
+ @mutex.synchronize do
38
+ if @queue.closed?
39
+ Appsignal.internal_logger.debug(
40
+ "Cannot transmit #{describe([event])}: AppSignal is stopped"
41
+ )
42
+ return
43
+ end
44
+ add_event(event)
45
+ # If we're not already waiting to be awakened from a scheduled
46
+ # debounce, schedule a short debounce, which will push the events
47
+ # to the queue and schedule a long debounce.
48
+ start_waker(INITIAL_DEBOUNCE_SECONDS) if @waker.nil?
49
+
50
+ Appsignal.internal_logger.debug(
51
+ "Scheduling #{describe([event])} to be transmitted"
52
+ )
53
+
54
+ # Make sure to start the thread after an event has been added.
55
+ @thread ||= Thread.new(&method(:run))
56
+ end
57
+ end
58
+
59
+ def stop
60
+ @mutex.synchronize do
61
+ # Flush all events before closing the queue.
62
+ push_events
63
+ rescue ClosedQueueError
64
+ # The queue is already closed (by a previous call to `#stop`)
65
+ # so it is not possible to push events to it anymore.
66
+ ensure
67
+ # Ensure calling `#stop` closes the queue and kills
68
+ # the waker thread, disallowing any further events from being
69
+ # scheduled with `#schedule`.
70
+ stop_waker
71
+ @queue.close
72
+
73
+ # Block until the thread has finished.
74
+ @thread&.join
75
+ end
76
+ end
77
+
78
+ # @api private
79
+ # For internal testing purposes.
80
+ attr_reader :thread, :waker, :queue, :events, :transmitted
81
+
82
+ private
83
+
84
+ def run
85
+ loop do
86
+ events = @queue.pop
87
+ break if events.nil?
88
+
89
+ transmit(events)
90
+ @transmitted += 1
91
+ end
92
+ end
93
+
94
+ def transmit(events)
95
+ description = describe(events)
96
+
97
+ begin
98
+ response = CheckIn.transmitter.transmit(events, :format => :ndjson)
99
+
100
+ if (200...300).include?(response.code.to_i)
101
+ Appsignal.internal_logger.debug(
102
+ "Transmitted #{description}"
103
+ )
104
+ else
105
+ Appsignal.internal_logger.error(
106
+ "Failed to transmit #{description}: #{response.code} status code"
107
+ )
108
+ end
109
+ rescue => e
110
+ Appsignal.internal_logger.error("Failed to transmit #{description}: #{e.message}")
111
+ end
112
+ end
113
+
114
+ def describe(events)
115
+ if events.empty?
116
+ # This shouldn't happen.
117
+ "no check-in events"
118
+ elsif events.length > 1
119
+ "#{events.length} check-in events"
120
+ else
121
+ event = events.first
122
+ if event[:check_in_type] == "cron"
123
+ "cron check-in `#{event[:identifier] || "unknown"}` " \
124
+ "#{event[:kind] || "unknown"} event (digest #{event[:digest] || "unknown"})" \
125
+ else
126
+ "unknown check-in event"
127
+ end
128
+ end
129
+ end
130
+
131
+ # Must be called from within a `@mutex.synchronize` block.
132
+ def add_event(event)
133
+ # Remove redundant events, keeping the newly added one, which
134
+ # should be the one with the most recent timestamp.
135
+ if event[:check_in_type] == "cron"
136
+ # Remove any existing cron check-in event with the same identifier,
137
+ # digest and kind as the one we're adding.
138
+ @events.reject! do |existing_event|
139
+ next unless existing_event[:identifier] == event[:identifier] &&
140
+ existing_event[:digest] == event[:digest] &&
141
+ existing_event[:kind] == event[:kind] &&
142
+ existing_event[:check_in_type] == "cron"
143
+
144
+ Appsignal.internal_logger.debug(
145
+ "Replacing previously scheduled #{describe([existing_event])}"
146
+ )
147
+
148
+ true
149
+ end
150
+ end
151
+
152
+ @events << event
153
+ end
154
+
155
+ # Must be called from within a `@mutex.synchronize` block.
156
+ def start_waker(debounce)
157
+ stop_waker
158
+
159
+ @waker = Thread.new do
160
+ sleep(debounce)
161
+
162
+ @mutex.synchronize do
163
+ # Make sure this waker doesn't get killed, so it can push
164
+ # events and schedule a new waker.
165
+ @waker = nil
166
+ push_events
167
+ end
168
+ end
169
+ end
170
+
171
+ # Must be called from within a `@mutex.synchronize` block.
172
+ def stop_waker
173
+ @waker&.kill
174
+ @waker&.join
175
+ @waker = nil
176
+ end
177
+
178
+ # Must be called from within a `@mutex.synchronize` block.
179
+ def push_events
180
+ return if @events.empty?
181
+
182
+ # Push a copy of the events to the queue, and clear the events array.
183
+ # This ensures that `@events` always contains events that have not
184
+ # yet been pushed to the queue.
185
+ @queue.push(@events.dup)
186
+ @events.clear
187
+
188
+ start_waker(BETWEEN_TRANSMISSIONS_DEBOUNCE_SECONDS)
189
+ end
190
+ end
191
+ end
192
+ end
@@ -39,8 +39,26 @@ module Appsignal
39
39
  cron.finish
40
40
  output
41
41
  end
42
+
43
+ # @api private
44
+ def transmitter
45
+ @transmitter ||= Transmitter.new(
46
+ "#{Appsignal.config[:logging_endpoint]}/check_ins/json"
47
+ )
48
+ end
49
+
50
+ # @api private
51
+ def scheduler
52
+ @scheduler ||= Scheduler.new
53
+ end
54
+
55
+ # @api private
56
+ def stop
57
+ scheduler&.stop
58
+ end
42
59
  end
43
60
  end
44
61
  end
45
62
 
63
+ require "appsignal/check_in/scheduler"
46
64
  require "appsignal/check_in/cron"
@@ -150,7 +150,7 @@ module Appsignal
150
150
  ENV.fetch("APPSIGNAL_DIAGNOSE_ENDPOINT", DIAGNOSE_ENDPOINT),
151
151
  Appsignal.config
152
152
  )
153
- response = transmitter.transmit(:diagnose => data)
153
+ response = transmitter.transmit({ :diagnose => data })
154
154
 
155
155
  unless response.code == "200"
156
156
  puts " Error: Something went wrong while submitting the report " \
@@ -35,7 +35,8 @@ module Appsignal
35
35
 
36
36
  IGNORED_ERRORS = [
37
37
  # Normal exits from the application we do not need to report
38
- SystemExit
38
+ SystemExit,
39
+ SignalException
39
40
  ].freeze
40
41
 
41
42
  def self.ignored_error?(error)
@@ -57,6 +57,21 @@ module Appsignal
57
57
  @transaction.set_error(error)
58
58
  raise error
59
59
  end
60
+
61
+ # Return whether the wrapped body responds to the method if this class does not.
62
+ # Based on:
63
+ # https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L16-L24
64
+ def respond_to_missing?(method_name, include_all = false)
65
+ super || @body.respond_to?(method_name, include_all)
66
+ end
67
+
68
+ # Delegate missing methods to the wrapped body.
69
+ # Based on:
70
+ # https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L44-L61
71
+ def method_missing(method_name, *args, &block)
72
+ @body.__send__(method_name, *args, &block)
73
+ end
74
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
60
75
  end
61
76
 
62
77
  # The standard Rack body wrapper which exposes "each" for iterating
@@ -9,7 +9,8 @@ require "json"
9
9
  module Appsignal
10
10
  # @api private
11
11
  class Transmitter
12
- CONTENT_TYPE = "application/json; charset=UTF-8"
12
+ JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
13
+ NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=UTF-8"
13
14
 
14
15
  HTTP_ERRORS = [
15
16
  EOFError,
@@ -53,17 +54,39 @@ module Appsignal
53
54
  end
54
55
  end
55
56
 
56
- def transmit(payload)
57
- config.logger.debug "Transmitting payload to #{uri}"
58
- http_client.request(http_post(payload))
57
+ def transmit(payload, format: :json)
58
+ Appsignal.internal_logger.debug "Transmitting payload to #{uri}"
59
+ http_client.request(http_post(payload, :format => format))
59
60
  end
60
61
 
61
62
  private
62
63
 
63
- def http_post(payload)
64
+ def http_post(payload, format: :json)
64
65
  Net::HTTP::Post.new(uri.request_uri).tap do |request|
65
- request["Content-Type"] = CONTENT_TYPE
66
- request.body = Appsignal::Utils::JSON.generate(payload)
66
+ request["Content-Type"] = content_type_for(format)
67
+ request.body = generate_body_for(format, payload)
68
+ end
69
+ end
70
+
71
+ def content_type_for(format)
72
+ case format
73
+ when :json
74
+ JSON_CONTENT_TYPE
75
+ when :ndjson
76
+ NDJSON_CONTENT_TYPE
77
+ else
78
+ raise ArgumentError, "Unknown Content-Type header for format: #{format}"
79
+ end
80
+ end
81
+
82
+ def generate_body_for(format, payload)
83
+ case format
84
+ when :json
85
+ Appsignal::Utils::JSON.generate(payload)
86
+ when :ndjson
87
+ Appsignal::Utils::NDJSON.generate(payload)
88
+ else
89
+ raise ArgumentError, "Unknown body generator for format: #{format}"
67
90
  end
68
91
  end
69
92
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Utils
5
+ class NDJSON
6
+ class << self
7
+ def generate(body)
8
+ body.map do |element|
9
+ Appsignal::Utils::JSON.generate(element)
10
+ end.join("\n")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,4 +12,5 @@ require "appsignal/utils/data"
12
12
  require "appsignal/utils/hash_sanitizer"
13
13
  require "appsignal/utils/integration_logger"
14
14
  require "appsignal/utils/json"
15
+ require "appsignal/utils/ndjson"
15
16
  require "appsignal/utils/query_params_sanitizer"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.0.3"
4
+ VERSION = "4.0.4"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -164,6 +164,7 @@ module Appsignal
164
164
  end
165
165
  Appsignal::Extension.stop
166
166
  Appsignal::Probes.stop
167
+ Appsignal::CheckIn.stop
167
168
  end
168
169
 
169
170
  # Configure the AppSignal Ruby gem using a DSL.
@@ -0,0 +1,210 @@
1
+ describe Appsignal::CheckIn::Cron do
2
+ let(:config) { project_fixture_config }
3
+ let(:cron_checkin) { described_class.new(:identifier => "cron-checkin-name") }
4
+ let(:transmitter) { Appsignal::Transmitter.new("https://checkin-endpoint.invalid") }
5
+ let(:scheduler) { Appsignal::CheckIn::Scheduler.new }
6
+
7
+ before do
8
+ allow(Appsignal).to receive(:active?).and_return(true)
9
+ config.logger = Logger.new(StringIO.new)
10
+ allow(Appsignal::CheckIn).to receive(:scheduler).and_return(scheduler)
11
+ allow(Appsignal::CheckIn).to receive(:transmitter).and_return(transmitter)
12
+ end
13
+
14
+ after do
15
+ scheduler.stop
16
+ end
17
+
18
+ describe "when Appsignal is not active" do
19
+ it "should not transmit any events" do
20
+ allow(Appsignal).to receive(:active?).and_return(false)
21
+
22
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
23
+ message.include?("Cannot transmit cron check-in `cron-checkin-name` start event") &&
24
+ message.include?("AppSignal is not active")
25
+ end)
26
+
27
+ cron_checkin.start
28
+
29
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
30
+ message.include?("Cannot transmit cron check-in `cron-checkin-name` finish event") &&
31
+ message.include?("AppSignal is not active")
32
+ end)
33
+
34
+ cron_checkin.finish
35
+
36
+ expect(transmitter).not_to receive(:transmit)
37
+
38
+ scheduler.stop
39
+ end
40
+ end
41
+
42
+ describe "when AppSignal is stopped" do
43
+ it "should not transmit any events" do
44
+ expect(transmitter).not_to receive(:transmit)
45
+
46
+ expect(Appsignal.internal_logger).to receive(:debug).with("Stopping AppSignal")
47
+
48
+ Appsignal.stop
49
+
50
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
51
+ message.include?("Cannot transmit cron check-in `cron-checkin-name` start event") &&
52
+ message.include?("AppSignal is stopped")
53
+ end)
54
+
55
+ cron_checkin.start
56
+
57
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
58
+ message.include?("Cannot transmit cron check-in `cron-checkin-name` finish event") &&
59
+ message.include?("AppSignal is stopped")
60
+ end)
61
+
62
+ cron_checkin.finish
63
+
64
+ expect(Appsignal.internal_logger).to receive(:debug).with("Stopping AppSignal")
65
+
66
+ Appsignal.stop
67
+ end
68
+ end
69
+
70
+ describe "#start" do
71
+ it "should send a cron check-in start" do
72
+ expect(Appsignal.internal_logger).not_to receive(:error)
73
+
74
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
75
+ message.include?("Scheduling cron check-in `cron-checkin-name` start event")
76
+ end)
77
+
78
+ cron_checkin.start
79
+
80
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
81
+ message.include?("Transmitted cron check-in `cron-checkin-name` start event")
82
+ end)
83
+
84
+ expect(transmitter).to receive(:transmit).with([hash_including(
85
+ :identifier => "cron-checkin-name",
86
+ :kind => "start",
87
+ :check_in_type => "cron"
88
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "200", nil))
89
+
90
+ scheduler.stop
91
+ end
92
+
93
+ it "should log an error if it fails" do
94
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
95
+ message.include?("Scheduling cron check-in `cron-checkin-name` start event")
96
+ end)
97
+
98
+ cron_checkin.start
99
+
100
+ expect(Appsignal.internal_logger).to receive(:error).with(satisfy do |message|
101
+ message.include?("Failed to transmit cron check-in `cron-checkin-name` start event") &&
102
+ message.include?("499 status code")
103
+ end)
104
+
105
+ expect(transmitter).to receive(:transmit).with([hash_including(
106
+ :identifier => "cron-checkin-name",
107
+ :kind => "start",
108
+ :check_in_type => "cron"
109
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "499", nil))
110
+
111
+ scheduler.stop
112
+ end
113
+ end
114
+
115
+ describe "#finish" do
116
+ it "should send a cron check-in finish" do
117
+ expect(Appsignal.internal_logger).not_to receive(:error)
118
+
119
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
120
+ message.include?("Scheduling cron check-in `cron-checkin-name` finish event")
121
+ end)
122
+
123
+ cron_checkin.finish
124
+
125
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
126
+ message.include?("Transmitted cron check-in `cron-checkin-name` finish event")
127
+ end)
128
+
129
+ expect(transmitter).to receive(:transmit).with([hash_including(
130
+ :identifier => "cron-checkin-name",
131
+ :kind => "finish",
132
+ :check_in_type => "cron"
133
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "200", nil))
134
+
135
+ scheduler.stop
136
+ end
137
+
138
+ it "should log an error if it fails" do
139
+ expect(Appsignal.internal_logger).to receive(:debug).with(satisfy do |message|
140
+ message.include?("Scheduling cron check-in `cron-checkin-name` finish event")
141
+ end)
142
+
143
+ cron_checkin.finish
144
+
145
+ expect(Appsignal.internal_logger).to receive(:error).with(satisfy do |message|
146
+ message.include?("Failed to transmit cron check-in `cron-checkin-name` finish event") &&
147
+ message.include?("499 status code")
148
+ end)
149
+
150
+ expect(transmitter).to receive(:transmit).with([hash_including(
151
+ :identifier => "cron-checkin-name",
152
+ :kind => "finish",
153
+ :check_in_type => "cron"
154
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "499", nil))
155
+
156
+ scheduler.stop
157
+ end
158
+ end
159
+
160
+ describe ".cron" do
161
+ describe "when a block is given" do
162
+ it "should send a cron check-in start and finish and return the block output" do
163
+ expect(scheduler).to receive(:schedule).with(hash_including(
164
+ :kind => "start",
165
+ :identifier => "cron-checkin-with-block",
166
+ :check_in_type => "cron"
167
+ ))
168
+
169
+ expect(scheduler).to receive(:schedule).with(hash_including(
170
+ :kind => "finish",
171
+ :identifier => "cron-checkin-with-block",
172
+ :check_in_type => "cron"
173
+ ))
174
+
175
+ output = Appsignal::CheckIn.cron("cron-checkin-with-block") { "output" }
176
+ expect(output).to eq("output")
177
+ end
178
+
179
+ it "should not send a cron check-in finish event when an error is raised" do
180
+ expect(scheduler).to receive(:schedule).with(hash_including(
181
+ :kind => "start",
182
+ :identifier => "cron-checkin-with-block",
183
+ :check_in_type => "cron"
184
+ ))
185
+
186
+ expect(scheduler).not_to receive(:schedule).with(hash_including(
187
+ :kind => "finish",
188
+ :identifier => "cron-checkin-with-block",
189
+ :check_in_type => "cron"
190
+ ))
191
+
192
+ expect do
193
+ Appsignal::CheckIn.cron("cron-checkin-with-block") { raise "error" }
194
+ end.to raise_error(RuntimeError, "error")
195
+ end
196
+ end
197
+
198
+ describe "when no block is given" do
199
+ it "should only send a cron check-in finish event" do
200
+ expect(scheduler).to receive(:schedule).with(hash_including(
201
+ :kind => "finish",
202
+ :identifier => "cron-checkin-without-block",
203
+ :check_in_type => "cron"
204
+ ))
205
+
206
+ Appsignal::CheckIn.cron("cron-checkin-without-block")
207
+ end
208
+ end
209
+ end
210
+ end