rails-profiler 0.37.0 → 0.38.0

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.
@@ -9,6 +9,8 @@ module Profiler
9
9
  # dashboard relies on the default: http profiles, as summaries.
10
10
  if params[:parent_token].present?
11
11
  render_children_page
12
+ elsif params[:trace_id].present?
13
+ render_trace_page
12
14
  elsif all_types?
13
15
  render_profile_page(type: profile_type_param, summary: false)
14
16
  else
@@ -57,6 +59,14 @@ module Profiler
57
59
  params[:type].to_s.match?(/\A[a-z_]{1,32}\z/) ? params[:type].to_s : nil
58
60
  end
59
61
 
62
+ # The profiles of a trace, oldest last, read through the store's trace index.
63
+ def render_trace_page
64
+ limit, offset = page_params
65
+ members = Profiler.storage.find_by_trace(params[:trace_id].to_s).reverse
66
+ members = members.select { |p| p.profile_type == "http" } unless all_types?
67
+ render_page(members.drop(offset).first(limit + 1), limit, offset)
68
+ end
69
+
60
70
  # The children of a page, oldest last, read through the store's parent index.
61
71
  def render_children_page
62
72
  limit, offset = page_params
@@ -10,8 +10,18 @@ module Profiler
10
10
  Thread.current[:profiler_token] = value
11
11
  end
12
12
 
13
+ # The trace id of the profile in progress, for the outgoing calls to carry.
14
+ def self.trace_id
15
+ Thread.current[:profiler_trace_id]
16
+ end
17
+
18
+ def self.trace_id=(value)
19
+ Thread.current[:profiler_trace_id] = value
20
+ end
21
+
13
22
  def self.clear
14
23
  Thread.current[:profiler_token] = nil
24
+ Thread.current[:profiler_trace_id] = nil
15
25
  end
16
26
  end
17
27
  end
@@ -8,12 +8,18 @@ require "securerandom"
8
8
  require_relative "../redaction"
9
9
  require_relative "../body_content"
10
10
  require_relative "../configuration"
11
+ require_relative "../current_context"
12
+ require_relative "../trace_context"
11
13
 
12
14
  module Profiler
13
15
  module Instrumentation
14
16
  module NetHttpInstrumentation
15
17
  module RequestPatch
16
18
  def request(req, body = nil, &block)
19
+ # Before the guards below: the profiler's own calls (the master's requests to its
20
+ # slaves) carry the trace too, so that the slave's profile joins the request's trace.
21
+ NetHttpInstrumentation.inject_trace_context(req, address.to_s, port)
22
+
17
23
  collector = Thread.current[:profiler_http_collector]
18
24
  return super unless collector
19
25
  # Re-entrancy guard: Net::HTTP#request calls itself recursively when
@@ -128,6 +134,20 @@ module Profiler
128
134
  @installed = true
129
135
  end
130
136
 
137
+ # Adds a traceparent to a call made while a profiled request runs on this thread: the trace
138
+ # id of that request and a span id of its own. A traceparent the application set is kept,
139
+ # and a host excluded from the instrumentation gets none.
140
+ def self.inject_trace_context(req, host, port)
141
+ trace_id = CurrentContext.trace_id
142
+ return unless trace_id
143
+ return if req[TraceContext::HEADER]
144
+ return if skip_host?(host, port)
145
+
146
+ req[TraceContext::HEADER] = TraceContext.format(trace_id, TraceContext.new_span_id)
147
+ rescue => e
148
+ Profiler.log_error("NetHttpInstrumentation: could not add the traceparent header", e)
149
+ end
150
+
131
151
  # The hosts of http_skip_hosts, and the cluster's master (host and port): a slave's calls to
132
152
  # it are the profiler's, whichever code makes them.
133
153
  def self.skip_host?(host, port = nil)
@@ -3,6 +3,7 @@
3
3
  require "rack"
4
4
  require_relative "../models/profile"
5
5
  require_relative "../current_context"
6
+ require_relative "../trace_context"
6
7
  require_relative "../collectors/lifecycle"
7
8
  require_relative "toolbar_injector"
8
9
  require_relative "capturing_body"
