salopulse 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c06e78cb26ed1b47d866b3cf8a0d403373cf9c8beb76ca91292b76505a56753
4
- data.tar.gz: 71295dd725c6063cfd8bb72ea668ac8160e9e92878fcd46997eed64a8245a1b7
3
+ metadata.gz: 54e35ff25bd4f509773e4121f6f18612741353a90fdcfb2f8d17444b48019ebc
4
+ data.tar.gz: a0b1d4b69b96151d2de4a2662c6bde1ebfb4160ccc23320b9d5c6e9e89685147
5
5
  SHA512:
6
- metadata.gz: 4181cb25abebd7be379e294e136a0c33e9c07e8bcaac9d01b1abc7fb14ed5b9927ad1e6adb645e7cb3233ab578b307ba248a8b0dcf135a796c06b470b20d5429
7
- data.tar.gz: 732c617c949e3f8f7897f229ed0ad5fb82aa7d99828afb6d5b2891e8bee591f86330c5899b3dc14a3e6ff4606d343226ff95e2ad05a37b5c420bc8b9bfc5d4a9
6
+ metadata.gz: a22fbc00d2411e2b88a6af24fbc854ef13be0d4f7e10f1ba2b50d082861ece0582e86ed783aff4cfb01b3e86f560fb7090e1a83a56829d1634248831bdc6be91
7
+ data.tar.gz: 0fd9d36419e61fe1b0633cdc49685cc2599fde166f852a5eba0435fb5fe6e27c8f06de1a22f1bc788b1cfdf7c87a7b0cf9b66e3afda3184025f3bdf600d6a0d1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
4
 
5
+ ## [0.4.0] - 2026-06-10
6
+
7
+ ### Added
8
+ - `StackFrameBuilder` parses exception backtraces into structured frames and
9
+ attaches `pre_context`, `context_line`, and `post_context` source lines for
10
+ in-app frames. `capture_exception` now sends a `stack_frames` array
11
+ alongside the existing `stack_trace` string, enabling source-snippet
12
+ rendering in the dashboard
13
+ - `Configuration#app_root` controls which paths are treated as in-app; defaults
14
+ to `Rails.root` when Rails is loaded and `Dir.pwd` otherwise
15
+
5
16
  ## [0.3.0] - 2026-06-08
6
17
 
7
18
  ### Added
@@ -9,6 +9,7 @@ require_relative "flusher"
9
9
  require_relative "sanitizer"
10
10
  require_relative "local_fingerprint"
11
11
  require_relative "request_context"
12
+ require_relative "stack_frame_builder"
12
13
 
13
14
  module Salopulse
14
15
  class Client
@@ -92,10 +93,12 @@ module Salopulse
92
93
  return unless sample?
93
94
 
94
95
  ctx = RequestContext.current
96
+ backtrace = Array(error.backtrace)
95
97
  data = {
96
98
  "error_class" => error.class.name,
97
99
  "message" => error.message.to_s,
98
- "stack_trace" => Array(error.backtrace).join("\n"),
100
+ "stack_trace" => backtrace.join("\n"),
101
+ "stack_frames" => StackFrameBuilder.call(backtrace, app_root: configuration&.app_root),
99
102
  "endpoint" => ctx&.dig(:endpoint),
100
103
  "http_method" => ctx&.dig(:http_method)
101
104
  }
@@ -4,7 +4,8 @@ module Salopulse
4
4
  class Configuration
5
5
  attr_accessor :dsn, :release, :environment, :sample_rate,
6
6
  :flush_interval, :flush_batch_size, :n1_threshold,
7
- :before_send, :logger, :enabled, :max_buffer_size
7
+ :before_send, :logger, :enabled, :max_buffer_size,
8
+ :app_root
8
9
 
9
10
  def initialize
10
11
  @release = nil
@@ -17,6 +18,16 @@ module Salopulse
17
18
  @logger = Logger.new($stdout, level: Logger::WARN)
18
19
  @enabled = true
19
20
  @max_buffer_size = 10_000
21
+ @app_root = detect_app_root
22
+ end
23
+
24
+ private
25
+
26
+ def detect_app_root
27
+ return Rails.root.to_s if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
28
+ Dir.pwd
29
+ rescue StandardError
30
+ Dir.pwd
20
31
  end
21
32
  end
22
33
  end
