appsignal 2.11.0.alpha.1-java → 2.11.0.beta.4-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +75 -61
  4. data/CHANGELOG.md +21 -1
  5. data/build_matrix.yml +13 -7
  6. data/ext/agent.yml +19 -19
  7. data/gemfiles/padrino.gemfile +2 -2
  8. data/gemfiles/rails-4.2.gemfile +9 -2
  9. data/gemfiles/rails-5.0.gemfile +1 -0
  10. data/gemfiles/rails-5.1.gemfile +1 -0
  11. data/gemfiles/rails-5.2.gemfile +1 -0
  12. data/gemfiles/rails-6.0.gemfile +1 -0
  13. data/gemfiles/resque-1.gemfile +7 -0
  14. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  15. data/lib/appsignal/hooks.rb +2 -0
  16. data/lib/appsignal/hooks/active_job.rb +114 -0
  17. data/lib/appsignal/hooks/puma.rb +2 -58
  18. data/lib/appsignal/hooks/resque.rb +60 -0
  19. data/lib/appsignal/hooks/sidekiq.rb +19 -192
  20. data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
  21. data/lib/appsignal/integrations/que.rb +1 -1
  22. data/lib/appsignal/integrations/resque.rb +9 -12
  23. data/lib/appsignal/integrations/resque_active_job.rb +9 -32
  24. data/lib/appsignal/probes/puma.rb +61 -0
  25. data/lib/appsignal/probes/sidekiq.rb +102 -0
  26. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  27. data/lib/appsignal/transaction.rb +10 -0
  28. data/lib/appsignal/utils/deprecation_message.rb +5 -1
  29. data/lib/appsignal/version.rb +1 -1
  30. data/lib/puma/plugin/appsignal.rb +2 -1
  31. data/spec/lib/appsignal/hooks/activejob_spec.rb +548 -0
  32. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +3 -14
  33. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  34. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  35. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +297 -549
  36. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  37. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  38. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -179
  39. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  40. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  41. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  42. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  43. data/spec/lib/appsignal/transaction_spec.rb +5 -7
  44. data/spec/lib/puma/appsignal_spec.rb +1 -1
  45. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  46. data/spec/support/helpers/dependency_helper.rb +9 -2
  47. data/spec/support/helpers/transaction_helpers.rb +6 -0
  48. data/spec/support/stubs/sidekiq/api.rb +2 -2
  49. metadata +18 -3
@@ -64,7 +64,7 @@ module Appsignal
64
64
  dot_split = default_name.split(".")
65
65
  return default_name if dot_split.length == 2
66
66
 
67
- ["unknown"]
67
+ "#{default_name}#perform"
68
68
  end
69
69
 
70
70
  def self.extract_value(object_or_hash, field, default_value = nil, convert_to_s = false)
@@ -32,7 +32,7 @@ module Appsignal
32
32
  transaction.set_error(error)
33
33
  raise error
34
34
  ensure
35
- transaction.set_action "#{local_attrs[:job_class]}#run"
35
+ transaction.set_action_if_nil "#{local_attrs[:job_class]}#run"
36
36
  Appsignal::Transaction.complete_current!
37
37
  end
38
38
  end
@@ -4,18 +4,15 @@ module Appsignal
4
4
  module Integrations
5
5
  # @api private
6
6
  module ResquePlugin
7
- # Do not use this file as a template for your own background processor
8
- # Resque is an exception to the rule and the code below causes the
9
- # extension to shut itself down after a single job.
10
- # see http://docs.appsignal.com/background-monitoring/custom.html
11
- def around_perform_resque_plugin(*_args)
12
- Appsignal.monitor_single_transaction(
13
- "perform_job.resque",
14
- :class => to_s,
15
- :method => "perform"
16
- ) do
17
- yield
18
- end
7
+ def self.extended(_)
8
+ callers = caller
9
+ Appsignal::Utils::DeprecationMessage.message \
10
+ "The AppSignal ResquePlugin is deprecated and does " \
11
+ "nothing on extend. In this version of the AppSignal Ruby gem " \
12
+ "the integration with Resque is automatic on all Resque workers. " \
13
+ "Please remove the following line from this file to remove this " \
14
+ "message: extend Appsignal::Integrations::ResquePlugin\n" \
15
+ "#{callers.first}"
19
16
  end