@@ -31,6 +32,7 @@ module Profiler
31
32
  profile = Models::Profile.new(build_request(env), params: false)
32
33
  profile.gem_version = Profiler.version
33
34
  Profiler::CurrentContext.token = profile.token
35
+ assign_trace(env, profile)
34
36
 
35
37
  # Capture request body before app processes it
36
38
  request_body = read_request_body(env)
@@ -76,6 +78,16 @@ module Profiler
76
78
 
77
79
  private
78
80
 
81
+ # Continues the trace of a valid incoming traceparent (its parent-id becomes this profile's
82
+ # parent_span_id), or starts a new one. Either way the profile gets a span of its own.
83
+ def assign_trace(env, profile)
84
+ incoming = TraceContext.parse(env["HTTP_TRACEPARENT"])
85
+ profile.trace_id = incoming ? incoming.trace_id : TraceContext.new_trace_id
86
+ profile.parent_span_id = incoming&.parent_id
87
+ profile.span_id = TraceContext.new_span_id
88
+ Profiler::CurrentContext.trace_id = profile.trace_id
89
+ end
90
+
79
91
  # Read after the application has run, so after the routing: the route parameters are there,
80
92
  # and the params carry the encoding the route declared, as the controller saw them.
81
93
  def capture_params(env, profile)
@@ -114,7 +114,9 @@ module Profiler
114
114
  # once the collectors are released.
115
115
  def enter
116
116
  token = CurrentContext.token
117
+ trace_id = CurrentContext.trace_id
117
118
  CurrentContext.token = @profile.token
119
+ CurrentContext.trace_id = @profile.trace_id
118
120
  lent = @mutex.synchronize do
119
121
  next [] if @released
120
122
 
@@ -122,13 +124,14 @@ module Profiler
122
124
  [collector, collector.lend_thread_slots] if collector.respond_to?(:lend_thread_slots)
123
125
  end
124
126
  end
125
- [token, lent]
127
+ [token, lent, trace_id]
126
128
  end
127
129
 
128
130
  def leave(state)
129
- token, lent = state
131
+ token, lent, trace_id = state
130
132
  lent.reverse_each { |collector, previous| collector.return_thread_slots(previous) }
131
133
  CurrentContext.token = token
134
+ CurrentContext.trace_id = trace_id
132
135
  end
133
136
  end
134
137
  end
@@ -21,7 +21,7 @@ module Profiler
21
21
  :request_body_size, :request_body_truncated, :request_body_size_is_minimum,
22
22
  :response_body_size, :response_body_truncated, :response_body_size_is_minimum,
23
23
  :collectors_released_after_seconds, :params_truncated,
24
- :gem_version
24
+ :gem_version, :trace_id, :span_id, :parent_span_id
25
25
 
26
26
  # +params+: false leaves the params to capture_params, for a caller that builds the profile
27
27
  # before the router has run.
@@ -167,6 +167,9 @@ module Profiler
167
167
  collectors_data: @collectors_data,
168
168
  tabs: @collectors_metadata,
169
169
  parent_token: @parent_token,
170
+ trace_id: @trace_id,
171
+ span_id: @span_id,
172
+ parent_span_id: @parent_span_id,
170
173
  is_ajax: @is_ajax
171
174
  }
172
175
  end
@@ -217,6 +220,9 @@ module Profiler
217
220
  profile.is_ajax = data[:is_ajax] || false
218
221
  profile.profile_type = data[:profile_type] || "http"
219
222
  profile.gem_version = data[:gem_version]
223
+ profile.trace_id = data[:trace_id]
224
+ profile.span_id = data[:span_id]
225
+ profile.parent_span_id = data[:parent_span_id]
220
226
 
221
227
  # Convert collectors_data keys to strings recursively for consistency
222
228
  profile.collectors_data = (data[:collectors_data] || {}).transform_keys(&:to_s).transform_values do |value|
@@ -45,6 +45,11 @@ module Profiler
45
45
  raise NotImplementedError, "#{self.class} must implement #find_by_parent"
46
46
  end
47
47
 
48
+ # The profiles sharing a trace id, oldest first.
49
+ def find_by_trace(trace_id)
50
+ raise NotImplementedError, "#{self.class} must implement #find_by_trace"
51
+ end
52
+
48
53
  def delete(token)
