appsignal 2.10.6 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/.semaphore/semaphore.yml +138 -62
  4. data/CHANGELOG.md +56 -0
  5. data/README.md +4 -4
  6. data/Rakefile +16 -4
  7. data/appsignal.gemspec +1 -1
  8. data/build_matrix.yml +20 -9
  9. data/ext/Rakefile +2 -0
  10. data/ext/agent.yml +19 -19
  11. data/ext/appsignal_extension.c +10 -1
  12. data/ext/base.rb +22 -4
  13. data/ext/extconf.rb +2 -0
  14. data/gemfiles/padrino.gemfile +2 -2
  15. data/gemfiles/rails-4.2.gemfile +9 -2
  16. data/gemfiles/rails-5.0.gemfile +1 -0
  17. data/gemfiles/rails-5.1.gemfile +1 -0
  18. data/gemfiles/rails-5.2.gemfile +1 -0
  19. data/gemfiles/rails-6.0.gemfile +1 -0
  20. data/gemfiles/resque-1.gemfile +7 -0
  21. data/gemfiles/{resque.gemfile → resque-2.gemfile} +1 -1
  22. data/lib/appsignal.rb +22 -1
  23. data/lib/appsignal/auth_check.rb +4 -2
  24. data/lib/appsignal/capistrano.rb +2 -0
  25. data/lib/appsignal/cli/diagnose.rb +1 -1
  26. data/lib/appsignal/config.rb +85 -16
  27. data/lib/appsignal/environment.rb +126 -0
  28. data/lib/appsignal/extension.rb +6 -5
  29. data/lib/appsignal/extension/jruby.rb +16 -5
  30. data/lib/appsignal/hooks.rb +25 -0
  31. data/lib/appsignal/hooks/active_job.rb +137 -0
  32. data/lib/appsignal/hooks/net_http.rb +10 -13
  33. data/lib/appsignal/hooks/puma.rb +1 -58
  34. data/lib/appsignal/hooks/redis.rb +2 -0
  35. data/lib/appsignal/hooks/resque.rb +60 -0
  36. data/lib/appsignal/hooks/sequel.rb +2 -0
  37. data/lib/appsignal/hooks/sidekiq.rb +18 -192
  38. data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
  39. data/lib/appsignal/integrations/object.rb +4 -0
  40. data/lib/appsignal/integrations/que.rb +1 -1
  41. data/lib/appsignal/integrations/resque.rb +9 -12
  42. data/lib/appsignal/integrations/resque_active_job.rb +9 -24
  43. data/lib/appsignal/probes.rb +7 -0
  44. data/lib/appsignal/probes/puma.rb +61 -0
  45. data/lib/appsignal/probes/sidekiq.rb +104 -0
  46. data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
  47. data/lib/appsignal/system.rb +0 -6
  48. data/lib/appsignal/transaction.rb +32 -7
  49. data/lib/appsignal/utils/deprecation_message.rb +6 -2
  50. data/lib/appsignal/version.rb +1 -1
  51. data/lib/puma/plugin/appsignal.rb +2 -1
  52. data/spec/lib/appsignal/auth_check_spec.rb +23 -0
  53. data/spec/lib/appsignal/capistrano2_spec.rb +1 -1
  54. data/spec/lib/appsignal/capistrano3_spec.rb +1 -1
  55. data/spec/lib/appsignal/cli/diagnose_spec.rb +44 -1
  56. data/spec/lib/appsignal/config_spec.rb +44 -1
  57. data/spec/lib/appsignal/environment_spec.rb +167 -0
  58. data/spec/lib/appsignal/extension/jruby_spec.rb +31 -28
  59. data/spec/lib/appsignal/extension_install_failure_spec.rb +23 -0
  60. data/spec/lib/appsignal/hooks/activejob_spec.rb +591 -0
  61. data/spec/lib/appsignal/hooks/delayed_job_spec.rb +187 -166
  62. data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
  63. data/spec/lib/appsignal/hooks/resque_spec.rb +185 -0
  64. data/spec/lib/appsignal/hooks/sidekiq_spec.rb +297 -549
  65. data/spec/lib/appsignal/hooks_spec.rb +57 -0
  66. data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
  67. data/spec/lib/appsignal/integrations/que_spec.rb +25 -6
  68. data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +20 -137
  69. data/spec/lib/appsignal/integrations/resque_spec.rb +20 -85
  70. data/spec/lib/appsignal/marker_spec.rb +1 -1
  71. data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
  72. data/spec/lib/appsignal/probes/sidekiq_spec.rb +204 -0
  73. data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
  74. data/spec/lib/appsignal/system_spec.rb +0 -36
  75. data/spec/lib/appsignal/transaction_spec.rb +35 -20
  76. data/spec/lib/appsignal_spec.rb +22 -0
  77. data/spec/lib/puma/appsignal_spec.rb +1 -1
  78. data/spec/spec_helper.rb +5 -0
  79. data/spec/support/helpers/action_mailer_helpers.rb +25 -0
  80. data/spec/support/helpers/config_helpers.rb +3 -2
  81. data/spec/support/helpers/dependency_helper.rb +12 -0
  82. data/spec/support/helpers/env_helpers.rb +1 -1
  83. data/spec/support/helpers/environment_metdata_helper.rb +16 -0
  84. data/spec/support/helpers/transaction_helpers.rb +6 -0
  85. data/spec/support/stubs/sidekiq/api.rb +2 -2
  86. data/spec/support/testing.rb +19 -19
  87. metadata +31 -9
  88. data/lib/appsignal/integrations/net_http.rb +0 -16