20
17
  end
21
18
  end
@@ -4,38 +4,15 @@ module Appsignal
4
4
  module Integrations
5
5
  # @api private
6
6
  module ResqueActiveJobPlugin
7
- include Appsignal::Hooks::Helpers
8
-
9
- def self.included(base)
10
- base.class_eval do
11
- around_perform do |job, block|
12
- params = Appsignal::Utils::HashSanitizer.sanitize(
13
- job.arguments,
14
- Appsignal.config[:filter_parameters]
15
- )
16
-
17
- queue_start =
18
- if job.respond_to?(:enqueued_at) && job.enqueued_at
19
- Time.parse(job.enqueued_at).utc
20
- end
21
-
22
- Appsignal.monitor_single_transaction(
23
- "perform_job.resque",
24
- :class => job.class.to_s,
25
- :method => "perform",
26
- :params => params,
27
- :queue_start => queue_start,
28
- :metadata => {
29
- :id => job.job_id,
30
- :queue => job.queue_name
31
- }
32
- ) do
33
- block.call
34
- end
35
- end
36
- end
37
-
38
- Appsignal::Environment.report("ruby_active_job_resque_enabled") { true }
7
+ def self.included(_)
8
+ callers = caller
9
+ Appsignal::Utils::DeprecationMessage.message \
10
+ "The AppSignal ResqueActiveJobPlugin is deprecated and does " \
11
+ "nothing on extend. In this version of the AppSignal Ruby gem " \
12
+ "the integration with Resque is automatic on all Resque workers. " \
13
+ "Please remove the following line from this file to remove this " \
14
+ "message: include Appsignal::Integrations::ResqueActiveJobPlugin\n" \
15
+ "#{callers.first}"
39
16
  end
40
17
  end
41
18
  end
