app_profiler 0.1.0 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e7279a1707f2425058eb5db7cb7e4fca673f1a3634ff76542d2703ce5fdfefc
4
- data.tar.gz: 8002f14694a26a43c87725ac54c44161401dc116b35dd793dcdf9b3f8f2e85c7
3
+ metadata.gz: 340bc83668cbe8abd27780ef23456a6d98f1f8c46059f2b447a25f1e6784483e
4
+ data.tar.gz: 06fcbd4abe95e53f8e37595df7ca445bcd59f61cd9ba24af8c82578cdb6bb9c4
5
5
  SHA512:
6
- metadata.gz: ed3f2daae5d8a9729c2aee42179927e6f311fd0b1ea39f8934ad6811be19075debebb985660984e8b15ce93f4ab7c8812fde0e3fd63f9b972341147606a9fb23
7
- data.tar.gz: 336ac8b880934ef7078347ba1b6a1df046ee58147f91d387ef581b2071a3bcb2acbc50c8cd78e4c465585407bf74babb8b5147ce461936e71ca1993c6682d06b
6
+ metadata.gz: 04cf486ef95efd05af19fd241f05b45c0a942c1e701295181fdef898f59cb24ace85c86e047ea63359b5c5ddfb2f8a233afbc3958004a823e0ffcf0daf692830
7
+ data.tar.gz: bab5ddd1f1dcfb4d06e23b575341a7d7a783e04191b62a74dd553cf78d5726187500931e34c49ebe5e019d60602ceec074d4071d024008c05c8bfe5e5ddb7d52
@@ -21,26 +21,18 @@ module AppProfiler
21
21
  def append_headers(response, upload:, autoredirect:)
22
22
  return unless upload
23
23
 
24
- response[1][profile_header] = profile_url(upload)
24
+ response[1][profile_header] = AppProfiler.profile_url(upload)
25
25
  response[1][profile_data_header] = profile_data_url(upload)
26
26
 
27
27
  return unless autoredirect
28
28
 
29
29
  # Automatically redirect to profile if autoredirect is true.
30
30
  if response[0].to_i < 500
31
- response[1]["Location"] = profile_url(upload)
31
+ response[1]["Location"] = AppProfiler.profile_url(upload)
32
32
  response[0] = 303
33
33
  end
34
34
  end
35
35
 
36
- def profile_url(upload)
37
- if AppProfiler.profile_url_formatter.nil?
38
- "#{AppProfiler.speedscope_host}#profileURL=#{upload.url}"
39
- else
40
- AppProfiler.profile_url_formatter.call(upload)
41
- end
42
- end
43
-
44
36
  def profile_data_url(upload)
45
37
  upload.url.to_s
46
38
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module AppProfiler
4
4
  class Profile
5
- INTERNAL_METADATA_KEYS = %i(id context)
5
+ INTERNAL_METADATA_KEYS = [:id, :context]
6
6
  private_constant :INTERNAL_METADATA_KEYS
7
7
  class UnsafeFilename < StandardError; end
8
8
 
@@ -40,7 +40,13 @@ module AppProfiler
40
40
  def upload
41
41
  AppProfiler.storage.upload(self).tap do |upload|
42
42
  if upload && defined?(upload.url)
43
- AppProfiler.logger.info("[Profiler] uploaded profile profile_url=#{upload.url}")
43
+ AppProfiler.logger.info(
44
+ <<~INFO.squish
45
+ [Profiler] data uploaded:
46
+ profile_url=#{upload.url}
47
+ profile_viewer_url=#{AppProfiler.profile_url(upload)}
48
+ INFO
49
+ )
44
50
  end
45
51
  end
46
52
  rescue => error
@@ -71,7 +77,7 @@ module AppProfiler
71
77
  Socket.gethostname,
72
78
  ].compact.join("-") << ".json"
73
79
 
74
- raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match(filename)
80
+ raise UnsafeFilename if /[^0-9A-Za-z.\-\_]/.match?(filename)
75
81
 
76
82
  AppProfiler.profile_root.join(filename)
77
83
  end
@@ -16,6 +16,12 @@ module AppProfiler
16
16
  AppProfiler.middleware = app.config.app_profiler.middleware || Middleware
17
17
  AppProfiler.middleware.action = app.config.app_profiler.middleware_action || default_middleware_action