@@ -0,0 +1,97 @@
1
+ module Salopulse
2
+ class StackFrameBuilder
3
+ BACKTRACE_LINE = /\A(?<file>.+?):(?<line>\d+):in\s+[`'](?<method>.+?)['`]\z/
4
+ CONTEXT_LINES = 5
5
+ MAX_FRAMES = 50
6
+ FRAMEWORK_MARKERS = [ "/gems/", "/rubygems/", "/bundle/" ].freeze
7
+
8
+ SOURCE_CACHE = {}
9
+ SOURCE_CACHE_MUTEX = Mutex.new
10
+ SOURCE_CACHE_LIMIT = 512
11
+
12
+ def self.call(backtrace, app_root: nil)
13
+ new(backtrace, app_root: app_root).call
14
+ end
15
+
16
+ def initialize(backtrace, app_root:)
17
+ @backtrace = Array(backtrace)
18
+ @app_root = app_root && File.expand_path(app_root.to_s)
19
+ end
20
+
21
+ def call
22
+ @backtrace.first(MAX_FRAMES).filter_map { |line| build_frame(line.to_s) }
23
+ end
24
+
25
+ private
26
+
27
+ def build_frame(raw)
28
+ match = BACKTRACE_LINE.match(raw)
29
+ return nil unless match
30
+
31
+ abs_path = match[:file]
32
+ lineno = match[:line].to_i
33
+ in_app = in_app?(abs_path)
34
+
35
+ frame = {
36
+ "file" => relative_file(abs_path),
37
+ "abs_path" => abs_path,
38
+ "line" => lineno,
39
+ "method" => match[:method],
40
+ "in_app" => in_app,
41
+ "package" => detect_package(abs_path)
42
+ }
43
+
44
+ attach_source_context(frame, abs_path, lineno) if in_app
45
+ frame
46
+ end
47
+
48
+ def in_app?(file)
49
+ return false if FRAMEWORK_MARKERS.any? { |marker| file.include?(marker) }
50
+ return true unless @app_root
51
+
52
+ File.expand_path(file).start_with?(@app_root)
53
+ end
54
+
55
+ def detect_package(file)
56
+ match = file.match(%r{/gems/(?<package>[^/]+)/})
57
+ match && match[:package]
58
+ end
59
+
60
+ def relative_file(file)
61
+ return file unless @app_root
62
+
63
+ expanded = File.expand_path(file)
64
+ return file unless expanded.start_with?(@app_root)
65
+
66
+ expanded.sub(/\A#{Regexp.escape(@app_root)}\/?/, "")
67
+ end
68
+
69
+ def attach_source_context(frame, file, lineno)
70
+ lines = read_source(file)
71
+ return if lines.empty?
72
+
73
+ idx = lineno - 1
74
+ return if idx.negative? || idx >= lines.length
75
+
76
+ pre_start = [ idx - CONTEXT_LINES, 0 ].max
77
+ post_end = [ idx + CONTEXT_LINES, lines.length - 1 ].min
78
+
79
+ frame["pre_context"] = lines[pre_start...idx].map { |l| l.to_s.chomp }
80
+ frame["context_line"] = lines[idx].to_s.chomp
81
+ frame["post_context"] = lines[(idx + 1)..post_end].to_a.map { |l| l.to_s.chomp }
82
+ end
83
+
84
+ def read_source(file)
85
+ SOURCE_CACHE_MUTEX.synchronize do
86
+ return SOURCE_CACHE[file] if SOURCE_CACHE.key?(file)
87
+
88
+ SOURCE_CACHE.shift if SOURCE_CACHE.size >= SOURCE_CACHE_LIMIT
89
+ SOURCE_CACHE[file] = begin
90
+ File.readlines(file)
91
+ rescue StandardError
92
+ []
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,3 +1,3 @@
1
1
  module Salopulse
2
- VERSION = "0.3.0".freeze
2
+ VERSION = "0.4.0".freeze
3
3
  end
data/lib/salopulse.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "salopulse/buffer"
7
7
  require_relative "salopulse/sanitizer"
8
8
  require_relative "salopulse/local_fingerprint"
9
9
  require_relative "salopulse/request_context"
10
+ require_relative "salopulse/stack_frame_builder"
10
11
  require_relative "salopulse/transport"
11
12
  require_relative "salopulse/flusher"
12
13
  require_relative "salopulse/client"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: salopulse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Salih İmran Büker
@@ -94,6 +94,7 @@ files:
94
94
  - lib/salopulse/railtie.rb
95
95
  - lib/salopulse/request_context.rb
96
96
  - lib/salopulse/sanitizer.rb
97
+ - lib/salopulse/stack_frame_builder.rb
97
98
  - lib/salopulse/transport.rb
98
99
  - lib/salopulse/version.rb
99
100
  homepage: https://github.com/mersieS/salopulse-ruby-sdk