@@ -0,0 +1,61 @@
1
+ module Appsignal
2
+ module Probes
3
+ # @api private
4
+ class PumaProbe
5
+ def initialize
6
+ @hostname = Appsignal.config[:hostname] || Socket.gethostname
7
+ end
8
+
9
+ def call
10
+ puma_stats = fetch_puma_stats
11
+ return unless puma_stats
12
+
13
+ stats = JSON.parse puma_stats, :symbolize_names => true
14
+ counts = {}
15
+ count_keys = [:backlog, :running, :pool_capacity, :max_threads]
16
+
17
+ if stats[:worker_status] # Multiple workers
18
+ stats[:worker_status].each do |worker|
19
+ stat = worker[:last_status]
20
+ count_keys.each do |key|
21
+ count_if_present counts, key, stat
22
+ end
23
+ end
24
+
25
+ gauge(:workers, stats[:workers], :type => :count)
26
+ gauge(:workers, stats[:booted_workers], :type => :booted)
27
+ gauge(:workers, stats[:old_workers], :type => :old)
28
+ else # Single worker
29
+ count_keys.each do |key|
30
+ count_if_present counts, key, stats
31
+ end
32
+ end
33
+
34
+ gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
35
+ gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
36
+ gauge(:threads, counts[:running], :type => :running) if counts[:running]
37
+ gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :hostname
43
+
44
+ def gauge(field, count, tags = {})
45
+ Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
46
+ end
47
+
48
+ def count_if_present(counts, key, stats)
49
+ stat_value = stats[key]
50
+ return unless stat_value
51
+ counts[key] ||= 0
52
+ counts[key] += stat_value
53
+ end
54
+
55
+ def fetch_puma_stats
56
+ ::Puma.stats
57
+ rescue NoMethodError # rubocop:disable Lint/HandleExceptions
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,102 @@
1
+ module Appsignal
2
+ module Probes
3
+ # @api private
4
+ class SidekiqProbe
5
+ attr_reader :config
6
+
7
+ def self.dependencies_present?
8
+ Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
9
+ end
10
+
11
+ def initialize(config = {})
12
+ @config = config
13
+ @cache = {}
14
+ config_string = " with config: #{config}" unless config.empty?
15
+ Appsignal.logger.debug("Initializing Sidekiq probe#{config_string}")
16
+ require "sidekiq/api"
17
+ end
18
+
19
+ def call
20
+ track_redis_info
21
+ track_stats
22
+ track_queues
23
+ end
24
+
25
+ private
26
+
27
+ attr_reader :cache
28
+
29
+ def track_redis_info
30
+ return unless ::Sidekiq.respond_to?(:redis_info)
31
+ redis_info = ::Sidekiq.redis_info
32
+
33
+ gauge "connection_count", redis_info.fetch("connected_clients")
34
+ gauge "memory_usage", redis_info.fetch("used_memory")
35
+ gauge "memory_usage_rss", redis_info.fetch("used_memory_rss")
36
+ end
37
+
38
+ def track_stats
39
+ stats = ::Sidekiq::Stats.new
40
+
41
+ gauge "worker_count", stats.workers_size
42
+ gauge "process_count", stats.processes_size
43
+ gauge_delta :jobs_processed, "job_count", stats.processed,
44
+ :status => :processed
45
+ gauge_delta :jobs_failed, "job_count", stats.failed, :status => :failed
46
+ gauge "job_count", stats.retry_size, :status => :retry_queue
47
+ gauge_delta :jobs_dead, "job_count", stats.dead_size, :status => :died
48
+ gauge "job_count", stats.scheduled_size, :status => :scheduled
49
+ gauge "job_count", stats.enqueued, :status => :enqueued
50
+ end
51
+
52
+ def track_queues
53
+ ::Sidekiq::Queue.all.each do |queue|
54
+ gauge "queue_length", queue.size, :queue => queue.name
55
+ # Convert latency from seconds to milliseconds
56
+ gauge "queue_latency", queue.latency * 1_000.0, :queue => queue.name
57
+ end
58
+ end
59
+
60
+ # Track a gauge metric with the `sidekiq_` prefix
61
+ def gauge(key, value, tags = {})
62
+ tags[:hostname] = hostname if hostname
63
+ Appsignal.set_gauge "sidekiq_#{key}", value, tags
64
+ end
65
+
66
+ # Track the delta of two values for a gauge metric
67
+ #
68
+ # First call will store the data for the metric and the second call will
69
+ # set a gauge metric with the difference. This is used for absolute
70
+ # counter values which we want to track as gauges.
71
+ #
72
+ # @example
73
+ # gauge_delta :my_cache_key, "my_gauge", 10
74
+ # gauge_delta :my_cache_key, "my_gauge", 15
75
+ # # Creates a gauge with the value `5`
76
+ # @see #gauge
77
+ def gauge_delta(cache_key, key, value, tags = {})
78
+ previous_value = cache[cache_key]
79
+ cache[cache_key] = value
80
+ return unless previous_value
81
+ new_value = value - previous_value
82
+ gauge key, new_value, tags
83
+ end
84
+
85
+ def hostname
86
+ return @hostname if defined?(@hostname)
87
+ if config.key?(:hostname)
88
+ @hostname = config[:hostname]
89
+ Appsignal.logger.debug "Sidekiq probe: Using hostname config " \
90
+ "option #{@hostname.inspect} as hostname"
91
+ return @hostname
92
+ end
93
+
94
+ host = nil
95
+ ::Sidekiq.redis { |c| host = c.connection[:host] }
96
+ Appsignal.logger.debug "Sidekiq probe: Using Redis server hostname " \
97
+ "#{host.inspect} as hostname"
98
+ @hostname = host
99
+ end
100
+ end
101
+ end
102
+ end
@@ -29,8 +29,11 @@ module Appsignal
29
29
  Appsignal.logger.debug \
30
30
  "Initializing Appsignal::Rack::JSExceptionCatcher"
31
31
  deprecation_message "The Appsignal::Rack::JSExceptionCatcher is " \
32
- "deprecated. Please use the official AppSignal JavaScript " \
33
- "integration instead. https://docs.appsignal.com/front-end/"
32
+ "deprecated and will be removed in a future version. Please use " \
33
+ "the official AppSignal JavaScript integration by disabling " \
34
+ "`enable_frontend_error_catching` in your configuration and " \
35
+ "installing AppSignal for JavaScript instead. " \
36
+ "(https://docs.appsignal.com/front-end/)"
34
37
  @app = app
35
38
  end
36
39
 