49
54
  raise NotImplementedError, "#{self.class} must implement #delete"
50
55
  end
@@ -41,7 +41,7 @@ module Profiler
41
41
  # Past a cap, the oldest profiles are evicted down to this share of it.
42
42
  LOW_WATER = 0.8
43
43
 
44
- Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence)
44
+ Entry = Struct.new(:token, :at, :type, :parent, :bytes, :summary, :sequence, :trace)
45
45
 
46
46
  def initialize(options = {})
47
47
  super()
@@ -138,6 +138,16 @@ module Profiler
138
138
  children.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
139
139
  end
140
140
 
141
+ def find_by_trace(trace_id)
142
+ return [] unless Token.valid?(trace_id)
143
+
144
+ members = @mutex.synchronize do
145
+ refresh
146
+ (@traces[trace_id] || {}).keys.filter_map { |token| @entries[token] }
147
+ end
148
+ members.filter_map { |entry| load(entry.token) }.sort_by(&:started_at)
149
+ end
150
+
141
151
  def delete(token)
142
152
  return unless Token.valid?(token)
143
153
 
@@ -173,13 +183,14 @@ module Profiler
173
183
  def put_record(token, data, bytes, started_at: nil)
174
184
  started_at = started_at&.to_f || (data[:started_at] ? Time.parse(data[:started_at]).to_f : Time.now.to_f)
175
185
  { "op" => "put", "token" => token, "at" => started_at, "type" => (data[:profile_type] || "http").to_s,
176
- "parent" => data[:parent_token], "bytes" => bytes, "summary" => Summary.build(data) }
186
+ "parent" => data[:parent_token], "trace" => data[:trace_id], "bytes" => bytes, "summary" => Summary.build(data) }
177
187
  end
178
188
 
179
189
  def forget_index
180
190
  @entries = {}
181
191
  @sequence = 0
182
192
  @children = {}
193
+ @traces = {}
183
194
  @bytes = 0
184
195
  @records = 0
185
196
  @offset = 0
@@ -243,8 +254,9 @@ module Profiler
243
254
  when "put"
244
255
  remove_entry(token)
245
256
  parent = Token.valid?(record["parent"]) ? record["parent"] : nil
257
+ trace = Token.valid?(record["trace"]) ? record["trace"] : nil
246
258
  add_entry(Entry.new(token, record["at"].to_f, record["type"], parent, record["bytes"].to_i,
247
- record["summary"].is_a?(Hash) ? record["summary"] : nil))
259
+ record["summary"].is_a?(Hash) ? record["summary"] : nil, nil, trace))
248
260
  when "del"
249
261
  remove_entry(token)
250
262
  end
@@ -255,6 +267,7 @@ module Profiler
255
267
  @entries[entry.token] = entry
256
268
  @bytes += entry.bytes
257
269
  (@children[entry.parent] ||= {})[entry.token] = true if entry.parent
270
+ (@traces[entry.trace] ||= {})[entry.token] = true if entry.trace
258
271
  @sorted = nil
259
272
  end
260
273
 
@@ -267,6 +280,10 @@ module Profiler
267
280
  siblings.delete(token)
268
281
  @children.delete(entry.parent) if siblings.empty?
269
282
  end
283
+ if entry.trace && (members = @traces[entry.trace])
284
+ members.delete(token)
285
+ @traces.delete(entry.trace) if members.empty?
286
+ end
270
287
  @sorted = nil
271
288
  end
272
289
 
@@ -344,7 +361,8 @@ module Profiler
344
361
  def entry_from_file(token)
345
362
  json = PrivateFiles.open_for_reading(profile_file_path(token), &:read)
346
363
  record = put_record(token, Models::Profile.from_json(json).to_h, json.bytesize)
347
- Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"])
364
+ trace = Token.valid?(record["trace"]) ? record["trace"] : nil
365
+ Entry.new(token, record["at"], record["type"], record["parent"], record["bytes"], record["summary"], nil, trace)
348
366
  rescue StandardError => e
349
367
  # Unreadable: kept in the count, so that it is evicted in turn; listed by no one.
