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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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