@@ -221,6 +221,16 @@ module Appsignal
221
221
  set_action_if_nil(group_and_action.compact.join("#"))
222
222
  end
223
223
 
224
+ # Set queue start time for transaction.
225
+ #
226
+ # Most commononly called by {set_http_or_background_queue_start}.
227
+ #
228
+ # @param start [Integer] Queue start time in milliseconds.
229
+ # @raise [RangeError] When the queue start time value is too big, this
230
+ # method raises a RangeError.
231
+ # @raise [TypeError] Raises a TypeError when the given `start` argument is
232
+ # not an Integer.
233
+ # @return [void]
224
234
  def set_queue_start(start)
225
235
  return unless start
226
236
  @ext.set_queue_start(start)
@@ -1,10 +1,14 @@
1
1
  module Appsignal
2
2
  module Utils
3
3
  module DeprecationMessage
4
- def deprecation_message(message, logger = Appsignal.logger)
4
+ def self.message(message, logger = Appsignal.logger)
5
5
  $stderr.puts "appsignal WARNING: #{message}"
6
6
  logger.warn message
7
7
  end
8
+
9
+ def deprecation_message(message, logger = Appsignal.logger)
10
+ Appsignal::Utils::DeprecationMessage.message(message, logger)
11
+ end
8
12
  end
9
13
  end
10
14
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "2.11.0.alpha.1".freeze
4
+ VERSION = "2.11.0.beta.4".freeze
5
5
  end
@@ -17,7 +17,8 @@ Puma::Plugin.create do
17
17
  launcher.events.on_booted do
18
18
  require "appsignal"
19
19
  if ::Puma.respond_to?(:stats)
20
- Appsignal::Minutely.probes.register :puma, Appsignal::Hooks::PumaProbe
20
+ require "appsignal/probes/puma"
21
+ Appsignal::Minutely.probes.register :puma, Appsignal::Probes::PumaProbe
21
22
  end
22
23
  Appsignal.start
23
24
  Appsignal.start_logger