350
368
  Profiler.log_error_once(:file_store_entry, "FileStore: could not read profile #{token} to index it", e)
@@ -353,7 +371,7 @@ module Profiler
353
371
  rescue SystemCallError
354
372
  nil
355
373
  end
356
- Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil)
374
+ Entry.new(token, stat ? stat.mtime.to_f : 0.0, nil, nil, stat ? stat.size : 0, nil, nil, nil)
357
375
  end
358
376
 
359
377
  def modified_at(token)
@@ -390,7 +408,8 @@ module Profiler
390
408
  "compacted_at" => Time.now.to_f)]
391
409
  @entries.each_value do |entry|
392
410
  lines << JSON.generate("op" => "put", "token" => entry.token, "at" => entry.at, "type" => entry.type,
393
- "parent" => entry.parent, "bytes" => entry.bytes, "summary" => entry.summary)
411
+ "parent" => entry.parent, "trace" => entry.trace, "bytes" => entry.bytes,
412
+ "summary" => entry.summary)
394
413
  end
395
414
  data = "#{lines.join("\n")}\n"
396
415
  PrivateFiles.write(@index_path, data)
@@ -12,7 +12,7 @@ module Profiler
12
12
  # cap). Beside them, the start time, type and parent of each, and the children of each parent,
13
13
  # so that listing, finding the children and evicting deserialize only what they return.
14
14
  class MemoryStore < BaseStore
15
- Entry = Struct.new(:at, :type, :parent, :sequence)
15
+ Entry = Struct.new(:at, :type, :parent, :sequence, :trace)
16
16
 
17
17
  def initialize(options = {})
18
18
  @profiles = Concurrent::Hash.new
@@ -20,6 +20,7 @@ module Profiler
20
20
  @mutex = Mutex.new
21
21
  @entries = {}
22
22
  @children = {}
23
+ @traces = {}
23
24
  @sequence = 0
24
25
  @sorted = nil
25
26
  end
@@ -30,8 +31,9 @@ module Profiler
30
31
  cleanup_if_needed unless @profiles.key?(token)
31
32
  remove_entry(token)
32
33
  @profiles[token] = data
33
- @entries[token] = Entry.new(profile.started_at.to_f, data[:profile_type].to_s, profile.parent_token, @sequence += 1)
34
+ @entries[token] = Entry.new(profile.started_at.to_f, data[:profile_type].to_s, profile.parent_token, @sequence += 1, profile.trace_id)
34
35
  (@children[profile.parent_token] ||= {})[token] = true if profile.parent_token
36
+ (@traces[profile.trace_id] ||= {})[token] = true if profile.trace_id
35
37
  @sorted = nil
36
38
  end
37
39
  token
@@ -70,6 +72,13 @@ module Profiler
70
72
  children.map { |data| deserialize_profile(data) }.sort_by(&:started_at)
71
73
  end
72
74
 
75
+ def find_by_trace(trace_id)
76
+ return [] unless Token.valid?(trace_id)
77
+
78
+ members = @mutex.synchronize { (@traces[trace_id] || {}).keys.filter_map { |token| @profiles[token] } }
79
+ members.map { |data| deserialize_profile(data) }.sort_by(&:started_at)
80
+ end
81
+
73
82
  def delete(token)
74
83
  return unless Token.valid?(token)
75
84
 
@@ -109,6 +118,10 @@ module Profiler
109
118
  siblings.delete(token)
110
119
  @children.delete(entry.parent) if siblings.empty?
111
120
  end
121
+ if entry.trace && (members = @traces[entry.trace])
122
+ members.delete(token)
123
+ @traces.delete(entry.trace) if members.empty?
124
+ end
112
125
  @sorted = nil
113
126
  end
114
127
 
@@ -102,6 +102,16 @@ module Profiler
102
102
  .sort_by(&:started_at)
103
103
  end
104
104
 
105
+ def find_by_trace(trace_id)
106
+ return [] unless Token.valid?(trace_id)
107
+
108
+ ensure_index
109
+ @redis.smembers(traces_key(trace_id))
110
+ .filter_map { |token| load(token) if Token.valid?(token) }
111
+ .select { |profile| profile.trace_id == trace_id }
112
+ .sort_by(&:started_at)
113
+ end
114
+
105
115
  def delete(token)