@@ -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")
@@ -263,7 +263,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
263
263
  },
264
264
  "download" => {
265
265
  "download_url" => kind_of(String),
266
- "checksum" => "verified"
266
+ "checksum" => "verified",
267
+ "http_proxy" => nil
267
268
  },
268
269
  "build" => {
269
270
  "time" => kind_of(String),
@@ -289,6 +290,8 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
289
290
  jruby = Appsignal::System.jruby?
290
291
  expect(output).to include(
291
292
  "Extension installation report",
293
+ "Installation result",
294
+ " Status: success",
292
295
  "Language details",
293
296
  " Implementation: #{jruby ? "jruby" : "ruby"}",
294
297
  " Ruby version: #{"#{rbconfig["ruby_version"]}-p#{rbconfig["PATCHLEVEL"]}"}",
@@ -309,6 +312,46 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
309
312
  )
310
313
  end
311
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
+
312
355
  context "without install report" do
313
356
  let(:error) { RuntimeError.new("foo") }
314
357
  before do
@@ -1,4 +1,18 @@
1
1
  describe Appsignal::Config do
2
+ describe "config keys" do
3
+ it "all config keys have an environment variable version registered" do
4
+ config = Appsignal::Config
5
+ mapped_env_keys = config::ENV_TO_KEY_MAPPING.keys.sort
6
+ configured_env_keys = (
7
+ config::ENV_STRING_KEYS +
8
+ config::ENV_BOOLEAN_KEYS +
9
+ config::ENV_ARRAY_KEYS
10
+ ).sort
11
+
12
+ expect(mapped_env_keys).to eql(configured_env_keys)
13
+ end
14
+ end
15
+
2
16
  describe "#initialize" do
3
17
  describe "environment" do
4
18
  context "when environment is nil" do
@@ -148,6 +162,7 @@ describe Appsignal::Config do
148
162
  :instrument_redis => true,
149
163
  :instrument_sequel => true,
150
164
  :skip_session_data => false,
165
+ :send_environment_metadata => true,
151
166
  :send_params => true,
152
167
  :endpoint => "https://push.appsignal.com",
153
168
  :push_api_key => "abc",
@@ -243,6 +258,27 @@ describe Appsignal::Config do
243
258
  end
244
259
  end
245
260
 
261
+ context "with an overriden config file" do
262
+ let(:config) do
263
+ project_fixture_config("production", {}, Appsignal.logger, File.join(project_fixture_path, "config", "appsignal.yml"))
264
+ end
265
+
266
+ it "is valid and active" do
267
+ expect(config.valid?).to be_truthy
268
+ expect(config.active?).to be_truthy
269
+ end
270
+
271
+ context "with an invalid overriden config file" do
272
+ let(:config) do
273
+ project_fixture_config("production", {}, Appsignal.logger, File.join(project_fixture_path, "config", "missing.yml"))
274
+ end
275
+
276
+ it "is not valid" do
277
+ expect(config.valid?).to be_falsy
278
+ end
279
+ end
280
+ end
281
+
246
282
  context "with the config file causing an error" do
247
283
  let(:config_path) do
248
284
  File.expand_path(
@@ -396,6 +432,7 @@ describe Appsignal::Config do
396
432
  :debug => true
397
433
  )
398
434
  end
435
+ let(:working_directory_path) { File.join(tmp_dir, "test_working_directory_path") }
399
436
  let(:env_config) do
400
437
  {
401
438
  :running_in_container => true,
@@ -411,7 +448,9 @@ describe Appsignal::Config do
411
448
  :instrument_sequel => false,
412
449
  :files_world_accessible => false,
413
450
  :request_headers => %w[accept accept-charset],
414
- :revision => "v2.5.1"
451
+ :revision => "v2.5.1",
452
+ :send_environment_metadata => false,
453
+ :working_directory_path => working_directory_path
415
454
  }
416
455
  end
417
456
  before do
@@ -428,6 +467,8 @@ describe Appsignal::Config do
428
467
  ENV["APPSIGNAL_INSTRUMENT_SEQUEL"] = "false"
429
468
  ENV["APPSIGNAL_FILES_WORLD_ACCESSIBLE"] = "false"
430
469
  ENV["APPSIGNAL_REQUEST_HEADERS"] = "accept,accept-charset"
470
+ ENV["APPSIGNAL_SEND_ENVIRONMENT_METADATA"] = "false"
471
+ ENV["APPSIGNAL_WORKING_DIRECTORY_PATH"] = working_directory_path
431
472
  ENV["APP_REVISION"] = "v2.5.1"
432
473
  end
433
474
 
@@ -527,6 +568,7 @@ describe Appsignal::Config do
527
568
  config[:running_in_container] = false
528
569
  config[:dns_servers] = ["8.8.8.8", "8.8.4.4"]
529
570
  config[:transaction_debug_mode] = true
571
+ config[:send_environment_metadata] = false
530
572
  config[:revision] = "v2.5.1"
531
573
  config.write_to_environment
532
574
  end
@@ -555,6 +597,7 @@ describe Appsignal::Config do
555
597
  expect(ENV["_APPSIGNAL_DNS_SERVERS"]).to eq "8.8.8.8,8.8.4.4"
556
598
  expect(ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"]).to eq "true"
557
599
  expect(ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"]).to eq "true"
600
+ expect(ENV["_APPSIGNAL_SEND_ENVIRONMENT_METADATA"]).to eq "false"
558
601
  expect(ENV["_APP_REVISION"]).to eq "v2.5.1"
559
602
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIR_PATH")
560
603
  expect(ENV).to_not have_key("_APPSIGNAL_WORKING_DIRECTORY_PATH")
@@ -0,0 +1,167 @@
1
+ describe Appsignal::Environment do
2
+ include EnvironmentMetadataHelper
3
+
4
+ before(:context) { start_agent }
5
+ before { capture_environment_metadata_report_calls }
6
+
7
+ def report(key, &value_block)
8
+ described_class.report(key, &value_block)
9
+ end
10
+
11
+ describe ".report" do
12
+ it "sends environment metadata to the extension" do
13
+ logs =
14
+ capture_logs do
15
+ report("_test_ruby_version") { "1.0.0" }
16
+ expect_environment_metadata("_test_ruby_version", "1.0.0")
17
+ end
18
+ expect(logs).to be_empty
19
+ end
20
+
21
+ context "when the key is a non String type" do
22
+ it "does not set the value" do
23
+ logs =
24
+ capture_logs do
25
+ report(:_test_symbol) { "1.0.0" }
26
+ expect_not_environment_metadata(:_test_symbol)
27
+ expect_not_environment_metadata("_test_symbol")
28
+ end
29
+ expect(logs).to contains_log(
30
+ :error,
31
+ "Unable to report on environment metadata: Unsupported value type for :_test_symbol"
32
+ )
33
+ end
34
+ end
35
+
36
+ context "when the key is nil" do
37
+ it "does not set the value" do
38
+ logs =
39
+ capture_logs do
40
+ report(nil) { "1" }
41
+ expect_not_environment_metadata(nil)
42
+ end
43
+ expect(logs).to contains_log(
44
+ :error,
45
+ "Unable to report on environment metadata: Unsupported value type for nil"
46
+ )
47
+ end
48
+ end
49
+
50
+ context "when the value is true or false" do
51
+ it "reports true or false as Strings" do
52
+ logs =
53
+ capture_logs do
54
+ report("_test_true") { true }
55
+ report("_test_false") { false }
56
+ expect_environment_metadata("_test_true", "true")
57
+ expect_environment_metadata("_test_false", "false")
58
+ end
59
+ expect(logs).to be_empty
60
+ end
61
+ end
62
+
63
+ context "when the value is nil" do
64
+ it "does not set the value" do
65
+ logs =
66
+ capture_logs do
67
+ report("_test_ruby_version") { nil }
68
+ expect_not_environment_metadata("_test_ruby_version")
69
+ end
70
+ expect(logs).to contains_log(
71
+ :error,
72
+ "Unable to report on environment metadata \"_test_ruby_version\": " \
73
+ "Unsupported value type for nil"
74
+ )
75
+ end
76
+ end
77
+
78
+ context "when the value block raises an error" do
79
+ it "does not re-raise the error and writes it to the log" do
80
+ logs =
81
+ capture_logs do
82
+ report("_test_error") { raise "uh oh" }
83
+ expect_not_environment_metadata("_test_error")
84
+ end
85
+ expect(logs).to contains_log(
86
+ :error,
87
+ "Unable to report on environment metadata \"_test_error\":\n" \
88
+ "RuntimeError: uh oh"
89
+ )
90
+ end
91
+ end
92
+
93
+ context "when something unforseen errors" do
94
+ it "does not re-raise the error and writes it to the log" do
95
+ klass = Class.new do
96
+ def inspect
97
+ raise "inspect error"
98
+ end
99
+ end
100
+
101
+ logs =
102
+ capture_logs do
103
+ report(klass.new) { raise "value error" }
104
+ expect(Appsignal::Extension).to_not have_received(:set_environment_metadata)
105
+ end
106
+ expect(logs).to contains_log(
107
+ :error,
108
+ "Unable to report on environment metadata:\n" \
109
+ "RuntimeError: inspect error"
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ describe ".report_supported_gems" do
116
+ it "reports about all AppSignal supported gems in the bundle" do
117
+ logs = capture_logs { described_class.report_supported_gems }
118
+
119
+ expect(logs).to be_empty
120
+
121
+ bundle_gem_specs = ::Bundler.rubygems.all_specs
122
+ rack_spec = bundle_gem_specs.find { |s| s.name == "rack" }
123
+ rake_spec = bundle_gem_specs.find { |s| s.name == "rake" }
124
+ expect_environment_metadata("ruby_rack_version", rack_spec.version.to_s)
125
+ expect_environment_metadata("ruby_rake_version", rake_spec.version.to_s)
126
+ expect(rack_spec.version.to_s).to_not be_empty
127
+ expect(rake_spec.version.to_s).to_not be_empty
128
+ end
129
+
130
+ context "when something unforseen errors" do
131
+ it "does not re-raise the error and writes it to the log" do
132
+ expect(Bundler).to receive(:rubygems).and_raise(RuntimeError, "bundler error")
133
+
134
+ logs = capture_logs { described_class.report_supported_gems }
135
+ expect(logs).to contains_log(
136
+ :error,
137
+ "Unable to report supported gems:\nRuntimeError: bundler error"
138
+ )
139
+ end
140
+ end
141
+ end
142
+
143
+ describe ".report_enabled" do
144
+ it "reports a feature being enabled" do
145
+ logs = capture_logs { described_class.report_enabled("a_test") }
146
+
147
+ expect(logs).to be_empty
148
+ expect_environment_metadata("ruby_a_test_enabled", "true")
149
+ end
150
+
151
+ context "when something unforseen errors" do
152
+ it "does not re-raise the error and writes it to the log" do
153
+ klass = Class.new do
154
+ def to_s
155
+ raise "to_s error"
156
+ end
157
+ end
158
+
159
+ logs = capture_logs { described_class.report_enabled(klass.new) }
160
+ expect(logs).to contains_log(
161
+ :error,
162
+ "Unable to report integration enabled:\nRuntimeError: to_s error"
163
+ )
164
+ end
165
+ end
166
+ end
167
+ end
@@ -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