18
18
  AppProfiler.middleware.disabled = app.config.app_profiler.middleware_disabled || false
19
+ AppProfiler.server.enabled = app.config.app_profiler.server_enabled || false
20
+ AppProfiler.server.transport = app.config.app_profiler.server_transport || default_appprofiler_transport
21
+ AppProfiler.server.port = app.config.app_profiler.server_port || 0
22
+ AppProfiler.server.duration = app.config.app_profiler.server_duration || 30
23
+ AppProfiler.server.cors = app.config.app_profiler.server_cors || true
24
+ AppProfiler.server.cors_host = app.config.app_profiler.server_cors_host || "*"
19
25
  AppProfiler.autoredirect = app.config.app_profiler.autoredirect || false
20
26
  AppProfiler.speedscope_host = app.config.app_profiler.speedscope_host || ENV.fetch(
21
27
  "APP_PROFILER_SPEEDSCOPE_URL", "https://speedscope.app"
@@ -37,6 +43,15 @@ module AppProfiler
37
43
  end
38
44
  end
39
45
 
46
+ initializer "app_profiler.enable_server" do
47
+ if AppProfiler.server.enabled
48
+ AppProfiler::Server.start
49
+ ActiveSupport::ForkTracker.after_fork do
50
+ AppProfiler::Server.start
51
+ end
52
+ end
53
+ end
54
+
40
55
  private
41
56
 
42
57
  def default_middleware_action
@@ -46,5 +61,15 @@ module AppProfiler
46
61
  Middleware::UploadAction
47
62
  end
48
63
  end
64
+
65
+ def default_appprofiler_transport
66
+ if Rails.env.development?
67
+ # default to TCP server in development so that if wanted users are able to target
68
+ # the server with speedscope
69
+ AppProfiler::Server::TRANSPORT_TCP
70
+ else
71
+ AppProfiler::Server::TRANSPORT_UNIX
72
+ end
73
+ end
49
74
  end
50
75
  end
@@ -81,6 +81,7 @@ module AppProfiler
81
81
 
82
82
  def header(name)
83
83
  return unless @request.has_header?(name)
84
+
84
85
  @request.get_header(name)
85
86
  end
86
87
 
@@ -0,0 +1,307 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+ require "rack"
5
+ require "tempfile"
6
+
7
+ # This module provides a means to start a golang-inspired profile server
8
+ # it is implemented using stdlib and Rack to avoid additional dependencies
9
+
10
+ module AppProfiler
11
+ module Server
12
+ HTTP_OK = 200
13
+ HTTP_BAD_REQUEST = 400
14
+ HTTP_NOT_FOUND = 404
15
+ HTTP_NOT_ALLOWED = 405
16
+ HTTP_CONFLICT = 409
17
+
18
+ TRANSPORT_UNIX = "unix"
19
+ TRANSPORT_TCP = "tcp"
20
+
21
+ DEFAULTS = {
22
+ enabled: false,
23
+ transport: TRANSPORT_UNIX,
24
+ cors: true,
25
+ cors_host: "*",
26
+ port: 0,
27
+ duration: 30,
28
+ }
29
+
30
+ mattr_accessor :enabled, default: DEFAULTS[:enabled]
31
+ mattr_accessor :transport, default: DEFAULTS[:transport]
32
+ mattr_accessor :cors, default: DEFAULTS[:cors]
33
+ mattr_accessor :cors_host, default: DEFAULTS[:cors_host]
34
+ mattr_accessor :port, default: DEFAULTS[:port]
35
+ mattr_accessor :duration, default: DEFAULTS[:duration]
36
+
37
+ class ProfileApplication
38
+ class InvalidProfileArgsError < StandardError; end
39
+
40
+ def initialize
41
+ @semaphore = Thread::Mutex.new
42
+ @profile_running = false
43
+ end
44
+
45
+ def call(env)
46
+ handle(Rack::Request.new(env)).finish
47
+ end
48
+
49
+ private
50
+
51
+ def handle(request)
52
+ response = Rack::Response.new
53
+ if request.request_method != "GET"
54
+ response.status = HTTP_NOT_ALLOWED
55
+ response.write("Only GET requests are supported")
56
+ return response
57
+ end
58
+ case request.path
59
+ when "/profile"
60
+ begin
61
+ stackprof_args, duration = validate_profile_params(request.params)
62
+ rescue InvalidProfileArgsError => e
63
+ response.status = HTTP_BAD_REQUEST
64
+ response.write("Invalid argument #{e.message}")
65
+ return response
66
+ end
67
+
68
+ if start_running
69
+ start_time = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
70
+ AppProfiler.start(**stackprof_args)
71
+ sleep(duration)
72
+ profile = AppProfiler.stop
73
+ stop_running
74
+ response.status = HTTP_OK
75
+ response.set_header("Content-Type", "application/json")
76
+ if AppProfiler::Server.cors
77
+ response.set_header("Access-Control-Allow-Origin", AppProfiler::Server.cors_host)
78
+ end
79
+ profile_hash = profile.to_h
80
+ profile_hash["start_time_nsecs"] = start_time # NOTE: this is not part of the stackprof profile spec
81
+ response.write(JSON.dump(profile_hash))
82
+ else
83
+ response.status = HTTP_CONFLICT
84
+ response.write("A profile is already running")
85
+ end
86
+ else
87
+ response.status = HTTP_NOT_FOUND
88
+ response.write("Unsupported endpoint #{request.path}")
89
+ end
90
+ response
91
+ end
92
+
93
+ def validate_profile_params(params)
94
+ params = params.symbolize_keys
95
+ stackprof_args = {}
96
+ begin
97
+ duration = Float(params.key?(:duration) ? params[:duration] : AppProfiler::Server.duration)
98
+ rescue ArgumentError
99
+ raise InvalidProfileArgsError, "invalid duration #{params[:duration]}"
100
+ end
101
+ if params.key?(:mode)
102
+ if ["cpu", "wall", "object"].include?(params[:mode])
103
+ stackprof_args[:mode] = params[:mode].to_sym
104
+ else
105
+ raise InvalidProfileArgsError, "invalid mode #{params[:mode]}"
106
+ end
107
+ end
108
+ if params.key?(:interval)
109
+ stackprof_args[:interval] = params[:interval].to_i
110
+ raise InvalidProfileArgsError, "invalid interval #{params[:interval]}" if stackprof_args[:interval] <= 0
111
+ end
112
+ [stackprof_args, duration]
113
+ end
114
+
115
+ # Prevent multiple concurrent profiles by synchronizing between threads
116
+ def start_running
117
+ @semaphore.synchronize do
118
+ return false if @profile_running
119
+
120
+ @profile_running = true
121
+ end
122
+ end
123
+
124
+ def stop_running
125
+ @semaphore.synchronize { @profile_running = false }
126
+ end
127
+ end
128
+
129
+ # This is a minimal, non-compliant "HTTP" server.
130
+ # It will create an extremely minimal rack environment hash and hand it off
131
+ # to our application to process
132
+ class ProfileServer
133
+ PROFILER_TEMPFILE_PATH = "/tmp/app_profiler" # for tempfile that indicates port in filename or unix sockets
134
+
135
+ class Transport
136
+ attr_reader :socket
137
+
138
+ def client
139
+ raise(NotImplementedError)
140
+ end
141
+
142
+ def stop
143
+ raise(NotImplementedError)
144
+ end
145
+ end
146
+
147
+ class UNIX < Transport
148
+ def initialize
149
+ super
150
+ FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
151
+ @socket_file = File.join(PROFILER_TEMPFILE_PATH, "app-profiler-#{Process.pid}.sock")
152
+ File.unlink(@socket_file) if File.exist?(@socket_file) && File.socket?(@socket_file)
153
+ @socket = UNIXServer.new(@socket_file)
154
+ end
155
+
156
+ def client
157
+ UNIXSocket.new(@socket_file)
158
+ end
159
+
160
+ def stop
161
+ File.unlink(@socket_file) if File.exist?(@socket_file) && File.socket?(@socket_file)
162
+ @socket.close
163
+ end
164
+ end
165
+
166
+ class TCP < Transport
167
+ SERVER_ADDRESS = "127.0.0.1" # it is ONLY safe to run this bound to localhost
168
+
169
+ def initialize(port = 0)
170
+ super()
171
+ FileUtils.mkdir_p(PROFILER_TEMPFILE_PATH)
172
+ @socket = TCPServer.new(SERVER_ADDRESS, port)
173
+ @port = @socket.addr[1]
174
+ @port_file = Tempfile.new("profileserver-#{Process.pid}-port-#{@port}-", PROFILER_TEMPFILE_PATH)
175
+ end
176
+
177
+ def client
178
+ TCPSocket.new(SERVER_ADDRESS, @port)
179
+ end
180
+
181
+ def stop
182
+ @port_file.unlink
183
+ @socket.close
184
+ end
185
+ end
186
+
187
+ def initialize(transport)
188
+ case transport
189
+ when TRANSPORT_UNIX
190
+ @transport = ProfileServer::UNIX.new
191
+ when TRANSPORT_TCP
192
+ @transport = ProfileServer::TCP.new(AppProfiler::Server.port)
193
+ else
194
+ raise "invalid transport #{transport}"
195
+ end
196
+
197
+ @listen_thread = nil
198
+
199
+ AppProfiler.logger.info(
200
+ "[AppProfiler::Server] listening on addr=#{@transport.socket.addr}"
201
+ )
202
+ @pid = Process.pid
203
+ at_exit { stop }
204
+ end
205
+
206
+ def client
207
+ @transport.client
208
+ end
209
+
210
+ def serve
211
+ return unless @listen_thread.nil?
212
+
213
+ app = ProfileApplication.new
214
+
215
+ @listen_thread = Thread.new do
216
+ loop do
217
+ Thread.new(@transport.socket.accept) do |session|
218
+ request = session.gets
219
+ if request.nil?
220
+ session.close
221
+ next
222
+ end
223
+ method, path, http_version = request.split(" ")
224
+ path_info, query_string = path.split("?")
225
+ env = { # an extremely minimal rack env hash, just enough to get the job done
226
+ "HTTP_VERSION" => http_version,
227
+ "REQUEST_METHOD" => method,
228
+ "PATH_INFO" => path_info,
229
+ "QUERY_STRING" => query_string,
230
+ "rack.input" => "",
231
+ }
232
+ status, headers, body = app.call(env)
233
+
234
+ begin
235
+ session.print("#{http_version} #{status}\r\n")
236
+ headers.each do |header, value|
237
+ session.print("#{header}: #{value}\r\n")
238
+ end
239
+ session.print("\r\n")
240
+ body.each do |part|
241
+ session.print(part)
242
+ end
243
+ rescue => e
244
+ AppProfiler.logger.error(
245
+ "[AppProfiler::Server] exception #{e} responding to request #{request}: #{e.message}"
246
+ )
247
+ ensure
248
+ session.close
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+
255
+ def stop
256
+ return unless @pid == Process.pid
257
+
258
+ @listen_thread.kill
259
+ @transport.stop
260
+ end
261
+ end
262
+
263
+ private_constant :ProfileApplication, :ProfileServer
264
+
265
+ class << self
266
+ def reset
267
+ profile_servers.clear
268
+ DEFAULTS.each do |config, value|
269
+ class_variable_set(:"@@#{config}", value) # rubocop:disable Style/ClassVars
270
+ end
271
+ end
272
+
273
+ def start
274
+ return if profile_server
275
+
276
+ profile_servers[Process.pid] = ProfileServer.new(AppProfiler::Server.transport)
277
+ profile_server.serve
278
+ profile_server
279
+ end
280
+
281
+ def client
282
+ return unless profile_server
283
+
284
+ profile_server.client
285
+ end
286
+
287
+ def stop
288
+ return unless profile_server
289
+
290
+ profile_server.stop
291
+ profile_servers.delete(Process.pid)
292
+ end
293
+
294
+ private
295
+
296
+ def profile_server
297
+ profile_servers[Process.pid]
298
+ end
299
+
300
+ # It is possible there will be a server pre-fork, this is to distinguish
301
+ # the child server from its parent via PID
302
+ def profile_servers
303
+ @profile_servers ||= {}
304
+ end
305
+ end
306
+ end
307
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppProfiler
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -7,10 +7,12 @@ module AppProfiler
7
7
  class SpeedscopeRemoteViewer < BaseViewer
8
8
  class BaseMiddleware
9
9
  class Sanitizer < Rails::Html::SafeListSanitizer
10
- self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
11
- sup dfn cite big small address hr br div span
12
- h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
13
- acronym a img blockquote del ins script))
10
+ self.allowed_tags = Set.new([
11
+ "strong", "em", "b", "i", "p", "code", "pre", "tt", "samp", "kbd", "var", "sub",
12
+ "sup", "dfn", "cite", "big", "small", "address", "hr", "br", "div", "span", "h1",
13
+ "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "dl", "dt", "dd", "abbr", "acronym",
14
+ "a", "img", "blockquote", "del", "ins", "script",
15
+ ])
14
16
  end
