sentry-ruby 5.3.1 → 5.16.1
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.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/Gemfile +26 -0
- data/Makefile +4 -0
- data/README.md +11 -8
- data/Rakefile +20 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +79 -0
- data/lib/sentry/backpressure_monitor.rb +75 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +70 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +76 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/check_in_event.rb +60 -0
- data/lib/sentry/client.rb +248 -0
- data/lib/sentry/configuration.rb +650 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +75 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +93 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +156 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +316 -0
- data/lib/sentry/integrable.rb +32 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +67 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +106 -0
- data/lib/sentry/profiler.rb +233 -0
- data/lib/sentry/propagation_context.rb +134 -0
- data/lib/sentry/puma.rb +32 -0
- data/lib/sentry/rack/capture_exceptions.rb +79 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +28 -0
- data/lib/sentry/redis.rb +108 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +360 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +273 -0
- data/lib/sentry/test_helper.rb +84 -0
- data/lib/sentry/transaction.rb +359 -0
- data/lib/sentry/transaction_event.rb +80 -0
- data/lib/sentry/transport/configuration.rb +98 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +206 -0
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +225 -0
- data/lib/sentry/utils/argument_checking_helper.rb +19 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +580 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +75 -16
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# @api private
|
5
|
+
class Backtrace
|
6
|
+
# Handles backtrace parsing line by line
|
7
|
+
class Line
|
8
|
+
RB_EXTENSION = ".rb"
|
9
|
+
# regexp (optional leading X: on windows, or JRuby9000 class-prefix)
|
10
|
+
RUBY_INPUT_FORMAT = /
|
11
|
+
^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
|
12
|
+
(\d+)
|
13
|
+
(?: :in \s `([^']+)')?$
|
14
|
+
/x.freeze
|
15
|
+
|
16
|
+
# org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
|
17
|
+
JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze
|
18
|
+
|
19
|
+
# The file portion of the line (such as app/models/user.rb)
|
20
|
+
attr_reader :file
|
21
|
+
|
22
|
+
# The line number portion of the line
|
23
|
+
attr_reader :number
|
24
|
+
|
25
|
+
# The method of the line (such as index)
|
26
|
+
attr_reader :method
|
27
|
+
|
28
|
+
# The module name (JRuby)
|
29
|
+
attr_reader :module_name
|
30
|
+
|
31
|
+
attr_reader :in_app_pattern
|
32
|
+
|
33
|
+
# Parses a single line of a given backtrace
|
34
|
+
# @param [String] unparsed_line The raw line from +caller+ or some backtrace
|
35
|
+
# @return [Line] The parsed backtrace line
|
36
|
+
def self.parse(unparsed_line, in_app_pattern)
|
37
|
+
ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
|
38
|
+
if ruby_match
|
39
|
+
_, file, number, method = ruby_match.to_a
|
40
|
+
file.sub!(/\.class$/, RB_EXTENSION)
|
41
|
+
module_name = nil
|
42
|
+
else
|
43
|
+
java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
|
44
|
+
_, module_name, method, file, number = java_match.to_a
|
45
|
+
end
|
46
|
+
new(file, number, method, module_name, in_app_pattern)
|
47
|
+
end
|
48
|
+
|
49
|
+
def initialize(file, number, method, module_name, in_app_pattern)
|
50
|
+
@file = file
|
51
|
+
@module_name = module_name
|
52
|
+
@number = number.to_i
|
53
|
+
@method = method
|
54
|
+
@in_app_pattern = in_app_pattern
|
55
|
+
end
|
56
|
+
|
57
|
+
def in_app
|
58
|
+
if file =~ in_app_pattern
|
59
|
+
true
|
60
|
+
else
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Reconstructs the line in a readable fashion
|
66
|
+
def to_s
|
67
|
+
"#{file}:#{number}:in `#{method}'"
|
68
|
+
end
|
69
|
+
|
70
|
+
def ==(other)
|
71
|
+
to_s == other.to_s
|
72
|
+
end
|
73
|
+
|
74
|
+
def inspect
|
75
|
+
"<Line:#{self}>"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/.freeze
|
80
|
+
|
81
|
+
# holder for an Array of Backtrace::Line instances
|
82
|
+
attr_reader :lines
|
83
|
+
|
84
|
+
def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
|
85
|
+
ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
|
86
|
+
|
87
|
+
ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
|
88
|
+
|
89
|
+
in_app_pattern ||= begin
|
90
|
+
Regexp.new("^(#{project_root}/)?#{app_dirs_pattern || APP_DIRS_PATTERN}")
|
91
|
+
end
|
92
|
+
|
93
|
+
lines = ruby_lines.to_a.map do |unparsed_line|
|
94
|
+
Line.parse(unparsed_line, in_app_pattern)
|
95
|
+
end
|
96
|
+
|
97
|
+
new(lines)
|
98
|
+
end
|
99
|
+
|
100
|
+
def initialize(lines)
|
101
|
+
@lines = lines
|
102
|
+
end
|
103
|
+
|
104
|
+
def inspect
|
105
|
+
"<Backtrace: " + lines.map(&:inspect).join(", ") + ">"
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
content = []
|
110
|
+
lines.each do |line|
|
111
|
+
content << line
|
112
|
+
end
|
113
|
+
content.join("\n")
|
114
|
+
end
|
115
|
+
|
116
|
+
def ==(other)
|
117
|
+
if other.respond_to?(:lines)
|
118
|
+
lines == other.lines
|
119
|
+
else
|
120
|
+
false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
# A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation.
|
7
|
+
class Baggage
|
8
|
+
SENTRY_PREFIX = 'sentry-'
|
9
|
+
SENTRY_PREFIX_REGEX = /^sentry-/.freeze
|
10
|
+
|
11
|
+
# @return [Hash]
|
12
|
+
attr_reader :items
|
13
|
+
|
14
|
+
# @return [Boolean]
|
15
|
+
attr_reader :mutable
|
16
|
+
|
17
|
+
def initialize(items, mutable: true)
|
18
|
+
@items = items
|
19
|
+
@mutable = mutable
|
20
|
+
end
|
21
|
+
|
22
|
+
# Creates a Baggage object from an incoming W3C Baggage header string.
|
23
|
+
#
|
24
|
+
# Sentry items are identified with the 'sentry-' prefix and stored in a hash.
|
25
|
+
# The presence of a Sentry item makes the baggage object immutable.
|
26
|
+
#
|
27
|
+
# @param header [String] The incoming Baggage header string.
|
28
|
+
# @return [Baggage, nil]
|
29
|
+
def self.from_incoming_header(header)
|
30
|
+
items = {}
|
31
|
+
mutable = true
|
32
|
+
|
33
|
+
header.split(',').each do |item|
|
34
|
+
item = item.strip
|
35
|
+
key, val = item.split('=')
|
36
|
+
|
37
|
+
next unless key && val
|
38
|
+
next unless key =~ SENTRY_PREFIX_REGEX
|
39
|
+
|
40
|
+
baggage_key = key.split('-')[1]
|
41
|
+
next unless baggage_key
|
42
|
+
|
43
|
+
items[CGI.unescape(baggage_key)] = CGI.unescape(val)
|
44
|
+
mutable = false
|
45
|
+
end
|
46
|
+
|
47
|
+
new(items, mutable: mutable)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Make the Baggage immutable.
|
51
|
+
# @return [void]
|
52
|
+
def freeze!
|
53
|
+
@mutable = false
|
54
|
+
end
|
55
|
+
|
56
|
+
# A {https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#envelope-header Dynamic Sampling Context}
|
57
|
+
# hash to be used in the trace envelope header.
|
58
|
+
# @return [Hash]
|
59
|
+
def dynamic_sampling_context
|
60
|
+
@items
|
61
|
+
end
|
62
|
+
|
63
|
+
# Serialize the Baggage object back to a string.
|
64
|
+
# @return [String]
|
65
|
+
def serialize
|
66
|
+
items = @items.map { |k, v| "#{SENTRY_PREFIX}#{CGI.escape(k)}=#{CGI.escape(v)}" }
|
67
|
+
items.join(',')
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Breadcrumb
|
7
|
+
module SentryLogger
|
8
|
+
LEVELS = {
|
9
|
+
::Logger::DEBUG => 'debug',
|
10
|
+
::Logger::INFO => 'info',
|
11
|
+
::Logger::WARN => 'warn',
|
12
|
+
::Logger::ERROR => 'error',
|
13
|
+
::Logger::FATAL => 'fatal'
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def add(*args, &block)
|
17
|
+
super
|
18
|
+
add_breadcrumb(*args, &block)
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_breadcrumb(severity, message = nil, progname = nil)
|
23
|
+
# because the breadcrumbs now belongs to different Hub's Scope in different threads
|
24
|
+
# we need to make sure the current thread's Hub has been set before adding breadcrumbs
|
25
|
+
return unless Sentry.initialized? && Sentry.get_current_hub
|
26
|
+
|
27
|
+
category = "logger"
|
28
|
+
|
29
|
+
# this is because the nature of Ruby Logger class:
|
30
|
+
#
|
31
|
+
# when given 1 argument, the argument will become both message and progname
|
32
|
+
#
|
33
|
+
# ```
|
34
|
+
# logger.info("foo")
|
35
|
+
# # message == progname == "foo"
|
36
|
+
# ```
|
37
|
+
#
|
38
|
+
# and to specify progname with a different message,
|
39
|
+
# we need to pass the progname as the argument and pass the message as a proc
|
40
|
+
#
|
41
|
+
# ```
|
42
|
+
# logger.info("progname") { "the message" }
|
43
|
+
# ```
|
44
|
+
#
|
45
|
+
# so the condition below is to replicate the similar behavior
|
46
|
+
if message.nil?
|
47
|
+
if block_given?
|
48
|
+
message = yield
|
49
|
+
category = progname
|
50
|
+
else
|
51
|
+
message = progname
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
return if ignored_logger?(progname) || message == ""
|
56
|
+
|
57
|
+
# some loggers will add leading/trailing space as they (incorrectly, mind you)
|
58
|
+
# think of logging as a shortcut to std{out,err}
|
59
|
+
message = message.to_s.strip
|
60
|
+
|
61
|
+
last_crumb = current_breadcrumbs.peek
|
62
|
+
# try to avoid dupes from logger broadcasts
|
63
|
+
if last_crumb.nil? || last_crumb.message != message
|
64
|
+
level = Sentry::Breadcrumb::SentryLogger::LEVELS.fetch(severity, nil)
|
65
|
+
crumb = Sentry::Breadcrumb.new(
|
66
|
+
level: level,
|
67
|
+
category: category,
|
68
|
+
message: message,
|
69
|
+
type: severity >= 3 ? "error" : level
|
70
|
+
)
|
71
|
+
|
72
|
+
Sentry.add_breadcrumb(crumb, hint: { severity: severity })
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def ignored_logger?(progname)
|
79
|
+
progname == LOGGER_PROGNAME ||
|
80
|
+
Sentry.configuration.exclude_loggers.include?(progname)
|
81
|
+
end
|
82
|
+
|
83
|
+
def current_breadcrumbs
|
84
|
+
Sentry.get_current_scope.breadcrumbs
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
::Logger.send(:prepend, Sentry::Breadcrumb::SentryLogger)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
class Breadcrumb
|
5
|
+
DATA_SERIALIZATION_ERROR_MESSAGE = "[data were removed due to serialization issues]"
|
6
|
+
|
7
|
+
# @return [String, nil]
|
8
|
+
attr_accessor :category
|
9
|
+
# @return [Hash, nil]
|
10
|
+
attr_accessor :data
|
11
|
+
# @return [String, nil]
|
12
|
+
attr_reader :level
|
13
|
+
# @return [Time, Integer, nil]
|
14
|
+
attr_accessor :timestamp
|
15
|
+
# @return [String, nil]
|
16
|
+
attr_accessor :type
|
17
|
+
# @return [String, nil]
|
18
|
+
attr_reader :message
|
19
|
+
|
20
|
+
# @param category [String, nil]
|
21
|
+
# @param data [Hash, nil]
|
22
|
+
# @param message [String, nil]
|
23
|
+
# @param timestamp [Time, Integer, nil]
|
24
|
+
# @param level [String, nil]
|
25
|
+
# @param type [String, nil]
|
26
|
+
def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: nil, type: nil)
|
27
|
+
@category = category
|
28
|
+
@data = data || {}
|
29
|
+
@timestamp = timestamp || Sentry.utc_now.to_i
|
30
|
+
@type = type
|
31
|
+
self.message = message
|
32
|
+
self.level = level
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Hash]
|
36
|
+
def to_hash
|
37
|
+
{
|
38
|
+
category: @category,
|
39
|
+
data: serialized_data,
|
40
|
+
level: @level,
|
41
|
+
message: @message,
|
42
|
+
timestamp: @timestamp,
|
43
|
+
type: @type
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param message [String]
|
48
|
+
# @return [void]
|
49
|
+
def message=(message)
|
50
|
+
@message = (message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @param level [String]
|
54
|
+
# @return [void]
|
55
|
+
def level=(level) # needed to meet the Sentry spec
|
56
|
+
@level = level == "warn" ? "warning" : level
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def serialized_data
|
62
|
+
begin
|
63
|
+
::JSON.parse(::JSON.generate(@data))
|
64
|
+
rescue Exception => e
|
65
|
+
Sentry.logger.debug(LOGGER_PROGNAME) do
|
66
|
+
<<~MSG
|
67
|
+
can't serialize breadcrumb data because of error: #{e}
|
68
|
+
data: #{@data}
|
69
|
+
MSG
|
70
|
+
end
|
71
|
+
|
72
|
+
DATA_SERIALIZATION_ERROR_MESSAGE
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sentry/breadcrumb"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class BreadcrumbBuffer
|
7
|
+
DEFAULT_SIZE = 100
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# @return [Array]
|
11
|
+
attr_accessor :buffer
|
12
|
+
|
13
|
+
# @param size [Integer, nil] If it's not provided, it'll fallback to DEFAULT_SIZE
|
14
|
+
def initialize(size = nil)
|
15
|
+
@buffer = Array.new(size || DEFAULT_SIZE)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param crumb [Breadcrumb]
|
19
|
+
# @return [void]
|
20
|
+
def record(crumb)
|
21
|
+
yield(crumb) if block_given?
|
22
|
+
@buffer.slice!(0)
|
23
|
+
@buffer << crumb
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [Array]
|
27
|
+
def members
|
28
|
+
@buffer.compact
|
29
|
+
end
|
30
|
+
|
31
|
+
# Returns the last breadcrumb stored in the buffer. If the buffer it's empty, it returns nil.
|
32
|
+
# @return [Breadcrumb, nil]
|
33
|
+
def peek
|
34
|
+
members.last
|
35
|
+
end
|
36
|
+
|
37
|
+
# Iterates through all breadcrumbs.
|
38
|
+
# @param block [Proc]
|
39
|
+
# @yieldparam crumb [Breadcrumb]
|
40
|
+
# @return [Array]
|
41
|
+
def each(&block)
|
42
|
+
members.each(&block)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [Boolean]
|
46
|
+
def empty?
|
47
|
+
members.none?
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Hash]
|
51
|
+
def to_hash
|
52
|
+
{
|
53
|
+
values: members.map(&:to_hash)
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [BreadcrumbBuffer]
|
58
|
+
def dup
|
59
|
+
copy = super
|
60
|
+
copy.buffer = buffer.deep_dup
|
61
|
+
copy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'sentry/cron/monitor_config'
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
class CheckInEvent < Event
|
8
|
+
TYPE = 'check_in'
|
9
|
+
|
10
|
+
# uuid to identify this check-in.
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :check_in_id
|
13
|
+
|
14
|
+
# Identifier of the monitor for this check-in.
|
15
|
+
# @return [String]
|
16
|
+
attr_accessor :monitor_slug
|
17
|
+
|
18
|
+
# Duration of this check since it has started in seconds.
|
19
|
+
# @return [Integer, nil]
|
20
|
+
attr_accessor :duration
|
21
|
+
|
22
|
+
# Monitor configuration to support upserts.
|
23
|
+
# @return [Cron::MonitorConfig, nil]
|
24
|
+
attr_accessor :monitor_config
|
25
|
+
|
26
|
+
# Status of this check-in.
|
27
|
+
# @return [Symbol]
|
28
|
+
attr_accessor :status
|
29
|
+
|
30
|
+
VALID_STATUSES = %i(ok in_progress error)
|
31
|
+
|
32
|
+
def initialize(
|
33
|
+
slug:,
|
34
|
+
status:,
|
35
|
+
duration: nil,
|
36
|
+
monitor_config: nil,
|
37
|
+
check_in_id: nil,
|
38
|
+
**options
|
39
|
+
)
|
40
|
+
super(**options)
|
41
|
+
|
42
|
+
self.monitor_slug = slug
|
43
|
+
self.status = status
|
44
|
+
self.duration = duration
|
45
|
+
self.monitor_config = monitor_config
|
46
|
+
self.check_in_id = check_in_id || SecureRandom.uuid.delete('-')
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Hash]
|
50
|
+
def to_hash
|
51
|
+
data = super
|
52
|
+
data[:check_in_id] = check_in_id
|
53
|
+
data[:monitor_slug] = monitor_slug
|
54
|
+
data[:status] = status
|
55
|
+
data[:duration] = duration if duration
|
56
|
+
data[:monitor_config] = monitor_config.to_hash if monitor_config
|
57
|
+
data
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|