Module: OpenTrace::SourceContext

Defined in:
lib/opentrace/source_context.rb

Constant Summary collapse

CONTEXT_LINES =

lines before and after the error line

3
MAX_CACHE_SIZE =
50
MAX_FILE_SIZE =

100KB — skip generated/minified files

100_000

Class Method Summary collapse

Class Method Details

.clear_cache!Object



93
94
95
# File 'lib/opentrace/source_context.rb', line 93

def clear_cache!
  @mutex.synchronize { @cache.clear }
end

.extract(backtrace_line) ⇒ Object

Extract source code context around a backtrace line. Returns nil if the file can’t be read.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/opentrace/source_context.rb', line 16

def extract(backtrace_line)
  match = backtrace_line&.match(/\A(.+):(\d+)/)
  return nil unless match

  file = match[1]
  line_no = match[2].to_i
  return nil if line_no <= 0

  full_path = resolve_path(file)
  return nil unless full_path && File.exist?(full_path)
  return nil unless safe_path?(full_path)

  lines = read_file_lines(full_path)
  return nil unless lines

  start_line = [line_no - CONTEXT_LINES, 1].max
  end_line = [line_no + CONTEXT_LINES, lines.size].min

  context = {}
  (start_line..end_line).each do |n|
    context[n] = lines[n - 1]&.rstrip&.slice(0, 200)
  end

  {
    file: file,
    line: line_no,
    context: context
  }
rescue StandardError
  nil
end

.read_file_lines(path) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/opentrace/source_context.rb', line 77

def read_file_lines(path)
  @mutex.synchronize do
    return @cache[path] if @cache.key?(path)

    if @cache.size >= MAX_CACHE_SIZE
      @cache.delete(@cache.keys.first)
    end

    lines = File.readlines(path)
    @cache[path] = lines
    lines
  end
rescue StandardError
  nil
end

.resolve_path(file) ⇒ Object



48
49
50
51
52
53
54
55
56
57
58
# File 'lib/opentrace/source_context.rb', line 48

def resolve_path(file)
  if file.start_with?("/")
    file
  elsif defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
    File.join(::Rails.root.to_s, file)
  else
    File.expand_path(file)
  end
rescue StandardError
  nil
end

.safe_path?(path) ⇒ Boolean

Returns:

  • (Boolean)


60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/opentrace/source_context.rb', line 60

def safe_path?(path)
  return false unless path.include?("/app/") || path.include?("/lib/") || path.include?("/config/")

  # Resolve symlinks and '..' to prevent path traversal
  real = File.realpath(path)

  # Verify the resolved path is under the application root
  if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
    root = ::Rails.root.to_s
    return false unless real.start_with?("#{root}/")
  end

  File.size(real) <= MAX_FILE_SIZE
rescue StandardError
  false
end