skylight 5.0.0.beta4 → 5.1.0.beta2
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/CHANGELOG.md +399 -362
- data/CLA.md +1 -1
- data/CONTRIBUTING.md +1 -1
- data/LICENSE.md +7 -17
- data/README.md +1 -1
- data/ext/extconf.rb +42 -54
- data/ext/libskylight.yml +10 -5
- data/lib/skylight.rb +20 -30
- data/lib/skylight/api.rb +22 -18
- data/lib/skylight/cli.rb +47 -46
- data/lib/skylight/cli/doctor.rb +50 -50
- data/lib/skylight/cli/helpers.rb +19 -19
- data/lib/skylight/cli/merger.rb +141 -139
- data/lib/skylight/config.rb +267 -310
- data/lib/skylight/deprecation.rb +4 -4
- data/lib/skylight/errors.rb +3 -4
- data/lib/skylight/extensions.rb +17 -29
- data/lib/skylight/extensions/source_location.rb +128 -128
- data/lib/skylight/formatters/http.rb +1 -3
- data/lib/skylight/gc.rb +30 -40
- data/lib/skylight/helpers.rb +57 -30
- data/lib/skylight/instrumenter.rb +25 -18
- data/lib/skylight/middleware.rb +31 -35
- data/lib/skylight/native.rb +8 -10
- data/lib/skylight/native_ext_fetcher.rb +10 -12
- data/lib/skylight/normalizers.rb +43 -38
- data/lib/skylight/normalizers/action_controller/process_action.rb +24 -25
- data/lib/skylight/normalizers/action_controller/send_file.rb +7 -6
- data/lib/skylight/normalizers/action_dispatch/route_set.rb +7 -7
- data/lib/skylight/normalizers/active_job/perform.rb +48 -44
- data/lib/skylight/normalizers/active_model_serializers/render.rb +7 -3
- data/lib/skylight/normalizers/active_storage.rb +11 -13
- data/lib/skylight/normalizers/active_support/cache.rb +1 -12
- data/lib/skylight/normalizers/coach/handler_finish.rb +1 -3
- data/lib/skylight/normalizers/default.rb +1 -9
- data/lib/skylight/normalizers/faraday/request.rb +1 -3
- data/lib/skylight/normalizers/grape/endpoint.rb +13 -19
- data/lib/skylight/normalizers/grape/endpoint_run.rb +16 -18
- data/lib/skylight/normalizers/grape/endpoint_run_filters.rb +1 -3
- data/lib/skylight/normalizers/graphql/base.rb +23 -28
- data/lib/skylight/normalizers/render.rb +19 -21
- data/lib/skylight/normalizers/shrine.rb +32 -0
- data/lib/skylight/normalizers/sql.rb +4 -4
- data/lib/skylight/probes.rb +38 -46
- data/lib/skylight/probes/action_controller.rb +32 -28
- data/lib/skylight/probes/action_dispatch/request_id.rb +9 -5
- data/lib/skylight/probes/action_dispatch/routing/route_set.rb +7 -5
- data/lib/skylight/probes/action_view.rb +9 -10
- data/lib/skylight/probes/active_job_enqueue.rb +3 -9
- data/lib/skylight/probes/active_model_serializers.rb +8 -8
- data/lib/skylight/probes/delayed_job.rb +37 -42
- data/lib/skylight/probes/elasticsearch.rb +4 -6
- data/lib/skylight/probes/excon.rb +1 -1
- data/lib/skylight/probes/excon/middleware.rb +22 -23
- data/lib/skylight/probes/graphql.rb +2 -7
- data/lib/skylight/probes/middleware.rb +14 -5
- data/lib/skylight/probes/mongo.rb +83 -91
- data/lib/skylight/probes/net_http.rb +1 -1
- data/lib/skylight/probes/redis.rb +5 -17
- data/lib/skylight/probes/sequel.rb +7 -11
- data/lib/skylight/probes/sinatra.rb +8 -5
- data/lib/skylight/probes/tilt.rb +2 -4
- data/lib/skylight/railtie.rb +121 -135
- data/lib/skylight/sidekiq.rb +4 -5
- data/lib/skylight/subscriber.rb +31 -33
- data/lib/skylight/test.rb +89 -84
- data/lib/skylight/trace.rb +121 -115
- data/lib/skylight/user_config.rb +14 -17
- data/lib/skylight/util/clock.rb +1 -0
- data/lib/skylight/util/component.rb +18 -21
- data/lib/skylight/util/deploy.rb +11 -13
- data/lib/skylight/util/http.rb +104 -105
- data/lib/skylight/util/logging.rb +4 -6
- data/lib/skylight/util/lru_cache.rb +2 -6
- data/lib/skylight/util/platform.rb +2 -6
- data/lib/skylight/util/ssl.rb +1 -25
- data/lib/skylight/version.rb +1 -1
- data/lib/skylight/vm/gc.rb +1 -9
- metadata +20 -5
@@ -11,9 +11,7 @@ module Skylight
|
|
11
11
|
# @param [String] query Request query string
|
12
12
|
# @return [Hash] a hash containing `:category`, `:title`, and `:annotations`
|
13
13
|
def self.build_opts(method, _scheme, host, _port, _path, _query)
|
14
|
-
{ category: "api.http.#{method.downcase}",
|
15
|
-
title: "#{method.upcase} #{host}",
|
16
|
-
internal: true }
|
14
|
+
{ category: "api.http.#{method.downcase}", title: "#{method.upcase} #{host}", internal: true }
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
data/lib/skylight/gc.rb
CHANGED
@@ -3,10 +3,10 @@ require "skylight/util/logging"
|
|
3
3
|
module Skylight
|
4
4
|
# @api private
|
5
5
|
class GC
|
6
|
-
METHODS
|
7
|
-
TH_KEY
|
6
|
+
METHODS = %i[enable total_time].freeze
|
7
|
+
TH_KEY = :SK_GC_CURR_WINDOW
|
8
8
|
MAX_COUNT = 1000
|
9
|
-
MAX_TIME
|
9
|
+
MAX_TIME = 30_000_000
|
10
10
|
|
11
11
|
include Util::Logging
|
12
12
|
|
@@ -14,9 +14,9 @@ module Skylight
|
|
14
14
|
|
15
15
|
def initialize(config, profiler)
|
16
16
|
@listeners = []
|
17
|
-
@config
|
18
|
-
@lock
|
19
|
-
@time
|
17
|
+
@config = config
|
18
|
+
@lock = Mutex.new
|
19
|
+
@time = 0
|
20
20
|
|
21
21
|
if METHODS.all? { |m| profiler.respond_to?(m) }
|
22
22
|
@profiler = profiler
|
@@ -46,9 +46,7 @@ module Skylight
|
|
46
46
|
# Cleanup any listeners that might have leaked
|
47
47
|
@listeners.shift until @listeners[0].time < MAX_TIME
|
48
48
|
|
49
|
-
if @listeners.length > MAX_COUNT
|
50
|
-
@listeners.shift
|
51
|
-
end
|
49
|
+
@listeners.shift if @listeners.length > MAX_COUNT
|
52
50
|
end
|
53
51
|
|
54
52
|
win
|
@@ -58,52 +56,44 @@ module Skylight
|
|
58
56
|
end
|
59
57
|
|
60
58
|
def release(win)
|
61
|
-
@lock.synchronize
|
62
|
-
@listeners.delete(win)
|
63
|
-
end
|
59
|
+
@lock.synchronize { @listeners.delete(win) }
|
64
60
|
end
|
65
61
|
|
66
62
|
def update
|
67
|
-
@lock.synchronize
|
68
|
-
__update
|
69
|
-
end
|
63
|
+
@lock.synchronize { __update }
|
70
64
|
|
71
65
|
nil
|
72
66
|
end
|
73
67
|
|
74
68
|
private
|
75
69
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
70
|
+
def __update
|
71
|
+
time = @profiler.total_time
|
72
|
+
diff = time - @time
|
73
|
+
@time = time
|
80
74
|
|
81
|
-
|
82
|
-
|
83
|
-
l.add(diff)
|
84
|
-
end
|
85
|
-
end
|
86
|
-
end
|
75
|
+
@listeners.each { |l| l.add(diff) } if diff > 0
|
76
|
+
end
|
87
77
|
|
88
|
-
|
89
|
-
|
78
|
+
class Window
|
79
|
+
attr_reader :time
|
90
80
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
81
|
+
def initialize(global)
|
82
|
+
@global = global
|
83
|
+
@time = 0
|
84
|
+
end
|
95
85
|
|
96
|
-
|
97
|
-
|
98
|
-
|
86
|
+
def update
|
87
|
+
@global&.update
|
88
|
+
end
|
99
89
|
|
100
|
-
|
101
|
-
|
102
|
-
|
90
|
+
def add(time)
|
91
|
+
@time += time
|
92
|
+
end
|
103
93
|
|
104
|
-
|
105
|
-
|
106
|
-
end
|
94
|
+
def release
|
95
|
+
@global&.release(self)
|
107
96
|
end
|
97
|
+
end
|
108
98
|
end
|
109
99
|
end
|
data/lib/skylight/helpers.rb
CHANGED
@@ -14,7 +14,7 @@ module Skylight
|
|
14
14
|
|
15
15
|
if (opts = @__sk_instrument_next_method)
|
16
16
|
@__sk_instrument_next_method = nil
|
17
|
-
instrument_method(name, opts)
|
17
|
+
instrument_method(name, **opts)
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -24,7 +24,7 @@ module Skylight
|
|
24
24
|
|
25
25
|
if (opts = @__sk_instrument_next_method)
|
26
26
|
@__sk_instrument_next_method = nil
|
27
|
-
instrument_class_method(name, opts)
|
27
|
+
instrument_class_method(name, **opts)
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
@@ -77,14 +77,12 @@ module Skylight
|
|
77
77
|
# do_expensive_stuff
|
78
78
|
# end
|
79
79
|
# end
|
80
|
-
def instrument_method(*args)
|
81
|
-
opts = args.pop if args.last.is_a?(Hash)
|
82
|
-
|
80
|
+
def instrument_method(*args, **opts)
|
83
81
|
if (name = args.pop)
|
84
82
|
title = "#{self}##{name}"
|
85
|
-
__sk_instrument_method_on(self, name, title, opts
|
83
|
+
__sk_instrument_method_on(self, name, title, **opts)
|
86
84
|
else
|
87
|
-
@__sk_instrument_next_method = opts
|
85
|
+
@__sk_instrument_next_method = opts
|
88
86
|
end
|
89
87
|
end
|
90
88
|
|
@@ -123,32 +121,58 @@ module Skylight
|
|
123
121
|
#
|
124
122
|
# instrument_class_method :my_method, title: 'Expensive work'
|
125
123
|
# end
|
126
|
-
def instrument_class_method(name, opts
|
124
|
+
def instrument_class_method(name, **opts)
|
127
125
|
# NOTE: If the class is defined anonymously and then assigned to a variable this code
|
128
126
|
# will not be aware of the updated name.
|
129
127
|
title = "#{self}.#{name}"
|
130
|
-
__sk_instrument_method_on(__sk_singleton_class, name, title, opts
|
128
|
+
__sk_instrument_method_on(__sk_singleton_class, name, title, **opts)
|
131
129
|
end
|
132
130
|
|
133
131
|
private
|
134
132
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
133
|
+
HAS_ARGUMENT_FORWARDING = Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.7.0")
|
134
|
+
|
135
|
+
def __sk_instrument_method_on(klass, name, title, **opts)
|
136
|
+
category = (opts[:category] || "app.method").to_s
|
137
|
+
title = (opts[:title] || title).to_s
|
138
|
+
desc = opts[:description].to_s if opts[:description]
|
139
|
+
|
140
|
+
# NOTE: The source location logic happens before we have have a config so we can'
|
141
|
+
# check if source locations are enabled. However, it only happens once so the potential impact
|
142
|
+
# should be minimal. This would more appropriately belong to Extensions::SourceLocation,
|
143
|
+
# but as that is a runtime concern, and this happens at compile time, there isn't currently
|
144
|
+
# a clean way to turn this on and off. The absence of the extension will cause the
|
145
|
+
# source_file and source_line to be removed from the trace span before it is submitted.
|
146
|
+
source_file, source_line = klass.instance_method(name).source_location
|
147
|
+
|
148
|
+
# We should strongly prefer using the new argument-forwarding syntax (...) where available.
|
149
|
+
# In Ruby 2.7, the following are known to be syntax errors:
|
150
|
+
#
|
151
|
+
# - mixing positional arguments with argument forwarding (e.g., send(:method_name, ...))
|
152
|
+
# - calling a setter method with multiple arguments, unless dispatched via send or public_send.
|
153
|
+
#
|
154
|
+
# So it is possible, though not recommended, to define setter methods that take multiple arguments,
|
155
|
+
# keywords, and/or blocks. Unfortunately, this means that for setters, we still need to explicitly
|
156
|
+
# forward the different argument types.
|
157
|
+
is_setter_method = name.to_s.end_with?("=")
|
139
158
|
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
159
|
+
arg_string =
|
160
|
+
if HAS_ARGUMENT_FORWARDING
|
161
|
+
is_setter_method ? "*args, **kwargs, &blk" : "..."
|
162
|
+
else
|
163
|
+
"*args, &blk"
|
164
|
+
end
|
165
|
+
|
166
|
+
original_method_dispatch =
|
167
|
+
if is_setter_method
|
168
|
+
"self.send(:before_instrument_#{name}, #{arg_string})"
|
169
|
+
else
|
170
|
+
"before_instrument_#{name}(#{arg_string})"
|
171
|
+
end
|
147
172
|
|
148
|
-
|
173
|
+
klass.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
149
174
|
alias_method :"before_instrument_#{name}", :"#{name}" # alias_method :"before_instrument_process", :"process"
|
150
|
-
|
151
|
-
def #{name}(*args, &blk) # def process(*args, &blk)
|
175
|
+
def #{name}(#{arg_string}) # def process(*args, **kwargs, &blk)
|
152
176
|
span = Skylight.instrument( # span = Skylight.instrument(
|
153
177
|
category: :"#{category}", # category: :"app.method",
|
154
178
|
title: #{title.inspect}, # title: "process",
|
@@ -157,8 +181,9 @@ module Skylight
|
|
157
181
|
source_line: #{source_line.inspect}) # source_line: 123)
|
158
182
|
#
|
159
183
|
meta = {} # meta = {}
|
184
|
+
#
|
160
185
|
begin # begin
|
161
|
-
|
186
|
+
#{original_method_dispatch} # self.before_instrument_process(...)
|
162
187
|
rescue Exception => e # rescue Exception => e
|
163
188
|
meta[:exception_object] = e # meta[:exception_object] = e
|
164
189
|
raise e # raise e
|
@@ -173,15 +198,17 @@ module Skylight
|
|
173
198
|
private :"#{name}" # private :"process"
|
174
199
|
end # end
|
175
200
|
RUBY
|
176
|
-
|
201
|
+
end
|
177
202
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
203
|
+
if respond_to?(:singleton_class)
|
204
|
+
alias __sk_singleton_class singleton_class
|
205
|
+
else
|
206
|
+
def __sk_singleton_class
|
207
|
+
class << self
|
208
|
+
self
|
183
209
|
end
|
184
210
|
end
|
211
|
+
end
|
185
212
|
end
|
186
213
|
|
187
214
|
# @api private
|
@@ -99,16 +99,14 @@ module Skylight
|
|
99
99
|
end
|
100
100
|
|
101
101
|
def current_trace=(trace)
|
102
|
-
t { "setting current_trace=#{trace ? trace.uuid :
|
102
|
+
t { "setting current_trace=#{trace ? trace.uuid : "nil"}; thread=#{Thread.current.object_id}" }
|
103
103
|
@trace_info.current = trace
|
104
104
|
end
|
105
105
|
|
106
106
|
def validate_installation
|
107
107
|
# Warn if there was an error installing Skylight.
|
108
108
|
|
109
|
-
if defined?(Skylight.check_install_errors)
|
110
|
-
Skylight.check_install_errors(config)
|
111
|
-
end
|
109
|
+
Skylight.check_install_errors(config) if defined?(Skylight.check_install_errors)
|
112
110
|
|
113
111
|
if !Skylight.native? && defined?(Skylight.warn_skylight_native_missing)
|
114
112
|
Skylight.warn_skylight_native_missing(config)
|
@@ -143,9 +141,7 @@ module Skylight
|
|
143
141
|
end
|
144
142
|
|
145
143
|
def silence_warnings(context)
|
146
|
-
@warnings_silenced || @mutex.synchronize
|
147
|
-
@warnings_silenced ||= {}
|
148
|
-
end
|
144
|
+
@warnings_silenced || @mutex.synchronize { @warnings_silenced ||= {} }
|
149
145
|
|
150
146
|
@warnings_silenced[context] = true
|
151
147
|
end
|
@@ -203,8 +199,18 @@ module Skylight
|
|
203
199
|
begin
|
204
200
|
meta ||= {}
|
205
201
|
extensions.process_trace_meta(meta)
|
206
|
-
trace =
|
207
|
-
|
202
|
+
trace =
|
203
|
+
Trace.new(
|
204
|
+
self,
|
205
|
+
endpoint,
|
206
|
+
Skylight::Util::Clock.nanos,
|
207
|
+
cat,
|
208
|
+
title,
|
209
|
+
desc,
|
210
|
+
meta: meta,
|
211
|
+
segment: segment,
|
212
|
+
component: component
|
213
|
+
)
|
208
214
|
rescue Exception => e
|
209
215
|
log_error e.message
|
210
216
|
t { e.backtrace.join("\n") }
|
@@ -301,7 +307,7 @@ module Skylight
|
|
301
307
|
finalize_endpoint_segment(trace)
|
302
308
|
native_submit_trace(trace)
|
303
309
|
true
|
304
|
-
rescue => e
|
310
|
+
rescue StandardError => e
|
305
311
|
handle_instrumenter_error(trace, e)
|
306
312
|
end
|
307
313
|
end
|
@@ -332,14 +338,15 @@ module Skylight
|
|
332
338
|
def finalize_endpoint_segment(trace)
|
333
339
|
return unless (segment = trace.segment)
|
334
340
|
|
335
|
-
segment =
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
341
|
+
segment =
|
342
|
+
case trace.compound_response_error_status
|
343
|
+
when :all
|
344
|
+
"error"
|
345
|
+
when :partial
|
346
|
+
"#{segment}+error"
|
347
|
+
else
|
348
|
+
segment
|
349
|
+
end
|
343
350
|
|
344
351
|
trace.endpoint += "<sk-segment>#{segment}</sk-segment>"
|
345
352
|
end
|
data/lib/skylight/middleware.rb
CHANGED
@@ -49,12 +49,16 @@ module Skylight
|
|
49
49
|
def self.with_after_close(resp, debug_identifier: "unknown", &block)
|
50
50
|
unless resp.respond_to?(:to_ary)
|
51
51
|
if resp.respond_to?(:to_a)
|
52
|
-
Skylight.warn(
|
53
|
-
|
52
|
+
Skylight.warn(
|
53
|
+
"Rack response from \"#{debug_identifier}\" cannot be implicitly converted to an array. " \
|
54
|
+
"This is in violation of the Rack SPEC and will raise an error in future versions."
|
55
|
+
)
|
54
56
|
resp = resp.to_a
|
55
57
|
else
|
56
|
-
Skylight.error(
|
57
|
-
|
58
|
+
Skylight.error(
|
59
|
+
"Rack response from \"#{debug_identifier}\" cannot be converted to an array. This is in " \
|
60
|
+
"violation of the Rack SPEC and may cause problems with Skylight operation."
|
61
|
+
)
|
58
62
|
return resp
|
59
63
|
end
|
60
64
|
end
|
@@ -91,11 +95,7 @@ module Skylight
|
|
91
95
|
|
92
96
|
resp = @app.call(env)
|
93
97
|
|
94
|
-
|
95
|
-
Middleware.with_after_close(resp, debug_identifier: "Rack App: #{@app.class}") { trace.submit }
|
96
|
-
else
|
97
|
-
resp
|
98
|
-
end
|
98
|
+
trace ? Middleware.with_after_close(resp, debug_identifier: "Rack App: #{@app.class}") { trace.submit } : resp
|
99
99
|
rescue Exception => e
|
100
100
|
t { "middleware exception: #{e}\n#{e.backtrace.join("\n")}" }
|
101
101
|
trace&.submit
|
@@ -106,36 +106,32 @@ module Skylight
|
|
106
106
|
|
107
107
|
private
|
108
108
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
109
|
+
def log_context
|
110
|
+
# Don't cache this, it will change
|
111
|
+
{ request_id: @current_request_id, inst: Skylight.instrumenter&.uuid }
|
112
|
+
end
|
113
113
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
114
|
+
# Allow for overwriting
|
115
|
+
def endpoint_name(_env)
|
116
|
+
"Rack"
|
117
|
+
end
|
118
118
|
|
119
|
-
|
120
|
-
|
121
|
-
|
119
|
+
def endpoint_meta(_env)
|
120
|
+
{ source_location: Trace::SYNTHETIC }
|
121
|
+
end
|
122
122
|
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
123
|
+
# Request ID code based on ActionDispatch::RequestId
|
124
|
+
def set_request_id(env)
|
125
|
+
existing_request_id = env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
126
|
+
@current_request_id = env["skylight.request_id"] = make_request_id(existing_request_id)
|
127
|
+
end
|
128
128
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
else
|
133
|
-
internal_request_id
|
134
|
-
end
|
135
|
-
end
|
129
|
+
def make_request_id(request_id)
|
130
|
+
request_id && !request_id.empty? ? request_id.gsub(/[^\w\-]/, "".freeze)[0...255] : internal_request_id
|
131
|
+
end
|
136
132
|
|
137
|
-
|
138
|
-
|
139
|
-
|
133
|
+
def internal_request_id
|
134
|
+
SecureRandom.uuid
|
135
|
+
end
|
140
136
|
end
|
141
137
|
end
|
data/lib/skylight/native.rb
CHANGED
@@ -105,20 +105,18 @@ module Skylight
|
|
105
105
|
install_log = File.expand_path("../../ext/install.log", __dir__)
|
106
106
|
|
107
107
|
if File.exist?(install_log) && File.read(install_log) =~ /ERROR/
|
108
|
-
config.alert_logger.error \
|
109
|
-
|
110
|
-
|
111
|
-
"The missing extension will not affect the functioning of your application."
|
108
|
+
config.alert_logger.error "[SKYLIGHT] [#{Skylight::VERSION}] The Skylight native extension failed to install. " \
|
109
|
+
"Please check #{install_log} and notify support@skylight.io. " \
|
110
|
+
"The missing extension will not affect the functioning of your application."
|
112
111
|
end
|
113
112
|
end
|
114
113
|
|
115
114
|
# @api private
|
116
115
|
def self.warn_skylight_native_missing(config)
|
117
|
-
config.alert_logger.error \
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
"supported platform, please contact support at support@skylight.io."
|
116
|
+
config.alert_logger.error "[SKYLIGHT] [#{Skylight::VERSION}] The Skylight native extension for " \
|
117
|
+
"your platform wasn't found. Supported operating systems are " \
|
118
|
+
"Linux 2.6.18+ and Mac OS X 10.8+. The missing extension will not " \
|
119
|
+
"affect the functioning of your application. If you are on a " \
|
120
|
+
"supported platform, please contact support at support@skylight.io."
|
123
121
|
end
|
124
122
|
end
|