106
116
  return unless Token.valid?(token)
107
117
 
@@ -172,6 +182,11 @@ module Profiler
172
182
  client.zadd(saved_key, saved_at, token)
173
183
  client.sadd(types_key, type)
174
184
  [list_key, type_list_key(type), saved_key, types_key].each { |key| client.expire(key, @ttl) }
185
+ trace = summary[:trace_id] || summary["trace_id"]
186
+ if Token.valid?(trace)
187
+ client.sadd(traces_key(trace), token)
188
+ client.expire(traces_key(trace), @ttl)
189
+ end
175
190
  return unless Token.valid?(parent)
176
191
 
177
192
  client.sadd(children_key(parent), token)
@@ -185,6 +200,8 @@ module Profiler
185
200
  @redis.zrem(type_list_key(summary["profile_type"] || "http"), token)
186
201
  parent = summary["parent_token"]
187
202
  @redis.srem(children_key(parent), token) if Token.valid?(parent)
203
+ trace = summary["trace_id"]
204
+ @redis.srem(traces_key(trace), token) if Token.valid?(trace)
188
205
  end
189
206
 
190
207
  def remove(token)
@@ -205,6 +222,8 @@ module Profiler
205
222
  pipe.zrem(type_list_key(summary["profile_type"] || "http"), token)
206
223
  parent = summary["parent_token"]
207
224
  pipe.srem(children_key(parent), token) if Token.valid?(parent)
225
+ trace = summary["trace_id"]
226
+ pipe.srem(traces_key(trace), token) if Token.valid?(trace)
208
227
  else
209
228
  types.each { |type| pipe.zrem(type_list_key(type), token) }
210
229
  end
@@ -349,6 +368,10 @@ module Profiler
349
368
  "#{@key_prefix}:children:#{parent_token}"
350
369
  end
351
370
 
371
+ def traces_key(trace_id)
372
+ "#{@key_prefix}:traces:#{trace_id}"
373
+ end
374
+
352
375
  def index_version_key
353
376
  "#{@key_prefix}:index_version"
354
377
  end
@@ -66,11 +66,11 @@ module Profiler
66
66
  <<~SQL,
67
67
  INSERT OR REPLACE INTO profiler_profiles (
68
68
  token, profile_type, gem_version, path, method, status, duration, memory,
69
- started_at, finished_at, parent_token, is_ajax,
69
+ started_at, finished_at, parent_token, trace_id, span_id, parent_span_id, is_ajax,
70
70
  tabs, params, headers, response_headers, collectors_meta, summary
71
71
  ) VALUES (
72
72
  :token, :profile_type, :gem_version, :path, :method, :status, :duration, :memory,
73
- :started_at, :finished_at, :parent_token, :is_ajax,
73
+ :started_at, :finished_at, :parent_token, :trace_id, :span_id, :parent_span_id, :is_ajax,
74
74
  :tabs, :params, :headers, :response_headers, :collectors_meta, :summary
75
75
  )
76
76
  SQL
@@ -85,6 +85,9 @@ module Profiler
85
85
  started_at: utc_iso8601(data[:started_at]),
86
86
  finished_at: utc_iso8601(data[:finished_at]),
87
87
  parent_token: data[:parent_token],
88
+ trace_id: data[:trace_id],
89
+ span_id: data[:span_id],
90
+ parent_span_id: data[:parent_span_id],
88
91
  is_ajax: data[:is_ajax] ? 1 : 0,
89
92
  tabs: JSON.generate(data[:tabs] || []),
90
93
  params: JSON.generate(data[:params] || {}),
@@ -136,6 +139,16 @@ module Profiler
136
139
  rows.map { |row| row_to_profile(row) }.compact
137
140
  end
138
141
 
142
+ def find_by_trace(trace_id)
143
+ return [] unless Token.valid?(trace_id)
144
+
145
+ rows = @db.execute(
146
+ "SELECT * FROM profiler_profiles WHERE trace_id = :trace_id ORDER BY started_at ASC, rowid ASC",
147
+ trace_id: trace_id
148
+ )
149
+ rows.map { |row| row_to_profile(row) }.compact
150
+ end
151
+
139
152
  def delete(token)
