activetracker 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +11 -0
  6. data/Gemfile +6 -0
  7. data/Gemfile.lock +160 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +399 -0
  10. data/Rakefile +21 -0
  11. data/THOUGHTS.md +50 -0
  12. data/activetracker.gemspec +49 -0
  13. data/app/assets/config/active_tracker_manifest.js +4 -0
  14. data/app/assets/images/active_tracker/.keep +0 -0
  15. data/app/assets/images/active_tracker/logo.svg +6 -0
  16. data/app/assets/images/active_tracker/reload.svg +10 -0
  17. data/app/assets/javascripts/active_tracker/active_tracker.js +15 -0
  18. data/app/assets/javascripts/active_tracker/tabs.js +12 -0
  19. data/app/assets/javascripts/active_tracker/tags.js +18 -0
  20. data/app/assets/javascripts/active_tracker/zepto.js +2 -0
  21. data/app/assets/stylesheets/active_tracker/active_tracker.css.scss +21 -0
  22. data/app/assets/stylesheets/active_tracker/tailwind.min.scss +1 -0
  23. data/app/controllers/active_tracker/base_controller.rb +22 -0
  24. data/app/controllers/active_tracker/dashboard_controller.rb +10 -0
  25. data/app/controllers/active_tracker/exceptions_controller.rb +41 -0
  26. data/app/controllers/active_tracker/queries_controller.rb +39 -0
  27. data/app/controllers/active_tracker/requests_controller.rb +41 -0
  28. data/app/helpers/active_tracker/application_helper.rb +4 -0
  29. data/app/helpers/active_tracker/images_helper.rb +8 -0
  30. data/app/helpers/active_tracker/output_helper.rb +35 -0
  31. data/app/helpers/active_tracker/pagination_helper.rb +16 -0
  32. data/app/jobs/active_tracker/application_job.rb +4 -0
  33. data/app/mailers/active_tracker/application_mailer.rb +6 -0
  34. data/app/models/active_tracker/application_record.rb +5 -0
  35. data/app/views/active_tracker/common/_empty.html.erb +9 -0
  36. data/app/views/active_tracker/common/_pagination.html.erb +22 -0
  37. data/app/views/active_tracker/common/_plugin_nav.html.erb +10 -0
  38. data/app/views/active_tracker/dashboard/index.html.erb +18 -0
  39. data/app/views/active_tracker/exceptions/index.html.erb +44 -0
  40. data/app/views/active_tracker/exceptions/show.html.erb +56 -0
  41. data/app/views/active_tracker/queries/index.html.erb +38 -0
  42. data/app/views/active_tracker/queries/show.html.erb +39 -0
  43. data/app/views/active_tracker/requests/_request.html.erb +30 -0
  44. data/app/views/active_tracker/requests/index.html.erb +26 -0
  45. data/app/views/active_tracker/requests/show.html.erb +81 -0
  46. data/app/views/layouts/active_tracker/active_tracker.html.erb +46 -0
  47. data/bin/console +14 -0
  48. data/bin/rails +25 -0
  49. data/bin/setup +8 -0
  50. data/doc/logo.md +11 -0
  51. data/integration/generators/installer.rb +7 -0
  52. data/integration/templates/initializer.rb +32 -0
  53. data/lib/active_tracker.rb +41 -0
  54. data/lib/active_tracker/configuration.rb +67 -0
  55. data/lib/active_tracker/engine.rb +24 -0
  56. data/lib/active_tracker/exception_capturer.rb +38 -0
  57. data/lib/active_tracker/model.rb +153 -0
  58. data/lib/active_tracker/output_capturer.rb +37 -0
  59. data/lib/active_tracker/plugin.rb +4 -0
  60. data/lib/active_tracker/plugin/base.rb +9 -0
  61. data/lib/active_tracker/plugin/exception.rb +113 -0
  62. data/lib/active_tracker/plugin/query.rb +128 -0
  63. data/lib/active_tracker/plugin/request.rb +163 -0
  64. data/lib/active_tracker/rails_logger.rb +120 -0
  65. data/lib/active_tracker/router.rb +16 -0
  66. data/lib/active_tracker/version.rb +3 -0
  67. data/lib/activetracker.rb +3 -0
  68. data/lib/tasks/active_tracker_tasks.rake +12 -0
  69. data/spec/activetracker_spec.rb +36 -0
  70. data/spec/dummy/Rakefile +6 -0
  71. data/spec/dummy/app/assets/config/manifest.js +3 -0
  72. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  73. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  74. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  75. data/spec/dummy/app/javascript/packs/application.js +15 -0
  76. data/spec/dummy/app/jobs/application_job.rb +7 -0
  77. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  78. data/spec/dummy/app/models/application_record.rb +3 -0
  79. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  80. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  81. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  82. data/spec/dummy/bin/rails +4 -0
  83. data/spec/dummy/bin/rake +4 -0
  84. data/spec/dummy/bin/setup +33 -0
  85. data/spec/dummy/config.ru +5 -0
  86. data/spec/dummy/config/application.rb +37 -0
  87. data/spec/dummy/config/boot.rb +5 -0
  88. data/spec/dummy/config/database.yml +54 -0
  89. data/spec/dummy/config/environment.rb +5 -0
  90. data/spec/dummy/config/environments/development.rb +62 -0
  91. data/spec/dummy/config/environments/production.rb +107 -0
  92. data/spec/dummy/config/environments/test.rb +48 -0
  93. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  94. data/spec/dummy/config/initializers/assets.rb +12 -0
  95. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  96. data/spec/dummy/config/initializers/content_security_policy.rb +28 -0
  97. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  98. data/spec/dummy/config/initializers/disable_remote_forms.rb +1 -0
  99. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  100. data/spec/dummy/config/initializers/inflections.rb +16 -0
  101. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  102. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  103. data/spec/dummy/config/locales/en.yml +33 -0
  104. data/spec/dummy/config/puma.rb +38 -0
  105. data/spec/dummy/config/routes.rb +3 -0
  106. data/spec/dummy/config/spring.rb +6 -0
  107. data/spec/dummy/config/storage.yml +34 -0
  108. data/spec/dummy/public/404.html +67 -0
  109. data/spec/dummy/public/422.html +67 -0
  110. data/spec/dummy/public/500.html +66 -0
  111. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  112. data/spec/dummy/public/apple-touch-icon.png +0 -0
  113. data/spec/dummy/public/favicon.ico +0 -0
  114. data/spec/lib/active_tracker/model_spec.rb +38 -0
  115. data/spec/spec_helper.rb +18 -0
  116. metadata +348 -0
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "activetracker"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails gems
3
+ # installed from the root of your application.
4
+
5
+ ENGINE_ROOT = File.expand_path('..', __dir__)
6
+ ENGINE_PATH = File.expand_path('../lib/active_tracker/engine', __dir__)
7
+ APP_PATH = File.expand_path('../test/dummy/config/application', __dir__)
8
+
9
+ # Set up gems listed in the Gemfile.
10
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
11
+ require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
12
+
13
+ require "rails"
14
+ # Pick the frameworks you want:
15
+ require "active_model/railtie"
16
+ require "active_job/railtie"
17
+ require "active_record/railtie"
18
+ require "active_storage/engine"
19
+ require "action_controller/railtie"
20
+ require "action_mailer/railtie"
21
+ require "action_view/railtie"
22
+ # require "action_cable/engine"
23
+ require "sprockets/railtie"
24
+ require "rails/test_unit/railtie"
25
+ require 'rails/engine/commands'
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,11 @@
1
+ # Logo
2
+
3
+ The icon used as part of ActiveTracker's logo was a free/promotional order from Logofound.com (order #24181, September 27, 2019).
4
+
5
+ When used as part of referring to ActiveTracker, it should not be used in any way implying any form of endorsement.
6
+
7
+ ## Original SVG
8
+
9
+ The SVG below is the one originally supplied to us, but run through [SVGOMG](https://jakearchibald.github.io/svgomg/) to optimise it
10
+
11
+ <svg xmlns="http://www.w3.org/2000/svg" width="11in" height="816" viewBox="0 0 11000 8500" shape-rendering="geometricPrecision" text-rendering="geometricPrecision" image-rendering="optimizeQuality" fill-rule="evenodd" clip-rule="evenodd"><g fill="#333"><path d="M7361 4662c-55 403-243 764-517 1039-330 330-786 534-1289 534-146 0-289-17-425-50l-912 1502c-142 234-447 309-681 167s-309-447-167-681l895-1474c-329-330-532-785-532-1287 0-85 6-169 17-251 209 90 429 167 647 230v21c0 320 130 610 339 819 210 210 499 339 819 339s610-130 819-339c142-142 247-320 300-519 222-3 441-17 652-45l34-5z" fill-rule="nonzero"/><path d="M4658 1194c315 0 478 330 975 330s696-330 1059-330c239 0 381 202 434 434 73 316 135 744 306 1225 111 45 216 93 312 141 479 243 769 528 726 794-46 282-454 481-1081 564-637 85-1469 47-2310-128-810-169-1443-428-1811-701-347-257-456-524-273-745 172-207 575-343 1090-400 67-255 90-511 138-750 48-234 195-434 434-434zm2866 2305c-438 81-864 96-1305 71-398-22-820-77-1245-165-417-87-692-185-1039-327-154-63-116-123-82-177 46-75 96-165 137-245 873 293 2373 625 3494 397 46 102 100 209 138 313 19 54 39 107-98 132z"/></g></svg>
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ initializer "activetracker.rb" do
4
+ File.read(ActiveTracker::Engine.root.join("integration", "templates", "initializer.rb"))
5
+ end
6
+
7
+ route "ActiveTracker::Router.load"
@@ -0,0 +1,32 @@
1
+ # Which plugins are active. The full list installed by default is:
2
+ #
3
+ # ActiveTracker::Plugin::Request # Entire request log, with response
4
+ # ActiveTracker::Plugin::Query # Queries performed with ms taken
5
+ # ActiveTracker::Plugin::Exception # When an exception is raised
6
+ # ActiveTracker::Plugin::Schedule # When a scheduled job is queued
7
+ # ActiveTracker::Plugin::ActiveJob # When a job is processed, with output
8
+ # ActiveTracker::Plugin::ActionMail # When an email is sent, with the body
9
+ # ActiveTracker::Plugin::Event # Random events triggered in the system
10
+
11
+ ActiveTracker::Configuration.plugins = [
12
+ ActiveTracker::Plugin::Request,
13
+ ActiveTracker::Plugin::Query,
14
+ ActiveTracker::Plugin::Exception,
15
+ ]
16
+
17
+ # You should change this to be your correct Redis URL
18
+ ActiveTracker::Configuration.redis_url = "redis://localhost:6379/15"
19
+
20
+ # By default ActiveTracker is mounted at /activetracker
21
+ # if you'd like to change it, you can do so like this:
22
+ # ActiveTracker::Configuration.mountpoint = "debugger"
23
+
24
+ # If you want to authenticate requests to ActiveTracker with username and password
25
+ # ActiveTracker::Configuration.authentication = "username:password"
26
+ # or using a proc:
27
+ # ActiveTracker::Configuration.authentication do
28
+ # false unless params[:password] == "password"
29
+ # end
30
+
31
+ # If you would like a particular number of items per page
32
+ # ActiveTracker::Configuration.per_page = 20
@@ -0,0 +1,41 @@
1
+ require "active_tracker/version"
2
+ require "redis"
3
+
4
+ module ActiveTracker
5
+ class Error < StandardError; end
6
+
7
+ def self.reset_connection
8
+ @redis = nil
9
+ end
10
+
11
+ def self.connection
12
+ if @redis
13
+ begin
14
+ @redis.ping
15
+ rescue
16
+ @redis = nil
17
+ end
18
+ end
19
+
20
+ @redis ||= Redis.new(url: ActiveTracker::Configuration.redis_url)
21
+
22
+ begin
23
+ @redis.ping
24
+ rescue
25
+ @redis = nil
26
+ end
27
+
28
+ @redis
29
+ rescue Errno::ECONNREFUSED, Redis::CannotConnectError
30
+ @redis = nil
31
+ end
32
+
33
+ def self.connection_offline?
34
+ offline = connection.nil?
35
+ Rails.logger.error("ActiveTracker: Redis is offline/unreachable") if offline
36
+ offline
37
+ end
38
+ end
39
+
40
+ require "active_tracker/configuration"
41
+ require "active_tracker/engine"
@@ -0,0 +1,67 @@
1
+ module ActiveTracker
2
+ class Configuration
3
+ def self.plugins
4
+ @@plugins ||= [
5
+ ActiveTracker::Plugin::Request,
6
+ ]
7
+ @@plugins
8
+ end
9
+
10
+ def self.plugins=(items)
11
+ items.each do |i|
12
+ if i.respond_to?(:register)
13
+ i.register
14
+ else
15
+ raise PluginInvalidError.new("#{i.name} doesn't correctly implement the ActiveTracker API")
16
+ end
17
+ end
18
+
19
+ @@plugins = items.dup
20
+ end
21
+
22
+ def self.redis_url
23
+ @@redis_url ||= "redis://localhost:6379/15"
24
+ end
25
+
26
+ def self.redis_url=(url)
27
+ unless url.start_with?("redis://")
28
+ raise PluginInvalidError.new("redis_url isn't a valid Redis URL - should begin with redis://")
29
+ end
30
+ @@redis_url = url
31
+ end
32
+
33
+ def self.mountpoint
34
+ @@mountpoint ||= "activetracker"
35
+ end
36
+
37
+ def self.root_path
38
+ "/#{mountpoint}"
39
+ end
40
+
41
+ def self.mountpoint=(path)
42
+ @@mountpoint = path
43
+ end
44
+
45
+ def self.per_page=(value)
46
+ @@per_page = value
47
+ end
48
+
49
+ def self.per_page
50
+ @@per_page ||= 20
51
+ @@per_page.to_i
52
+ end
53
+
54
+ def self.authentication(&block)
55
+ if block
56
+ @@authentication = block
57
+ end
58
+ @@authentication ||= nil
59
+ end
60
+
61
+ def self.authentication=(value)
62
+ @@authentication = value
63
+ end
64
+
65
+ class PluginInvalidError < ActiveTracker::Error ; end
66
+ end
67
+ end
@@ -0,0 +1,24 @@
1
+ module ActiveTracker
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace ActiveTracker
4
+ config.autoload_paths += Dir["#{config.root}/lib/**/"]
5
+
6
+ initializer 'active_tracker_helper.action_controller' do
7
+ ActiveSupport.on_load :action_controller do
8
+ helper ActiveTracker::ImagesHelper
9
+ helper ActiveTracker::ApplicationHelper
10
+ helper ActiveTracker::PaginationHelper
11
+ helper ActiveTracker::OutputHelper
12
+ end
13
+ end
14
+
15
+ config.generators do |g|
16
+ g.test_framework :rspec, fixture: false
17
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
18
+ g.assets false
19
+ g.helper false
20
+ end
21
+
22
+ config.assets.precompile += %w(active_tracker_manifest active_tracker/active_tracker.js active_tracker/active_tracker.css)
23
+ end
24
+ end
@@ -0,0 +1,38 @@
1
+ module ActiveTracker
2
+ class ExceptionCapturer
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+
9
+ response = @app.call(env)
10
+
11
+ framework_exception = env['action_dispatch.exception']
12
+ if framework_exception
13
+ record_exception(env, framework_exception)
14
+ end
15
+
16
+ response
17
+ rescue Exception => exception
18
+
19
+ record_exception(env, exception)
20
+ raise exception
21
+ end
22
+
23
+ def record_exception(env, exception)
24
+ if env['action_dispatch.backtrace_cleaner']
25
+ backtrace = env['action_dispatch.backtrace_cleaner'].filter(exception.backtrace)
26
+ backtrace = exception.backtrace if backtrace.blank?
27
+ else
28
+ backtrace = exception.backtrace
29
+ end
30
+ class_name = exception.class.name
31
+ message = exception.message
32
+ backtrace = backtrace || []
33
+
34
+ ActiveTracker::Plugin::Exception.exception_capture(class_name, message, backtrace)
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,153 @@
1
+ require 'json'
2
+
3
+ module ActiveTracker
4
+ class Model
5
+ PREFIX = "/ActiveTracker".freeze
6
+
7
+ def self.find(key)
8
+ return nil if ActiveTracker.connection_offline?
9
+
10
+ connection = ActiveTracker.connection
11
+ value = connection.get(key)
12
+ if value.nil?
13
+ raise NotFound.new("Couldn't find entry - #{key}")
14
+ else
15
+ if value.start_with?(PREFIX)
16
+ find(value)
17
+ else
18
+ self.new(key, value)
19
+ end
20
+ end
21
+ end
22
+
23
+ def self.delete(key)
24
+ ActiveTracker.connection.del(key)
25
+ end
26
+
27
+ def self.all(type, tags: {}, data_type: nil)
28
+ return [] if ActiveTracker.connection_offline?
29
+
30
+ keys = "#{PREFIX}/#{type}/"
31
+
32
+ keys += "*"
33
+
34
+ keys += tags.sort_by { |k,v| k.to_s }.map do |k,v|
35
+ "#{k}:#{CGI.escape(v.to_s)}"
36
+ end.join("*")
37
+
38
+ keys += "*" unless keys.end_with?("*")
39
+
40
+ if data_type
41
+ keys += "/#{data_type}"
42
+ end
43
+
44
+ connection = ActiveTracker.connection
45
+ result = connection.keys(keys).sort { |a,b| b <=> a }.map { |key| self.new(key, {}.to_json) }
46
+ end
47
+
48
+ def self.generate_key(type, log_time, tags, data_type)
49
+ key = PREFIX + "/#{type}/#{log_time}"
50
+ converted_tags = tags.sort_by { |k,v| k.to_s }.map {|k,v| "#{k}:#{CGI.escape(v.to_s)}"}
51
+ if converted_tags.any?
52
+ key = key + "/" + converted_tags.join("/")
53
+ end
54
+ key += "/#{data_type}"
55
+ key.gsub!(%r{/{2,}}, '/')
56
+ key
57
+ end
58
+
59
+ def self.save(type, data, tags: {}, data_type: nil, expiry: 7.days, log_at: Time.current)
60
+ return nil if ActiveTracker.connection_offline?
61
+
62
+ if log_at.respond_to?(:strftime)
63
+ log_time = log_at.strftime("%Y%m%d%H%M%S")
64
+ else
65
+ log_time = log_at
66
+ end
67
+ key = generate_key(type, log_time, tags, data_type)
68
+ value = data.to_json
69
+ connection = ActiveTracker.connection
70
+ connection.set(key, value)
71
+ connection.expire(key, expiry)
72
+ if tags[:id].present?
73
+ connection.set(tags[:id], key)
74
+ connection.expire(tags[:id], expiry)
75
+ end
76
+ key
77
+ end
78
+
79
+ def self.find_or_create(type, tags: {}, data_type: nil)
80
+ keys = all(type, tags: tags, data_type: data_type)
81
+ if keys.length > 0
82
+ obj = find(keys.first.key) rescue nil
83
+ end
84
+
85
+ if !obj
86
+ obj = new(generate_key(type, "-", tags, data_type), {}.to_json, false)
87
+ end
88
+ yield obj
89
+ obj.save
90
+ end
91
+
92
+ def self.paginate(items, page, per_page)
93
+ page = (page || 1).to_i
94
+
95
+ total = items.length
96
+ start_point = (page - 1) * per_page
97
+
98
+ items = items[start_point, per_page]
99
+
100
+ total_pages = total / per_page
101
+ total_pages += 1 if (total % per_page != 0)
102
+
103
+ window = []
104
+ window << page - 2 if page > 2
105
+ window << page - 1 if page > 1
106
+ window << page
107
+ window << page + 1 if (total_pages - page) > 0
108
+ window << page + 2 if (total_pages - page) > 1
109
+
110
+ [items, {total: total, total_pages: total_pages, page: page, window: window}]
111
+ end
112
+
113
+ def initialize(key, value, persisted = true)
114
+ @attrs = {key: key, persisted: persisted}
115
+ key.gsub!(/^A#{PREFIX}/, "")
116
+ parts = key.split("/")
117
+ _ = parts.shift # Starts with a /
118
+ _ = parts.shift # then comes PREFIX
119
+ @attrs[:type] = parts.shift
120
+ @attrs[:data_type] = parts.pop
121
+ t = parts.shift
122
+ @attrs[:log_at] = Time.parse(t) rescue t
123
+ @attrs[:tags] = {}
124
+ parts.sort.each do |part|
125
+ tag_name, tag_value = part.split(":")
126
+ if tag_name == "id"
127
+ @attrs[:id] = tag_value
128
+ else
129
+ @attrs[:tags][tag_name.to_sym] = CGI.unescape("#{tag_value}")
130
+ end
131
+ end
132
+ value = JSON.parse(value)
133
+ self.send("data=", value)
134
+ value.each do |key, value|
135
+ self.send("#{key}=", value)
136
+ end
137
+ end
138
+
139
+ def save
140
+ self.class.save(@attrs[:type], data, tags: {id: id}.merge(@attrs[:tags]), data_type: @attrs[:data_type], expiry: expiry, log_at: log_at)
141
+ end
142
+
143
+ def method_missing(name, value = nil)
144
+ if name.to_s.end_with?("=")
145
+ @attrs[name.to_s.gsub("=", "").to_sym] = value
146
+ else
147
+ @attrs[name]
148
+ end
149
+ end
150
+
151
+ class NotFound < ActiveTracker::Error ; end
152
+ end
153
+ end
@@ -0,0 +1,37 @@
1
+ module ActiveTracker
2
+ class OutputCapturer
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ start_time = Time.current
9
+ status, headers, response = @app.call(env)
10
+ [status, headers, response]
11
+ ensure
12
+ capture(response)
13
+ duration = (Time.current.to_f - start_time.to_f) * 1000
14
+ ActiveTracker::Plugin::Request.record_duration(duration)
15
+ end
16
+
17
+ def capture(response)
18
+ body = response.body rescue nil
19
+ unless body.is_a?(String)
20
+ body = body.to_a rescue [body.body] rescue "No body given"
21
+ end
22
+
23
+ if body.respond_to?(:each)
24
+ output = []
25
+ body.each do |line|
26
+ output << line
27
+ end
28
+ output = output.join("\n")
29
+ else
30
+ output = body.to_s
31
+ end
32
+
33
+ ActiveTracker::Plugin::Request.output_capture(output)
34
+ end
35
+
36
+ end
37
+ end