@@ -0,0 +1,548 @@
1
+ if DependencyHelper.active_job_present?
2
+ require "active_job"
3
+ require "action_mailer"
4
+
5
+ describe Appsignal::Hooks::ActiveJobHook do
6
+ describe "#dependencies_present?" do
7
+ subject { described_class.new.dependencies_present? }
8
+
9
+ context "when ActiveJob constant is found" do
10
+ before { stub_const "ActiveJob", Class.new }
11
+
12
+ it { is_expected.to be_truthy }
13
+ end
14
+
15
+ context "when ActiveJob constant is not found" do
16
+ before { hide_const "ActiveJob" }
17
+
18
+ it { is_expected.to be_falsy }
19
+ end
20
+ end
21
+
22
+ describe "#install" do
23
+ it "extends ActiveJob::Base with the AppSignal ActiveJob plugin" do
24
+ start_agent
25
+
26
+ path, _line_number = ActiveJob::Base.method(:execute).source_location
27
+ expect(path).to end_with("/lib/appsignal/hooks/active_job.rb")
28
+ end
29
+ end
30
+ end
31
+
32
+ describe Appsignal::Hooks::ActiveJobHook::ActiveJobClassInstrumentation do
33
+ let(:time) { Time.parse("2001-01-01 10:00:00UTC") }
34
+ let(:namespace) { Appsignal::Transaction::BACKGROUND_JOB }
35
+ let(:queue) { "default" }
36
+ let(:log) { StringIO.new }
37
+ let(:parameterized_given_args) do
38
+ {
39
+ :foo => "Foo",
40
+ "bar" => "Bar",
41
+ "baz" => { "1" => "foo" }
42
+ }
43
+ end
44
+ let(:method_given_args) do
45
+ [
46
+ "foo",
47
+ parameterized_given_args
48
+ ]
49
+ end
50
+ let(:parameterized_expected_args) do
51
+ {
52
+ "_aj_symbol_keys" => ["foo"],
53
+ "foo" => "Foo",
54
+ "bar" => "Bar",
55
+ "baz" => {
56
+ "_aj_symbol_keys" => [],
57
+ "1" => "foo"
58
+ }
59
+ }
60
+ end
61
+ let(:method_expected_args) do
62
+ [
63
+ "foo",
64
+ parameterized_expected_args
65
+ ]
66
+ end
67
+ before do
68
+ ActiveJob::Base.queue_adapter = :inline
69
+
70
+ start_agent
71
+ Appsignal.logger = test_logger(log)
72
+ class ActiveJobTestJob < ActiveJob::Base
73
+ def perform(*_args)
74
+ end
75
+ end
76
+
77
+ class ActiveJobErrorTestJob < ActiveJob::Base
78
+ def perform
79
+ raise "uh oh"
80
+ end
81
+ end
82
+
83
+ class ActiveJobCustomQueueTestJob < ActiveJob::Base
84
+ queue_as :custom_queue
85
+
86
+ def perform(*_args)
87
+ end
88
+ end
89
+ end
90
+ around { |example| keep_transactions { example.run } }
91
+ after do
92
+ Object.send(:remove_const, :ActiveJobTestJob)
93
+ Object.send(:remove_const, :ActiveJobErrorTestJob)
94
+ Object.send(:remove_const, :ActiveJobCustomQueueTestJob)
95
+ end
96
+
97
+ it "reports the name from the ActiveJob integration" do
98
+ tags = { :queue => queue }
99
+ expect(Appsignal).to receive(:increment_counter)
100
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
101
+
102
+ perform_job(ActiveJobTestJob)
103
+
104
+ transaction = last_transaction
105
+ transaction_hash = transaction.to_h
106
+ expect(transaction_hash).to include(
107
+ "action" => "ActiveJobTestJob#perform",
108
+ "error" => nil,
109
+ "namespace" => namespace,
110
+ "metadata" => {},
111
+ "sample_data" => hash_including(
112
+ "params" => [],
113
+ "tags" => {
114
+ "active_job_id" => kind_of(String),
115
+ "queue" => queue
116
+ }
117
+ )
118
+ )
119
+ events = transaction_hash["events"]
120
+ .sort_by { |e| e["start"] }
121
+ .map { |event| event["name"] }
122
+ expect(events).to eq(["perform_start.active_job", "perform.active_job"])
123
+ end
124
+
125
+ context "with custom queue" do
126
+ it "reports the custom queue as tag on the transaction" do
127
+ tags = { :queue => "custom_queue" }
128
+ expect(Appsignal).to receive(:increment_counter)
129
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
130
+ perform_job(ActiveJobCustomQueueTestJob)
131
+
132
+ transaction = last_transaction
133
+ transaction_hash = transaction.to_h
134
+ expect(transaction_hash).to include(
135
+ "sample_data" => hash_including(
136
+ "tags" => hash_including("queue" => "custom_queue")
137
+ )
138
+ )
139
+ end
140
+ end
141
+
142
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
143
+ context "with priority" do
144
+ before do
145
+ class ActiveJobPriorityTestJob < ActiveJob::Base
146
+ queue_with_priority 10
147
+
148
+ def perform(*_args)
149
+ end
150
+ end
151
+ end
152
+ after do
153
+ Object.send(:remove_const, :ActiveJobPriorityTestJob)
154
+ end
155
+
156
+ it "reports the priority as tag on the transaction" do
157
+ tags = { :priority => 10, :queue => queue }
158
+ expect(Appsignal).to receive(:increment_counter)
159
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
160
+
161
+ perform_job(ActiveJobPriorityTestJob)
162
+
163
+ transaction = last_transaction
164
+ transaction_hash = transaction.to_h
165
+ expect(transaction_hash).to include(
166
+ "sample_data" => hash_including(
167
+ "tags" => hash_including("queue" => queue, "priority" => 10)
168
+ )
169
+ )
170
+ end
171
+ end
172
+ end
173
+
174
+ context "with error" do
175
+ it "reports the error on the transaction from the ActiveRecord integration" do
176
+ allow(Appsignal).to receive(:increment_counter) # Other calls we're testing in another test
177
+ tags = { :queue => queue }
178
+ expect(Appsignal).to receive(:increment_counter)
179
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
180
+ expect(Appsignal).to receive(:increment_counter)
181
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
182
+
183
+ expect do
184
+ perform_job(ActiveJobErrorTestJob)
185
+ end.to raise_error(RuntimeError, "uh oh")
186
+
187
+ transaction = last_transaction
188
+ transaction_hash = transaction.to_h
189
+ expect(transaction_hash).to include(
190
+ "action" => "ActiveJobErrorTestJob#perform",
191
+ "error" => {
192
+ "name" => "RuntimeError",
193
+ "message" => "uh oh",
194
+ "backtrace" => kind_of(String)
195
+ },
196
+ "namespace" => namespace,
197
+ "metadata" => {},
198
+ "sample_data" => hash_including(
199
+ "params" => [],
200
+ "tags" => {
201
+ "active_job_id" => kind_of(String),
202
+ "queue" => queue
203
+ }
204
+ )
205
+ )
206
+ events = transaction_hash["events"]
207
+ .sort_by { |e| e["start"] }
208
+ .map { |event| event["name"] }
209
+ expect(events).to eq(["perform_start.active_job", "perform.active_job"])
210
+ end
211
+ end
212
+
213
+ context "when wrapped in another transaction" do
214
+ it "does not create a new transaction or close the currently open one" do
215
+ current_transaction = background_job_transaction
216
+ allow(current_transaction).to receive(:complete).and_call_original
217
+ set_current_transaction current_transaction
218
+
219
+ perform_job(ActiveJobTestJob)
220
+
221
+ expect(created_transactions.count).to eql(1)
222
+ expect(current_transaction).to_not have_received(:complete)
223
+ current_transaction.complete
224
+
225
+ transaction = current_transaction
226
+ transaction_hash = transaction.to_h
227
+ # It does set data on the transaction
228
+ expect(transaction_hash).to include(
229
+ "id" => current_transaction.transaction_id,
230
+ "action" => "ActiveJobTestJob#perform",
231
+ "error" => nil,
232
+ "namespace" => namespace,
233
+ "metadata" => {},
234
+ "sample_data" => hash_including(
235
+ "params" => [],
236
+ "tags" => {
237
+ "active_job_id" => kind_of(String),
238
+ "queue" => queue
239
+ }
240
+ )
241
+ )
242
+ events = transaction_hash["events"]
243
+ .reject { |e| e["name"] == "enqueue.active_job" }
244
+ .sort_by { |e| e["start"] }
245
+ .map { |event| event["name"] }
246
+ expect(events).to eq(["perform_start.active_job", "perform.active_job"])
247
+ end
248
+ end
249
+
250
+ context "with params" do
251
+ it "filters the configured params" do
252
+ Appsignal.config = project_fixture_config("production")
253
+ Appsignal.config[:filter_parameters] = ["foo"]
254
+ perform_job(ActiveJobTestJob, method_given_args)
255
+
256
+ transaction = last_transaction
257
+ transaction_hash = transaction.to_h
258
+ expect(transaction_hash["sample_data"]["params"]).to include(
259
+ [
260
+ "foo",
261
+ {
262
+ "_aj_symbol_keys" => ["foo"],
263
+ "foo" => "[FILTERED]",
264
+ "bar" => "Bar",
265
+ "baz" => { "_aj_symbol_keys" => [], "1" => "foo" }
266
+ }
267
+ ]
268
+ )
269
+ end
270
+ end
271
+
272
+ context "with provider_job_id", :skip => DependencyHelper.rails_version < Gem::Version.new("5.0.0") do
273
+ before do
274
+ module ActiveJob
275
+ module QueueAdapters
276
+ # Adapter used in our test suite to add provider data to the job
277
+ # data, as is done by Rails provided ActiveJob adapters.
278
+ #
279
+ # This implementation is based on the
280
+ # `ActiveJob::QueueAdapters::InlineAdapter`.
281
+ class AppsignalTestAdapter < InlineAdapter
282
+ def enqueue(job)
283
+ Base.execute(job.serialize.merge("provider_job_id" => "my_provider_job_id"))
284
+ end
285
+ end
286
+ end
287
+ end
288
+
289
+ class ProviderWrappedActiveJobTestJob < ActiveJob::Base
290
+ self.queue_adapter = :appsignal_test
291
+
292
+ def perform(*_args)
293
+ end
294
+ end
295
+ end
296
+ after do
297
+ ActiveJob::QueueAdapters.send(:remove_const, :AppsignalTestAdapter)
298
+ Object.send(:remove_const, :ProviderWrappedActiveJobTestJob)
299
+ end
300
+
301
+ it "sets provider_job_id as tag" do
302
+ perform_job(ProviderWrappedActiveJobTestJob)
303
+
304
+ transaction = last_transaction
305
+ transaction_hash = transaction.to_h
306
+ expect(transaction_hash["sample_data"]["tags"]).to include(
307
+ "provider_job_id" => "my_provider_job_id"
308
+ )
309
+ end
310
+ end
311
+
312
+ context "with enqueued_at", :skip => DependencyHelper.rails_version < Gem::Version.new("6.0.0") do
313
+ before do
314
+ module ActiveJob
315
+ module QueueAdapters
316
+ # Adapter used in our test suite to add provider data to the job
317
+ # data, as is done by Rails provided ActiveJob adapters.
318
+ #
319
+ # This implementation is based on the
320
+ # `ActiveJob::QueueAdapters::InlineAdapter`.
321
+ class AppsignalTestAdapter < InlineAdapter
322
+ def enqueue(job)
323
+ Base.execute(job.serialize.merge("enqueued_at" => "2020-10-10T10:10:10Z"))
324
+ end
325
+ end
326
+ end
327
+ end
328
+
329
+ class ProviderWrappedActiveJobTestJob < ActiveJob::Base
330
+ self.queue_adapter = :appsignal_test
331
+
332
+ def perform(*_args)
333
+ end
334
+ end
335
+ end
336
+ after do
337
+ ActiveJob::QueueAdapters.send(:remove_const, :AppsignalTestAdapter)
338
+ Object.send(:remove_const, :ProviderWrappedActiveJobTestJob)
339
+ end
340
+
341
+ it "sets queue time on transaction" do
342
+ allow_any_instance_of(Appsignal::Transaction).to receive(:set_queue_start).and_call_original
343
+ perform_job(ProviderWrappedActiveJobTestJob)
344
+
345
+ transaction = last_transaction
346
+ queue_time = Time.parse("2020-10-10T10:10:10Z")
347
+ expect(transaction).to have_received(:set_queue_start)
348
+ .with((queue_time.to_f * 1_000).to_i)
349
+ end
350
+ end
351
+
352
+ context "with ActionMailer job" do
353
+ include ActionMailerHelpers
354
+
355
+ before do
356
+ class ActionMailerTestJob < ActionMailer::Base
357
+ def welcome(_first_arg = nil, _second_arg = nil)
358
+ end
359
+ end
360
+ end
361
+ after do
362
+ Object.send(:remove_const, :ActionMailerTestJob)
363
+ end
364
+
365
+ context "without params" do
366
+ it "sets the Action mailer data on the transaction" do
367
+ perform_mailer(ActionMailerTestJob, :welcome)
368
+
369
+ transaction = last_transaction
370
+ transaction_hash = transaction.to_h
371
+ expect(transaction_hash).to include(
372
+ "action" => "ActionMailerTestJob#welcome",
373
+ "sample_data" => hash_including(
374
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now"],
375
+ "tags" => {
376
+ "active_job_id" => kind_of(String),
377
+ "queue" => "mailers"
378
+ }
379
+ )
380
+ )
381
+ end
382
+ end
383
+
384
+ context "with multiple arguments" do
385
+ it "sets the arguments on the transaction" do
386
+ perform_mailer(ActionMailerTestJob, :welcome, method_given_args)
387
+
388
+ transaction = last_transaction
389
+ transaction_hash = transaction.to_h
390
+ expect(transaction_hash).to include(
391
+ "action" => "ActionMailerTestJob#welcome",
392
+ "sample_data" => hash_including(
393
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now"] + method_expected_args,
394
+ "tags" => {
395
+ "active_job_id" => kind_of(String),
396
+ "queue" => "mailers"
397
+ }
398
+ )
399
+ )
400
+ end
401
+ end
402
+
403
+ if DependencyHelper.rails_version >= Gem::Version.new("5.2.0")
404
+ context "with parameterized arguments" do
405
+ it "sets the arguments on the transaction" do
406
+ perform_mailer(ActionMailerTestJob, :welcome, parameterized_given_args)
407
+
408
+ transaction = last_transaction
409
+ transaction_hash = transaction.to_h
410
+ expect(transaction_hash).to include(
411
+ "action" => "ActionMailerTestJob#welcome",
412
+ "sample_data" => hash_including(
413
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now", parameterized_expected_args],
414
+ "tags" => {
415
+ "active_job_id" => kind_of(String),
416
+ "queue" => "mailers"
417
+ }
418
+ )
419
+ )
420
+ end
421
+ end
422
+ end
423
+ end
424
+
425
+ if DependencyHelper.rails_version >= Gem::Version.new("6.0.0")
426
+ context "with ActionMailer MailDeliveryJob job" do
427
+ include ActionMailerHelpers
428
+
429
+ before do
430
+ class ActionMailerTestMailDeliveryJob < ActionMailer::Base
431
+ self.delivery_job = ActionMailer::MailDeliveryJob
432
+
433
+ def welcome(*_args)
434
+ end
435
+ end
436
+ end
437
+ after do
438
+ Object.send(:remove_const, :ActionMailerTestMailDeliveryJob)
439
+ end
440
+
441
+ it "sets the Action mailer data on the transaction" do
442
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome)
443
+
444
+ transaction = last_transaction
445
+ transaction_hash = transaction.to_h
446
+ expect(transaction_hash).to include(
447
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
448
+ "sample_data" => hash_including(
449
+ "params" => [
450
+ "ActionMailerTestMailDeliveryJob",
451
+ "welcome",
452
+ "deliver_now",
453
+ { active_job_internal_key => ["args"], "args" => [] }
454
+ ],
455
+ "tags" => {
456
+ "active_job_id" => kind_of(String),
457
+ "queue" => "mailers"
458
+ }
459
+ )
460
+ )
461
+ end
462
+
463
+ context "with method arguments" do
464
+ it "sets the Action mailer data on the transaction" do
465
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, method_given_args)
466
+
467
+ transaction = last_transaction
468
+ transaction_hash = transaction.to_h
469
+ expect(transaction_hash).to include(
470
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
471
+ "sample_data" => hash_including(
472
+ "params" => [
473
+ "ActionMailerTestMailDeliveryJob",
474
+ "welcome",
475
+ "deliver_now",
476
+ {
477
+ active_job_internal_key => ["args"],
478
+ "args" => method_expected_args
479
+ }
480
+ ],
481
+ "tags" => {
482
+ "active_job_id" => kind_of(String),
483
+ "queue" => "mailers"
484
+ }
485
+ )
486
+ )
487
+ end
488
+ end
489
+
490
+ context "with parameterized arguments" do
491
+ it "sets the Action mailer data on the transaction" do
492
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, parameterized_given_args)
493
+
494
+ transaction = last_transaction
495
+ transaction_hash = transaction.to_h
496
+ expect(transaction_hash).to include(
497
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
498
+ "sample_data" => hash_including(
499
+ "params" => [
500
+ "ActionMailerTestMailDeliveryJob",
501
+ "welcome",
502
+ "deliver_now",
503
+ {
504
+ active_job_internal_key => ["params", "args"],
505
+ "args" => [],
506
+ "params" => parameterized_expected_args
507
+ }
508
+ ],
509
+ "tags" => {
510
+ "active_job_id" => kind_of(String),
511
+ "queue" => "mailers"
512
+ }
513
+ )
514
+ )
515
+ end
516
+ end
517
+ end
518
+ end
519
+
520
+ def perform_active_job
521
+ Timecop.freeze(time) do
522
+ yield
523
+ end
524
+ end
525
+
526
+ def perform_job(job_class, args = nil)
527
+ perform_active_job do
528
+ if args
529
+ job_class.perform_later(args)
530
+ else
531
+ job_class.perform_later
532
+ end
533
+ end
534
+ end
535
+
536
+ def perform_mailer(mailer, method, args = nil)
537
+ perform_active_job { perform_action_mailer(mailer, method, args) }
538
+ end
539
+
540
+ def active_job_internal_key
541
+ if DependencyHelper.ruby_version >= Gem::Version.new("2.7.0")
542
+ "_aj_ruby2_keywords"
543
+ else
544
+ "_aj_symbol_keys"
545
+ end
546
+ end
547
+ end
548
+ end