15
17
 
16
18
  private_constant(:Sanitizer)
@@ -73,7 +75,7 @@ module AppProfiler
73
75
 
74
76
  def index(_env)
75
77
  render(
76
- String.new.tap do |content|
78
+ (+"").tap do |content|
77
79
  content << "<h1>Profiles</h1>"
78
80
  profile_files.each do |file|
79
81
  content << <<~HTML
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module AppProfiler
3
4
  module Yarn
4
5
  module Command
@@ -32,7 +33,7 @@ module AppProfiler
32
33
 
33
34
  def ensure_command_valid(command)
34
35
  unless valid_command?(command)
35
- raise YarnError, "Illegal command: #{command.join(' ')}."
36
+ raise YarnError, "Illegal command: #{command.join(" ")}."
36
37
  end
37
38
  end
38
39
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module AppProfiler
3
4
  module Yarn
4
5
  module WithSpeedscope
data/lib/app_profiler.rb CHANGED
@@ -10,6 +10,10 @@ module AppProfiler
10
10
  class ConfigurationError < StandardError
11
11
  end
12
12
 
13
+ DefaultProfileFormatter = proc do |upload|
14
+ "#{AppProfiler.speedscope_host}#profileURL=#{upload.url}"
15
+ end
16
+
13
17
  module Storage
14
18
  autoload :BaseStorage, "app_profiler/storage/base_storage"
15
19
  autoload :FileStorage, "app_profiler/storage/file_storage"
@@ -26,6 +30,7 @@ module AppProfiler
26
30
  require "app_profiler/request_parameters"
27
31
  require "app_profiler/profiler"
28
32
  require "app_profiler/profile"
33
+ require "app_profiler/server"
29
34
 
30
35
  mattr_accessor :logger, default: Logger.new($stdout)
31
36
  mattr_accessor :root
@@ -35,11 +40,13 @@ module AppProfiler
35
40
  mattr_accessor :autoredirect, default: false
36
41
  mattr_reader :profile_header, default: "X-Profile"
37
42
  mattr_accessor :context, default: nil
38
- mattr_reader :profile_url_formatter, default: nil
43
+ mattr_reader :profile_url_formatter,
44
+ default: DefaultProfileFormatter
39
45
 
40
46
  mattr_accessor :storage, default: Storage::FileStorage
41
47
  mattr_accessor :viewer, default: Viewer::SpeedscopeViewer
42
48
  mattr_accessor :middleware, default: Middleware
49
+ mattr_accessor :server, default: Server
43
50
 
44
51
  class << self
45
52
  def run(*args, &block)
@@ -62,9 +69,7 @@ module AppProfiler
62
69
  end
63
70
 
64
71
  def request_profile_header
65
- @@request_profile_header ||= begin # rubocop:disable Style/ClassVars
66
- profile_header.upcase.gsub("-", "_").prepend("HTTP_")
67
- end
72
+ @@request_profile_header ||= profile_header.upcase.tr("-", "_").prepend("HTTP_") # rubocop:disable Style/ClassVars
68
73
  end
69
74
 
70
75
  def profile_data_header
@@ -74,5 +79,9 @@ module AppProfiler
74
79
  def profile_url_formatter=(block)
75
80
  @@profile_url_formatter = block # rubocop:disable Style/ClassVars
76
81
  end
82
+
83
+ def profile_url(upload)
84
+ AppProfiler.profile_url_formatter.call(upload)
85
+ end
77
86
  end
78
87
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: app_profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gannon McGibbon
@@ -13,7 +13,7 @@ authors:
13
13
  autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
- date: 2022-04-26 00:00:00.000000000 Z
16
+ date: 2022-06-16 00:00:00.000000000 Z
17
17
  dependencies:
18
18
  - !ruby/object:Gem::Dependency
19
19
  name: activesupport
@@ -30,33 +30,33 @@ dependencies:
30
30
  - !ruby/object:Gem::Version
31
31
  version: '5.2'
32
32
  - !ruby/object:Gem::Dependency
33
- name: railties
33
+ name: rack
34
34
  requirement: !ruby/object:Gem::Requirement
35
35
  requirements:
36
36
  - - ">="
37
37
  - !ruby/object:Gem::Version
38
- version: '5.2'
38
+ version: '0'
39
39
  type: :runtime
40
40
  prerelease: false
41
41
  version_requirements: !ruby/object:Gem::Requirement
42
42
  requirements:
43
43
  - - ">="
44
44
  - !ruby/object:Gem::Version
45
- version: '5.2'
45
+ version: '0'
46
46
  - !ruby/object:Gem::Dependency
47
- name: rack
47
+ name: railties
48
48
  requirement: !ruby/object:Gem::Requirement
49
49
  requirements:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
- version: '0'
52
+ version: '5.2'
53
53
  type: :runtime
54
54
  prerelease: false
55
55
  version_requirements: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: '0'
59
+ version: '5.2'
60
60
  - !ruby/object:Gem::Dependency
61
61
  name: stackprof
62
62
  requirement: !ruby/object:Gem::Requirement
@@ -86,7 +86,7 @@ dependencies:
86
86
  - !ruby/object:Gem::Version
87
87
  version: '0'
88
88
  - !ruby/object:Gem::Dependency
89
- name: rake
89
+ name: minitest
90
90
  requirement: !ruby/object:Gem::Requirement
91
91
  requirements:
92
92
  - - ">="
@@ -100,19 +100,19 @@ dependencies:
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  - !ruby/object:Gem::Dependency
103
- name: minitest
103
+ name: minitest-stub-const
104
104
  requirement: !ruby/object:Gem::Requirement
105
105
  requirements:
106
- - - ">="
106
+ - - '='
107
107
  - !ruby/object:Gem::Version
108
- version: '0'
108
+ version: '0.6'
109
109
  type: :development
110
110
  prerelease: false
111
111
  version_requirements: !ruby/object:Gem::Requirement
112
112
  requirements:
113
- - - ">="
113
+ - - '='
114
114
  - !ruby/object:Gem::Version
115
- version: '0'
115
+ version: '0.6'
116
116
  - !ruby/object:Gem::Dependency
117
117
  name: mocha
118
118
  requirement: !ruby/object:Gem::Requirement
@@ -128,19 +128,19 @@ dependencies:
128
128
  - !ruby/object:Gem::Version
129
129
  version: '0'
130
130
  - !ruby/object:Gem::Dependency
131
- name: minitest-stub-const
131
+ name: rake
132
132
  requirement: !ruby/object:Gem::Requirement
133
133
  requirements:
134
- - - '='
134
+ - - ">="
135
135
  - !ruby/object:Gem::Version
136
- version: '0.6'
136
+ version: '0'
137
137
  type: :development
138
138
  prerelease: false
139
139
  version_requirements: !ruby/object:Gem::Requirement
140
140
  requirements:
141
- - - '='
141
+ - - ">="
142
142
  - !ruby/object:Gem::Version
143
- version: '0.6'
143
+ version: '0'
144
144
  description:
145
145
  email:
146
146
  - gems@shopify.com
@@ -157,6 +157,7 @@ files:
157
157
  - lib/app_profiler/profiler.rb
158
158
  - lib/app_profiler/railtie.rb
159
159
  - lib/app_profiler/request_parameters.rb
160
+ - lib/app_profiler/server.rb
160
161
  - lib/app_profiler/storage/base_storage.rb
161
162
  - lib/app_profiler/storage/file_storage.rb
162
163
  - lib/app_profiler/storage/google_cloud_storage.rb
@@ -180,14 +181,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
181
  requirements:
181
182
  - - ">="
182
183
  - !ruby/object:Gem::Version
183
- version: '0'
184
+ version: '2.7'
184
185
  required_rubygems_version: !ruby/object:Gem::Requirement
185
186
  requirements:
186
187
  - - ">="
187
188
  - !ruby/object:Gem::Version
188
189
  version: '0'
189
190
  requirements: []
190
- rubygems_version: 3.2.20
191
+ rubygems_version: 3.3.3
191
192
  signing_key:
192
193
  specification_version: 4
193
194
  summary: Collect performance profiles for your Rails application.