140
153
  return unless Token.valid?(token)
141
154
 
@@ -276,12 +289,13 @@ module Profiler
276
289
  ON profiler_profiles(profile_type);
277
290
  SQL
278
291
 
279
- %w[gem_version summary].each do |column|
292
+ %w[gem_version summary trace_id span_id parent_span_id].each do |column|
280
293
  @db.execute("ALTER TABLE profiler_profiles ADD COLUMN #{column} TEXT")
281
294
  rescue SQLite3::Exception
282
295
  # column already exists
283
296
  end
284
297
 
298
+ @db.execute("CREATE INDEX IF NOT EXISTS idx_profiler_trace_id ON profiler_profiles(trace_id)")
285
299
  migrate_times_to_utc
286
300
  end
287
301
 
@@ -331,6 +345,9 @@ module Profiler
331
345
  started_at: local_iso8601(row["started_at"]),
332
346
  finished_at: local_iso8601(row["finished_at"]),
333
347
  parent_token: row["parent_token"],
348
+ trace_id: row["trace_id"],
349
+ span_id: row["span_id"],
350
+ parent_span_id: row["parent_span_id"],
334
351
  is_ajax: row["is_ajax"] == 1,
335
352
  tabs: parse_json(row["tabs"], []),
336
353
  params: parse_json(row["params"], {}),
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Profiler
6
+ # The W3C Trace Context `traceparent` header, version 00:
7
+ # `00-{trace-id:32 hex}-{parent-id:16 hex}-{trace-flags:2 hex}`.
8
+ module TraceContext
9
+ HEADER = "traceparent"
10
+ # Version ff is forbidden by the specification; a later version is read by its first four
11
+ # fields, as the specification asks of a parser that does not know it.
12
+ PATTERN = /\A(?<version>\h{2})-(?<trace_id>\h{32})-(?<parent_id>\h{16})-(?<flags>\h{2})(?:-.*)?\z/
13
+ ZERO_TRACE_ID = ("0" * 32).freeze
14
+ ZERO_SPAN_ID = ("0" * 16).freeze
15
+
16
+ Parsed = Struct.new(:trace_id, :parent_id, :flags)
17
+
18
+ module_function
19
+
20
+ # The trace-id, parent-id and flags of a valid header value, nil for anything else.
21
+ def parse(value)
22
+ match = PATTERN.match(value.to_s.strip)
23
+ return nil unless match
24
+
25
+ version = match[:version].downcase
26
+ trace_id = match[:trace_id].downcase
27
+ parent_id = match[:parent_id].downcase
28
+ return nil if version == "ff"
29
+ # Version 00 has exactly four fields; the all-zero ids are invalid.
30
+ return nil if version == "00" && value.to_s.strip.count("-") != 3
31
+ return nil if trace_id == ZERO_TRACE_ID || parent_id == ZERO_SPAN_ID
32
+
33
+ Parsed.new(trace_id, parent_id, match[:flags].downcase)
34
+ end
35
+
36
+ def new_trace_id
37
+ SecureRandom.hex(16)
38
+ end
39
+
40
+ def new_span_id
41
+ SecureRandom.hex(8)
42
+ end
43
+
44
+ def format(trace_id, span_id, flags = "01")
45
+ "00-#{trace_id}-#{span_id}-#{flags}"
46
+ end
47
+ end
48
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Profiler
4
- VERSION = "0.37.0"
4
+ VERSION = "0.38.0"
5
5
 
6
6
  GEM_ROOT = File.expand_path("../..", __dir__)
7
7
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.37.0
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Duplessy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-18 00:00:00.000000000 Z
11
+ date: 2026-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -260,6 +260,7 @@ files:
260
260
  - lib/profiler/test_runner/discovery.rb
261
261
  - lib/profiler/test_runner/run_store.rb
262
262
  - lib/profiler/test_runner/runner.rb
263
+ - lib/profiler/trace_context.rb
263
264
  - lib/profiler/version.rb
264
265
  homepage: https://github.com/Pepsit36/rails-profiler
265
266
  licenses: