appsignal 2.11.0.alpha.2 → 2.11.0.beta.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +94 -10
  4. data/CHANGELOG.md +31 -1
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +7 -3
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/base.rb +7 -0
  12. data/ext/extconf.rb +2 -0
  13. data/gemfiles/rails-4.2.gemfile +9 -2
  14. data/gemfiles/rails-5.0.gemfile +1 -0
  15. data/gemfiles/rails-5.1.gemfile +1 -0
  16. data/gemfiles/rails-5.2.gemfile +1 -0
  17. data/gemfiles/rails-6.0.gemfile +1 -0
  18. data/gemfiles/resque-1.gemfile +7 -0
  19. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  20. data/lib/appsignal.rb +1 -0
  21. data/lib/appsignal/auth_check.rb +4 -2
  22. data/lib/appsignal/cli/diagnose.rb +1 -1
  23. data/lib/appsignal/config.rb +35 -2
  24. data/lib/appsignal/extension.rb +6 -5
  25. data/lib/appsignal/extension/jruby.rb +6 -5
  26. data/lib/appsignal/hooks.rb +25 -0
  27. data/lib/appsignal/hooks/active_job.rb +137 -0
  28. data/lib/appsignal/hooks/puma.rb +0 -1
  29. data/lib/appsignal/hooks/resque.rb +60 -0
  30. data/lib/appsignal/hooks/sidekiq.rb +17 -94
  31. data/lib/appsignal/integrations/delayed_job_plugin.rb +1 -1
  32. data/lib/appsignal/integrations/que.rb +1 -1
  33. data/lib/appsignal/integrations/resque.rb +9 -12
  34. data/lib/appsignal/integrations/resque_active_job.rb +9 -32
  35. data/lib/appsignal/probes.rb +7 -0
  36. data/lib/appsignal/probes/puma.rb +1 -1
  37. data/lib/appsignal/probes/sidekiq.rb +3 -1
  38. data/lib/appsignal/transaction.rb +10 -0
  39. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  40. data/lib/appsignal/version.rb +1 -1
  41. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  42. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  43. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  44. data/spec/lib/appsignal/cli/diagnose_spec.rb +42 -0
  45. data/spec/lib/appsignal/config_spec.rb +21 -0
  46. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  47. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  48. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  49. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +3 -14
  50. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  51. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +222 -268
  52. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  53. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  54. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -179
  55. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  56. data/spec/lib/appsignal/marker_spec.rb +1 -1
  57. data/spec/lib/appsignal/probes/sidekiq_spec.rb +10 -7
  58. data/spec/lib/appsignal/transaction_spec.rb +5 -7
  59. data/spec/spec_helper.rb +5 -0
  60. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  61. data/spec/support/helpers/config_helpers.rb +3 -2
  62. data/spec/support/helpers/dependency_helper.rb +9 -2
  63. data/spec/support/helpers/transaction_helpers.rb +6 -0
  64. data/spec/support/stubs/sidekiq/api.rb +1 -1
  65. data/spec/support/testing.rb +19 -19
  66. metadata +16 -4
@@ -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,7 @@
1
+ module Appsignal
2
+ module Probes
3
+ end
4
+ end
5
+
6
+ require "appsignal/probes/puma"
7
+ require "appsignal/probes/sidekiq"
@@ -1,11 +1,11 @@
1
1
  module Appsignal
2
2
  module Probes
3
- # @api private
4
3
  class PumaProbe
5
4
  def initialize
6
5
  @hostname = Appsignal.config[:hostname] || Socket.gethostname
7
6
  end
8
7
 
8
+ # @api private
9
9
  def call
10
10
  puma_stats = fetch_puma_stats
11
11
  return unless puma_stats
@@ -1,9 +1,10 @@
1
1
  module Appsignal
2
2
  module Probes
3
- # @api private
4
3
  class SidekiqProbe
4
+ # @api private
5
5
  attr_reader :config
6
6
 
7
+ # @api private
7
8
  def self.dependencies_present?
8
9
  Gem::Version.new(::Redis::VERSION) >= Gem::Version.new("3.3.5")
9
10
  end
@@ -16,6 +17,7 @@ module Appsignal
16
17
  require "sidekiq/api"
17
18
  end
18
19
 
20
+ # @api private
19
21
  def call
20
22
  track_redis_info
21
23
  track_stats
@@ -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)
5
- $stderr.puts "appsignal WARNING: #{message}"
4
+ def self.message(message, logger = Appsignal.logger)
5
+ Kernel.warn "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.2".freeze
4
+ VERSION = "2.11.0.beta.5".freeze
5
5
  end
@@ -28,6 +28,29 @@ describe Appsignal::AuthCheck do
28
28
  end.join("&")
29
29
  end
30
30
 
31
+ describe ".new" do
32
+ describe "with logger argument" do
33
+ let(:err_stream) { std_stream }
34
+ let(:stderr) { err_stream.read }
35
+ let(:log_stream) { std_stream }
36
+ let(:log) { log_contents(log_stream) }
37
+
38
+ it "logs and prints a deprecation message" do
39
+ Appsignal.logger = test_logger(log_stream)
40
+
41
+ capture_std_streams(std_stream, err_stream) do
42
+ Appsignal::AuthCheck.new(config, Appsignal.logger)
43
+ end
44
+
45
+ deprecation_message =
46
+ "`Appsignal::AuthCheck.new`'s `logger` argument " \
47
+ "will be removed in the next major version."
48
+ expect(stderr).to include "appsignal WARNING: #{deprecation_message}"
49
+ expect(log).to contains_log :warn, deprecation_message
50
+ end
51
+ end
52
+ end
53
+
31
54
  describe "#perform" do
32
55
  subject { auth_check.perform }
33
56
 
@@ -10,7 +10,7 @@ if DependencyHelper.capistrano2_present?
10
10
  let(:capistrano_config) do
11
11
  Capistrano::Configuration.new.tap do |c|
12
12
  c.set(:rails_env, "production")
13
- c.set(:repository, "master")
13
+ c.set(:repository, "main")
14
14
  c.set(:deploy_to, "/home/username/app")
15
15
  c.set(:current_release, "")
16
16
  c.set(:current_revision, "503ce0923ed177a3ce000005")
@@ -15,7 +15,7 @@ if DependencyHelper.capistrano3_present?
15
15
  c.set(:log_level, :error)
16
16
  c.set(:logger, logger)
17
17
  c.set(:rails_env, "production")
18
- c.set(:repository, "master")
18
+ c.set(:repository, "main")
19
19
  c.set(:deploy_to, "/home/username/app")
20
20
  c.set(:current_release, "")
21
21
  c.set(:current_revision, "503ce0923ed177a3ce000005")
@@ -290,6 +290,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
290
290
  jruby = Appsignal::System.jruby?
291
291
  expect(output).to include(
292
292
  "Extension installation report",
293
+ "Installation result",
294
+ " Status: success",
293
295
  "Language details",
294
296
  " Implementation: #{jruby ? "jruby" : "ruby"}",
295
297
  " Ruby version: #{"#{rbconfig["ruby_version"]}-p#{rbconfig["PATCHLEVEL"]}"}",
@@ -310,6 +312,46 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
310
312
  )
311
313
  end
312
314
 
315
+ context "with error in install report" do
316
+ let(:error) { RuntimeError.new("some error") }
317
+ before do
318
+ allow(File).to receive(:read).and_call_original
319
+ expect(File).to receive(:read)
320
+ .with(File.expand_path("../../../../../ext/install.report", __FILE__))
321
+ .and_return(
322
+ YAML.dump(
323
+ "result" => {
324
+ "status" => "error",
325
+ "error" => "RuntimeError: some error",
326
+ "backtrace" => error.backtrace
327
+ }
328
+ )
329
+ )
330
+ end
331
+
332
+ it "sends an error" do
333
+ run
334
+ expect(received_report["installation"]).to match(
335
+ "result" => {
336
+ "status" => "error",
337
+ "error" => "RuntimeError: some error",
338
+ "backtrace" => error.backtrace
339
+ }
340
+ )
341
+ end
342
+
343
+ it "prints the error" do
344
+ run
345
+
346
+ expect(output).to include(
347
+ "Extension installation report",
348
+ "Installation result",
349
+ "Status: error\n Error: RuntimeError: some error"
350
+ )
351
+ expect(output).to_not include("Raw report:")
352
+ end
353
+ end
354
+
313
355
  context "without install report" do
314
356
  let(:error) { RuntimeError.new("foo") }
315
357
  before do
@@ -244,6 +244,27 @@ describe Appsignal::Config do
244
244
  end
245
245
  end
246
246
 
247
+ context "with an overriden config file" do
248
+ let(:config) do
249
+ project_fixture_config("production", {}, Appsignal.logger, File.join(project_fixture_path, "config", "appsignal.yml"))
250
+ end
251
+
252
+ it "is valid and active" do
253
+ expect(config.valid?).to be_truthy
254
+ expect(config.active?).to be_truthy
255
+ end
256
+
257
+ context "with an invalid overriden config file" do
258
+ let(:config) do
259
+ project_fixture_config("production", {}, Appsignal.logger, File.join(project_fixture_path, "config", "missing.yml"))
260
+ end
261
+
262
+ it "is not valid" do
263
+ expect(config.valid?).to be_falsy
264
+ end
265
+ end
266
+ end
267
+
247
268
  context "with the config file causing an error" do
248
269
  let(:config_path) do
249
270
  File.expand_path(
@@ -1,42 +1,45 @@
1
- if Appsignal::System.jruby?
2
- describe Appsignal::Extension::Jruby do
3
- let(:extension) { Appsignal::Extension }
1
+ describe "JRuby extension", :jruby do
2
+ let(:extension) { Appsignal::Extension }
3
+ let(:jruby_module) { Appsignal::Extension::Jruby }
4
4
 
5
- describe "string conversions" do
6
- it "keeps the same value during string type conversions" do
7
- # UTF-8 string with NULL
8
- # Tests if the conversions between the conversions without breaking on
9
- # NULL terminated strings in C.
10
- string = "Merry Christmas! \u0000 🎄"
5
+ it "creates a JRuby extension module" do
6
+ expect(Appsignal::Extension::Jruby).to be_kind_of(Module)
7
+ end
11
8
 
12
- appsignal_string = extension.make_appsignal_string(string)
13
- ruby_string = extension.make_ruby_string(appsignal_string)
9
+ describe "string conversions" do
10
+ it "keeps the same value during string type conversions" do
11
+ # UTF-8 string with NULL
12
+ # Tests if the conversions between the conversions without breaking on
13
+ # NULL terminated strings in C.
14
+ string = "Merry Christmas! \u0000 🎄"
14
15
 
15
- expect(ruby_string).to eq("Merry Christmas! \u0000 🎄")
16
- end
17
- end
16
+ appsignal_string = extension.make_appsignal_string(string)
17
+ ruby_string = extension.make_ruby_string(appsignal_string)
18
18
 
19
- it "loads libappsignal with FFI" do
20
- expect(described_class.ffi_libraries.map(&:name).first).to include "libappsignal"
19
+ expect(ruby_string).to eq("Merry Christmas! \u0000 🎄")
21
20
  end
21
+ end
22
22
 
23
- describe ".lib_extension" do
24
- subject { described_class.lib_extension }
23
+ it "loads libappsignal with FFI" do
24
+ expect(jruby_module.ffi_libraries.map(&:name).first).to include "libappsignal"
25
+ end
26
+
27
+ describe ".lib_extension" do
28
+ subject { jruby_module.lib_extension }
25
29
 
26
- context "when on a darwin system" do
27
- before { expect(Appsignal::System).to receive(:agent_platform).and_return("darwin") }
30
+ context "when on a darwin system" do
31
+ before { expect(Appsignal::System).to receive(:agent_platform).and_return("darwin") }
28
32
 
29
- it "returns the extension for darwin" do
30
- is_expected.to eq "dylib"
31
- end
33
+ it "returns the extension for darwin" do
34
+ is_expected.to eq "dylib"
32
35
  end
36
+ end
33
37
 
34
- context "when on a linux system" do
35
- before { expect(Appsignal::System).to receive(:agent_platform).and_return("linux") }
38
+ context "when on a linux system" do
39
+ before { expect(Appsignal::System).to receive(:agent_platform).and_return("linux") }
36
40
 
37
- it "returns the lib extension for linux" do
38
- is_expected.to eq "so"
39
- end
41
+ it "returns the lib extension for linux" do
42
+ is_expected.to eq "so"
40
43
  end
41
44
  end
42
45
  end
@@ -0,0 +1,23 @@
1
+ describe Appsignal::Extension, :extension_installation_failure do
2
+ context "when the extension library cannot be loaded" do
3
+ # This test breaks the installation on purpose and is not run by default.
4
+ # See `rake test:failure`. If this test was run, run `rake
5
+ # extension:install` again to fix the extension installation.
6
+ it "prints and logs an error" do
7
+ # ENV var to make sure installation fails on purpurse
8
+ ENV["_TEST_APPSIGNAL_EXTENSION_FAILURE"] = "true"
9
+ `rake extension:install` # Run installation
10
+
11
+ require "open3"
12
+ _stdout, stderr, _status = Open3.capture3("bin/appsignal --version")
13
+ expect(stderr).to include("ERROR: AppSignal failed to load extension")
14
+ error_message =
15
+ if DependencyHelper.running_jruby?
16
+ "cannot open shared object file"
17
+ else
18
+ "LoadError: cannot load such file"
19
+ end
20
+ expect(stderr).to include(error_message)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,591 @@
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 = { :queue => queue }
158
+ expect(Appsignal).to receive(:increment_counter)
159
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
160
+ expect(Appsignal).to receive(:increment_counter)
161
+ .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10, :status => :processed))
162
+
163
+ perform_job(ActiveJobPriorityTestJob)
164
+
165
+ transaction = last_transaction
166
+ transaction_hash = transaction.to_h
167
+ expect(transaction_hash).to include(
168
+ "sample_data" => hash_including(
169
+ "tags" => hash_including("queue" => queue, "priority" => 10)
170
+ )
171
+ )
172
+ end
173
+ end
174
+ end
175
+
176
+ context "with error" do
177
+ it "reports the error on the transaction from the ActiveRecord integration" do
178
+ allow(Appsignal).to receive(:increment_counter) # Other calls we're testing in another test
179
+ tags = { :queue => queue }
180
+ expect(Appsignal).to receive(:increment_counter)
181
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
182
+ expect(Appsignal).to receive(:increment_counter)
183
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
184
+
185
+ expect do
186
+ perform_job(ActiveJobErrorTestJob)
187
+ end.to raise_error(RuntimeError, "uh oh")
188
+
189
+ transaction = last_transaction
190
+ transaction_hash = transaction.to_h
191
+ expect(transaction_hash).to include(
192
+ "action" => "ActiveJobErrorTestJob#perform",
193
+ "error" => {
194
+ "name" => "RuntimeError",
195
+ "message" => "uh oh",
196
+ "backtrace" => kind_of(String)
197
+ },
198
+ "namespace" => namespace,
199
+ "metadata" => {},
200
+ "sample_data" => hash_including(
201
+ "params" => [],
202
+ "tags" => {
203
+ "active_job_id" => kind_of(String),
204
+ "queue" => queue
205
+ }
206
+ )
207
+ )
208
+ events = transaction_hash["events"]
209
+ .sort_by { |e| e["start"] }
210
+ .map { |event| event["name"] }
211
+ expect(events).to eq(["perform_start.active_job", "perform.active_job"])
212
+ end
213
+
214
+ if DependencyHelper.rails_version >= Gem::Version.new("5.0.0")
215
+ context "with priority" do
216
+ before do
217
+ class ActiveJobErrorPriorityTestJob < ActiveJob::Base
218
+ queue_with_priority 10
219
+
220
+ def perform(*_args)
221
+ raise "uh oh"
222
+ end
223
+ end
224
+ end
225
+ after do
226
+ Object.send(:remove_const, :ActiveJobErrorPriorityTestJob)
227
+ end
228
+
229
+ it "reports the priority as tag on the transaction" do
230
+ tags = { :queue => queue }
231
+ expect(Appsignal).to receive(:increment_counter)
232
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :processed))
233
+ expect(Appsignal).to receive(:increment_counter)
234
+ .with("active_job_queue_job_count", 1, tags.merge(:status => :failed))
235
+ expect(Appsignal).to receive(:increment_counter)
236
+ .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10, :status => :processed))
237
+ expect(Appsignal).to receive(:increment_counter)
238
+ .with("active_job_queue_priority_job_count", 1, tags.merge(:priority => 10, :status => :failed))
239
+
240
+ expect do
241
+ perform_job(ActiveJobErrorPriorityTestJob)
242
+ end.to raise_error(RuntimeError, "uh oh")
243
+
244
+ transaction = last_transaction
245
+ transaction_hash = transaction.to_h
246
+ expect(transaction_hash).to include(
247
+ "sample_data" => hash_including(
248
+ "tags" => hash_including("queue" => queue, "priority" => 10)
249
+ )
250
+ )
251
+ end
252
+ end
253
+ end
254
+ end
255
+
256
+ context "when wrapped in another transaction" do
257
+ it "does not create a new transaction or close the currently open one" do
258
+ current_transaction = background_job_transaction
259
+ allow(current_transaction).to receive(:complete).and_call_original
260
+ set_current_transaction current_transaction
261
+
262
+ perform_job(ActiveJobTestJob)
263
+
264
+ expect(created_transactions.count).to eql(1)
265
+ expect(current_transaction).to_not have_received(:complete)
266
+ current_transaction.complete
267
+
268
+ transaction = current_transaction
269
+ transaction_hash = transaction.to_h
270
+ # It does set data on the transaction
271
+ expect(transaction_hash).to include(
272
+ "id" => current_transaction.transaction_id,
273
+ "action" => "ActiveJobTestJob#perform",
274
+ "error" => nil,
275
+ "namespace" => namespace,
276
+ "metadata" => {},
277
+ "sample_data" => hash_including(
278
+ "params" => [],
279
+ "tags" => {
280
+ "active_job_id" => kind_of(String),
281
+ "queue" => queue
282
+ }
283
+ )
284
+ )
285
+ events = transaction_hash["events"]
286
+ .reject { |e| e["name"] == "enqueue.active_job" }
287
+ .sort_by { |e| e["start"] }
288
+ .map { |event| event["name"] }
289
+ expect(events).to eq(["perform_start.active_job", "perform.active_job"])
290
+ end
291
+ end
292
+
293
+ context "with params" do
294
+ it "filters the configured params" do
295
+ Appsignal.config = project_fixture_config("production")
296
+ Appsignal.config[:filter_parameters] = ["foo"]
297
+ perform_job(ActiveJobTestJob, method_given_args)
298
+
299
+ transaction = last_transaction
300
+ transaction_hash = transaction.to_h
301
+ expect(transaction_hash["sample_data"]["params"]).to include(
302
+ [
303
+ "foo",
304
+ {
305
+ "_aj_symbol_keys" => ["foo"],
306
+ "foo" => "[FILTERED]",
307
+ "bar" => "Bar",
308
+ "baz" => { "_aj_symbol_keys" => [], "1" => "foo" }
309
+ }
310
+ ]
311
+ )
312
+ end
313
+ end
314
+
315
+ context "with provider_job_id", :skip => DependencyHelper.rails_version < Gem::Version.new("5.0.0") do
316
+ before do
317
+ module ActiveJob
318
+ module QueueAdapters
319
+ # Adapter used in our test suite to add provider data to the job
320
+ # data, as is done by Rails provided ActiveJob adapters.
321
+ #
322
+ # This implementation is based on the
323
+ # `ActiveJob::QueueAdapters::InlineAdapter`.
324
+ class AppsignalTestAdapter < InlineAdapter
325
+ def enqueue(job)
326
+ Base.execute(job.serialize.merge("provider_job_id" => "my_provider_job_id"))
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ class ProviderWrappedActiveJobTestJob < ActiveJob::Base
333
+ self.queue_adapter = :appsignal_test
334
+
335
+ def perform(*_args)
336
+ end
337
+ end
338
+ end
339
+ after do
340
+ ActiveJob::QueueAdapters.send(:remove_const, :AppsignalTestAdapter)
341
+ Object.send(:remove_const, :ProviderWrappedActiveJobTestJob)
342
+ end
343
+
344
+ it "sets provider_job_id as tag" do
345
+ perform_job(ProviderWrappedActiveJobTestJob)
346
+
347
+ transaction = last_transaction
348
+ transaction_hash = transaction.to_h
349
+ expect(transaction_hash["sample_data"]["tags"]).to include(
350
+ "provider_job_id" => "my_provider_job_id"
351
+ )
352
+ end
353
+ end
354
+
355
+ context "with enqueued_at", :skip => DependencyHelper.rails_version < Gem::Version.new("6.0.0") do
356
+ before do
357
+ module ActiveJob
358
+ module QueueAdapters
359
+ # Adapter used in our test suite to add provider data to the job
360
+ # data, as is done by Rails provided ActiveJob adapters.
361
+ #
362
+ # This implementation is based on the
363
+ # `ActiveJob::QueueAdapters::InlineAdapter`.
364
+ class AppsignalTestAdapter < InlineAdapter
365
+ def enqueue(job)
366
+ Base.execute(job.serialize.merge("enqueued_at" => "2020-10-10T10:10:10Z"))
367
+ end
368
+ end
369
+ end
370
+ end
371
+
372
+ class ProviderWrappedActiveJobTestJob < ActiveJob::Base
373
+ self.queue_adapter = :appsignal_test
374
+
375
+ def perform(*_args)
376
+ end
377
+ end
378
+ end
379
+ after do
380
+ ActiveJob::QueueAdapters.send(:remove_const, :AppsignalTestAdapter)
381
+ Object.send(:remove_const, :ProviderWrappedActiveJobTestJob)
382
+ end
383
+
384
+ it "sets queue time on transaction" do
385
+ allow_any_instance_of(Appsignal::Transaction).to receive(:set_queue_start).and_call_original
386
+ perform_job(ProviderWrappedActiveJobTestJob)
387
+
388
+ transaction = last_transaction
389
+ queue_time = Time.parse("2020-10-10T10:10:10Z")
390
+ expect(transaction).to have_received(:set_queue_start)
391
+ .with((queue_time.to_f * 1_000).to_i)
392
+ end
393
+ end
394
+
395
+ context "with ActionMailer job" do
396
+ include ActionMailerHelpers
397
+
398
+ before do
399
+ class ActionMailerTestJob < ActionMailer::Base
400
+ def welcome(_first_arg = nil, _second_arg = nil)
401
+ end
402
+ end
403
+ end
404
+ after do
405
+ Object.send(:remove_const, :ActionMailerTestJob)
406
+ end
407
+
408
+ context "without params" do
409
+ it "sets the Action mailer data on the transaction" do
410
+ perform_mailer(ActionMailerTestJob, :welcome)
411
+
412
+ transaction = last_transaction
413
+ transaction_hash = transaction.to_h
414
+ expect(transaction_hash).to include(
415
+ "action" => "ActionMailerTestJob#welcome",
416
+ "sample_data" => hash_including(
417
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now"],
418
+ "tags" => {
419
+ "active_job_id" => kind_of(String),
420
+ "queue" => "mailers"
421
+ }
422
+ )
423
+ )
424
+ end
425
+ end
426
+
427
+ context "with multiple arguments" do
428
+ it "sets the arguments on the transaction" do
429
+ perform_mailer(ActionMailerTestJob, :welcome, method_given_args)
430
+
431
+ transaction = last_transaction
432
+ transaction_hash = transaction.to_h
433
+ expect(transaction_hash).to include(
434
+ "action" => "ActionMailerTestJob#welcome",
435
+ "sample_data" => hash_including(
436
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now"] + method_expected_args,
437
+ "tags" => {
438
+ "active_job_id" => kind_of(String),
439
+ "queue" => "mailers"
440
+ }
441
+ )
442
+ )
443
+ end
444
+ end
445
+
446
+ if DependencyHelper.rails_version >= Gem::Version.new("5.2.0")
447
+ context "with parameterized arguments" do
448
+ it "sets the arguments on the transaction" do
449
+ perform_mailer(ActionMailerTestJob, :welcome, parameterized_given_args)
450
+
451
+ transaction = last_transaction
452
+ transaction_hash = transaction.to_h
453
+ expect(transaction_hash).to include(
454
+ "action" => "ActionMailerTestJob#welcome",
455
+ "sample_data" => hash_including(
456
+ "params" => ["ActionMailerTestJob", "welcome", "deliver_now", parameterized_expected_args],
457
+ "tags" => {
458
+ "active_job_id" => kind_of(String),
459
+ "queue" => "mailers"
460
+ }
461
+ )
462
+ )
463
+ end
464
+ end
465
+ end
466
+ end
467
+
468
+ if DependencyHelper.rails_version >= Gem::Version.new("6.0.0")
469
+ context "with ActionMailer MailDeliveryJob job" do
470
+ include ActionMailerHelpers
471
+
472
+ before do
473
+ class ActionMailerTestMailDeliveryJob < ActionMailer::Base
474
+ self.delivery_job = ActionMailer::MailDeliveryJob
475
+
476
+ def welcome(*_args)
477
+ end
478
+ end
479
+ end
480
+ after do
481
+ Object.send(:remove_const, :ActionMailerTestMailDeliveryJob)
482
+ end
483
+
484
+ it "sets the Action mailer data on the transaction" do
485
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome)
486
+
487
+ transaction = last_transaction
488
+ transaction_hash = transaction.to_h
489
+ expect(transaction_hash).to include(
490
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
491
+ "sample_data" => hash_including(
492
+ "params" => [
493
+ "ActionMailerTestMailDeliveryJob",
494
+ "welcome",
495
+ "deliver_now",
496
+ { active_job_internal_key => ["args"], "args" => [] }
497
+ ],
498
+ "tags" => {
499
+ "active_job_id" => kind_of(String),
500
+ "queue" => "mailers"
501
+ }
502
+ )
503
+ )
504
+ end
505
+
506
+ context "with method arguments" do
507
+ it "sets the Action mailer data on the transaction" do
508
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, method_given_args)
509
+
510
+ transaction = last_transaction
511
+ transaction_hash = transaction.to_h
512
+ expect(transaction_hash).to include(
513
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
514
+ "sample_data" => hash_including(
515
+ "params" => [
516
+ "ActionMailerTestMailDeliveryJob",
517
+ "welcome",
518
+ "deliver_now",
519
+ {
520
+ active_job_internal_key => ["args"],
521
+ "args" => method_expected_args
522
+ }
523
+ ],
524
+ "tags" => {
525
+ "active_job_id" => kind_of(String),
526
+ "queue" => "mailers"
527
+ }
528
+ )
529
+ )
530
+ end
531
+ end
532
+
533
+ context "with parameterized arguments" do
534
+ it "sets the Action mailer data on the transaction" do
535
+ perform_mailer(ActionMailerTestMailDeliveryJob, :welcome, parameterized_given_args)
536
+
537
+ transaction = last_transaction
538
+ transaction_hash = transaction.to_h
539
+ expect(transaction_hash).to include(
540
+ "action" => "ActionMailerTestMailDeliveryJob#welcome",
541
+ "sample_data" => hash_including(
542
+ "params" => [
543
+ "ActionMailerTestMailDeliveryJob",
544
+ "welcome",
545
+ "deliver_now",
546
+ {
547
+ active_job_internal_key => ["params", "args"],
548
+ "args" => [],
549
+ "params" => parameterized_expected_args
550
+ }
551
+ ],
552
+ "tags" => {
553
+ "active_job_id" => kind_of(String),
554
+ "queue" => "mailers"
555
+ }
556
+ )
557
+ )
558
+ end
559
+ end
560
+ end
561
+ end
562
+
563
+ def perform_active_job
564
+ Timecop.freeze(time) do
565
+ yield
566
+ end
567
+ end
568
+
569
+ def perform_job(job_class, args = nil)
570
+ perform_active_job do
571
+ if args
572
+ job_class.perform_later(args)
573
+ else
574
+ job_class.perform_later
575
+ end
576
+ end
577
+ end
578
+
579
+ def perform_mailer(mailer, method, args = nil)
580
+ perform_active_job { perform_action_mailer(mailer, method, args) }
581
+ end
582
+
583
+ def active_job_internal_key
584
+ if DependencyHelper.ruby_version >= Gem::Version.new("2.7.0")
585
+ "_aj_ruby2_keywords"
586
+ else
587
+ "_aj_symbol_keys"
588
+ end
589
+ end
590
+ end
591
+ end