rails_error_dashboard 0.1.29 → 0.1.30
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/README.md +34 -6
- data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
- data/app/models/rails_error_dashboard/application.rb +1 -1
- data/app/models/rails_error_dashboard/error_log.rb +44 -16
- data/app/views/layouts/rails_error_dashboard.html.erb +66 -1237
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
- data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
- data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
- data/app/views/rails_error_dashboard/errors/show.html.erb +44 -20
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
- data/db/migrate/20251226020100_create_error_comments.rb +3 -0
- data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
- data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
- data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
- data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
- data/lib/rails_error_dashboard/configuration.rb +160 -3
- data/lib/rails_error_dashboard/configuration_error.rb +24 -0
- data/lib/rails_error_dashboard/engine.rb +17 -0
- data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
- data/lib/rails_error_dashboard/queries/errors_list.rb +20 -8
- data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
- data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
- data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +6 -0
- metadata +13 -10
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
- data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
- data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
6
|
+
module RailsErrorDashboard
|
|
7
|
+
module Services
|
|
8
|
+
# Reads git blame information for specific file lines
|
|
9
|
+
# Executes git blame command and parses porcelain output
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# reader = GitBlameReader.new("/path/to/file.rb", 42)
|
|
13
|
+
# blame = reader.read_blame
|
|
14
|
+
# # => { author: "John Doe", email: "john@example.com", date: Time, sha: "abc123", line: "code" }
|
|
15
|
+
class GitBlameReader
|
|
16
|
+
COMMAND_TIMEOUT = 5 # seconds
|
|
17
|
+
PORCELAIN_FIELDS = %w[
|
|
18
|
+
author
|
|
19
|
+
author-mail
|
|
20
|
+
author-time
|
|
21
|
+
author-tz
|
|
22
|
+
committer
|
|
23
|
+
committer-mail
|
|
24
|
+
committer-time
|
|
25
|
+
committer-tz
|
|
26
|
+
summary
|
|
27
|
+
filename
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :file_path, :line_number, :error
|
|
31
|
+
|
|
32
|
+
# Initialize a new git blame reader
|
|
33
|
+
#
|
|
34
|
+
# @param file_path [String] Path to the source file
|
|
35
|
+
# @param line_number [Integer] Target line number
|
|
36
|
+
def initialize(file_path, line_number)
|
|
37
|
+
@file_path = file_path
|
|
38
|
+
@line_number = line_number.to_i
|
|
39
|
+
@error = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Read git blame information for the target line
|
|
43
|
+
#
|
|
44
|
+
# @return [Hash, nil] Blame data hash or nil if unavailable
|
|
45
|
+
def read_blame
|
|
46
|
+
unless git_available?
|
|
47
|
+
@error = "Git not available"
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
unless File.exist?(file_path)
|
|
52
|
+
@error = "File not found"
|
|
53
|
+
return nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Execute git blame command
|
|
57
|
+
output = execute_git_blame
|
|
58
|
+
return nil unless output
|
|
59
|
+
|
|
60
|
+
# Parse porcelain format output
|
|
61
|
+
parse_blame_output(output)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
@error = "Error reading git blame: #{e.message}"
|
|
64
|
+
RailsErrorDashboard::Logger.error("GitBlameReader error for #{file_path}:#{line_number} - #{e.message}")
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Check if git is available on the system
|
|
69
|
+
#
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def git_available?
|
|
72
|
+
@git_available ||= begin
|
|
73
|
+
_stdout, _stderr, status = Open3.capture3("git", "--version")
|
|
74
|
+
status.success?
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Execute git blame command for the specific line
|
|
83
|
+
#
|
|
84
|
+
# @return [String, nil] Command output or nil if failed
|
|
85
|
+
def execute_git_blame
|
|
86
|
+
# Build command array (prevents command injection)
|
|
87
|
+
cmd = [
|
|
88
|
+
"git",
|
|
89
|
+
"blame",
|
|
90
|
+
"-L", "#{line_number},#{line_number}",
|
|
91
|
+
"--porcelain",
|
|
92
|
+
"--", # Separator
|
|
93
|
+
file_path
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
# Execute with timeout
|
|
97
|
+
stdout, stderr, status = Timeout.timeout(COMMAND_TIMEOUT) do
|
|
98
|
+
Open3.capture3(*cmd, chdir: Rails.root)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
unless status.success?
|
|
102
|
+
@error = "Git blame failed: #{stderr}"
|
|
103
|
+
RailsErrorDashboard::Logger.debug("Git blame failed for #{file_path}:#{line_number} - #{stderr}")
|
|
104
|
+
return nil
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
stdout
|
|
108
|
+
rescue Timeout::Error
|
|
109
|
+
@error = "Git blame timeout"
|
|
110
|
+
RailsErrorDashboard::Logger.warn("Git blame timeout for #{file_path}:#{line_number}")
|
|
111
|
+
nil
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
@error = "Git blame execution error: #{e.message}"
|
|
114
|
+
RailsErrorDashboard::Logger.error("Git blame execution error for #{file_path}:#{line_number} - #{e.message}")
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Parse git blame porcelain format output
|
|
119
|
+
#
|
|
120
|
+
# Git blame --porcelain format:
|
|
121
|
+
# <sha> <line_number> <final_line_number> <num_lines>
|
|
122
|
+
# author <author_name>
|
|
123
|
+
# author-mail <<author_email>>
|
|
124
|
+
# author-time <unix_timestamp>
|
|
125
|
+
# author-tz <timezone>
|
|
126
|
+
# committer <committer_name>
|
|
127
|
+
# committer-mail <<committer_email>>
|
|
128
|
+
# committer-time <unix_timestamp>
|
|
129
|
+
# committer-tz <timezone>
|
|
130
|
+
# summary <commit_message_summary>
|
|
131
|
+
# filename <filename>
|
|
132
|
+
# <TAB><line_content>
|
|
133
|
+
#
|
|
134
|
+
# @param output [String] Git blame porcelain output
|
|
135
|
+
# @return [Hash, nil] Parsed blame data
|
|
136
|
+
def parse_blame_output(output)
|
|
137
|
+
return nil if output.blank?
|
|
138
|
+
|
|
139
|
+
lines = output.split("\n")
|
|
140
|
+
return nil if lines.empty?
|
|
141
|
+
|
|
142
|
+
# First line contains commit SHA and line info
|
|
143
|
+
first_line = lines[0]
|
|
144
|
+
match = first_line.match(/^([0-9a-f]+)\s+(\d+)\s+(\d+)/)
|
|
145
|
+
unless match
|
|
146
|
+
@error = "Incomplete git blame data"
|
|
147
|
+
return nil
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
sha = match[1]
|
|
151
|
+
data = { sha: sha }
|
|
152
|
+
|
|
153
|
+
# Parse subsequent lines
|
|
154
|
+
lines[1..].each do |line|
|
|
155
|
+
# Check for field: value lines
|
|
156
|
+
PORCELAIN_FIELDS.each do |field|
|
|
157
|
+
if line.start_with?("#{field} ")
|
|
158
|
+
value = line.sub("#{field} ", "")
|
|
159
|
+
|
|
160
|
+
case field
|
|
161
|
+
when "author"
|
|
162
|
+
data[:author] = value
|
|
163
|
+
when "author-mail"
|
|
164
|
+
# Remove < and > brackets
|
|
165
|
+
data[:email] = value.gsub(/[<>]/, "")
|
|
166
|
+
when "author-time"
|
|
167
|
+
# Convert Unix timestamp to Time object
|
|
168
|
+
data[:date] = Time.at(value.to_i)
|
|
169
|
+
when "summary"
|
|
170
|
+
data[:commit_message] = value
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Line content starts with tab
|
|
176
|
+
if line.start_with?("\t")
|
|
177
|
+
data[:line] = line.sub("\t", "")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Validate required fields
|
|
182
|
+
if data[:author].present? && data[:sha].present?
|
|
183
|
+
data
|
|
184
|
+
else
|
|
185
|
+
@error = "Incomplete git blame data"
|
|
186
|
+
nil
|
|
187
|
+
end
|
|
188
|
+
rescue StandardError => e
|
|
189
|
+
@error = "Error parsing git blame output: #{e.message}"
|
|
190
|
+
RailsErrorDashboard::Logger.error("Git blame parsing error: #{e.message}")
|
|
191
|
+
nil
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Services
|
|
5
|
+
# Generates links to source code on GitHub, GitLab, or Bitbucket
|
|
6
|
+
# Supports commit SHA, branch, and tag references
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# generator = GithubLinkGenerator.new(
|
|
10
|
+
# repository_url: "https://github.com/user/repo",
|
|
11
|
+
# file_path: "app/models/user.rb",
|
|
12
|
+
# line_number: 42,
|
|
13
|
+
# commit_sha: "abc123def456"
|
|
14
|
+
# )
|
|
15
|
+
# generator.generate_link
|
|
16
|
+
# # => "https://github.com/user/repo/blob/abc123def456/app/models/user.rb#L42"
|
|
17
|
+
class GithubLinkGenerator
|
|
18
|
+
attr_reader :repository_url, :file_path, :line_number, :commit_sha, :branch, :error
|
|
19
|
+
|
|
20
|
+
# Initialize a new link generator
|
|
21
|
+
#
|
|
22
|
+
# @param repository_url [String] Base repository URL (GitHub, GitLab, Bitbucket)
|
|
23
|
+
# @param file_path [String] Relative path to file from repository root
|
|
24
|
+
# @param line_number [Integer] Line number to link to
|
|
25
|
+
# @param commit_sha [String, nil] Git commit SHA (recommended for accuracy)
|
|
26
|
+
# @param branch [String, nil] Branch name (fallback if no commit SHA)
|
|
27
|
+
def initialize(repository_url:, file_path:, line_number:, commit_sha: nil, branch: nil)
|
|
28
|
+
@repository_url = repository_url
|
|
29
|
+
@file_path = file_path
|
|
30
|
+
@line_number = line_number.to_i
|
|
31
|
+
@commit_sha = commit_sha
|
|
32
|
+
@branch = branch || "main"
|
|
33
|
+
@error = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Generate a link to the source file on the repository host
|
|
37
|
+
#
|
|
38
|
+
# @return [String, nil] URL to the source file or nil if invalid
|
|
39
|
+
def generate_link
|
|
40
|
+
return nil if repository_url.blank? || file_path.blank?
|
|
41
|
+
|
|
42
|
+
# Normalize repository URL
|
|
43
|
+
normalized_repo = normalize_repository_url
|
|
44
|
+
|
|
45
|
+
# Determine reference (commit SHA or branch)
|
|
46
|
+
reference = determine_reference
|
|
47
|
+
|
|
48
|
+
# Generate link based on repository type
|
|
49
|
+
case detect_repository_type
|
|
50
|
+
when :github
|
|
51
|
+
generate_github_link(normalized_repo, reference)
|
|
52
|
+
when :gitlab
|
|
53
|
+
generate_gitlab_link(normalized_repo, reference)
|
|
54
|
+
when :bitbucket
|
|
55
|
+
generate_bitbucket_link(normalized_repo, reference)
|
|
56
|
+
else
|
|
57
|
+
@error = "Unsupported repository type"
|
|
58
|
+
nil
|
|
59
|
+
end
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
@error = "Error generating link: #{e.message}"
|
|
62
|
+
RailsErrorDashboard::Logger.error("GithubLinkGenerator error for #{repository_url} - #{e.message}")
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# Normalize repository URL (remove .git suffix, trailing slashes, etc.)
|
|
69
|
+
#
|
|
70
|
+
# @return [String]
|
|
71
|
+
def normalize_repository_url
|
|
72
|
+
url = repository_url.strip
|
|
73
|
+
url = url.chomp(".git")
|
|
74
|
+
url = url.chomp("/")
|
|
75
|
+
url
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Detect repository type from URL
|
|
79
|
+
#
|
|
80
|
+
# @return [Symbol] :github, :gitlab, :bitbucket, or :unknown
|
|
81
|
+
def detect_repository_type
|
|
82
|
+
normalized = normalize_repository_url.downcase
|
|
83
|
+
|
|
84
|
+
return :github if normalized.include?("github.com")
|
|
85
|
+
return :gitlab if normalized.include?("gitlab.com") || normalized.include?("gitlab.")
|
|
86
|
+
return :bitbucket if normalized.include?("bitbucket.org") || normalized.include?("bitbucket.")
|
|
87
|
+
|
|
88
|
+
:unknown
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Determine which reference to use (commit SHA or branch)
|
|
92
|
+
#
|
|
93
|
+
# @return [String]
|
|
94
|
+
def determine_reference
|
|
95
|
+
if commit_sha.present?
|
|
96
|
+
commit_sha
|
|
97
|
+
else
|
|
98
|
+
branch
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Normalize file path (remove leading slashes, Rails.root prefix, etc.)
|
|
103
|
+
#
|
|
104
|
+
# @return [String]
|
|
105
|
+
def normalize_file_path
|
|
106
|
+
path = file_path.strip
|
|
107
|
+
|
|
108
|
+
# Remove leading slash
|
|
109
|
+
path = path.sub(%r{^/}, "")
|
|
110
|
+
|
|
111
|
+
# Remove Rails.root or app root prefix if present
|
|
112
|
+
# Handles paths like "/Users/foo/myapp/app/models/user.rb" -> "app/models/user.rb"
|
|
113
|
+
# Match pattern: look for one of the standard Rails directories
|
|
114
|
+
match = path.match(%r{.*/?((?:app|lib|config|db|spec|test)/.*)$})
|
|
115
|
+
if match
|
|
116
|
+
path = match[1]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Generate GitHub link
|
|
123
|
+
#
|
|
124
|
+
# Format: https://github.com/user/repo/blob/{ref}/path/to/file.rb#L42
|
|
125
|
+
#
|
|
126
|
+
# @param repo_url [String] Normalized repository URL
|
|
127
|
+
# @param ref [String] Commit SHA or branch name
|
|
128
|
+
# @return [String]
|
|
129
|
+
def generate_github_link(repo_url, ref)
|
|
130
|
+
normalized_path = normalize_file_path
|
|
131
|
+
"#{repo_url}/blob/#{ref}/#{normalized_path}#L#{line_number}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Generate GitLab link
|
|
135
|
+
#
|
|
136
|
+
# Format: https://gitlab.com/user/repo/-/blob/{ref}/path/to/file.rb#L42
|
|
137
|
+
#
|
|
138
|
+
# @param repo_url [String] Normalized repository URL
|
|
139
|
+
# @param ref [String] Commit SHA or branch name
|
|
140
|
+
# @return [String]
|
|
141
|
+
def generate_gitlab_link(repo_url, ref)
|
|
142
|
+
normalized_path = normalize_file_path
|
|
143
|
+
"#{repo_url}/-/blob/#{ref}/#{normalized_path}#L#{line_number}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Generate Bitbucket link
|
|
147
|
+
#
|
|
148
|
+
# Format: https://bitbucket.org/user/repo/src/{ref}/path/to/file.rb#lines-42
|
|
149
|
+
#
|
|
150
|
+
# @param repo_url [String] Normalized repository URL
|
|
151
|
+
# @param ref [String] Commit SHA or branch name
|
|
152
|
+
# @return [String]
|
|
153
|
+
def generate_bitbucket_link(repo_url, ref)
|
|
154
|
+
normalized_path = normalize_file_path
|
|
155
|
+
"#{repo_url}/src/#{ref}/#{normalized_path}#lines-#{line_number}"
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Services
|
|
5
|
+
# Reads source code files from disk with security validation
|
|
6
|
+
# and context lines around a target line number
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# reader = SourceCodeReader.new("/path/to/app/models/user.rb", 42)
|
|
10
|
+
# lines = reader.read_lines(context: 5)
|
|
11
|
+
# # Returns array of hashes with line numbers and content
|
|
12
|
+
class SourceCodeReader
|
|
13
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
14
|
+
MAX_CONTEXT_LINES = 50
|
|
15
|
+
|
|
16
|
+
attr_reader :file_path, :line_number, :error
|
|
17
|
+
|
|
18
|
+
# Initialize a new source code reader
|
|
19
|
+
#
|
|
20
|
+
# @param file_path [String] Path to the source file
|
|
21
|
+
# @param line_number [Integer] Target line number
|
|
22
|
+
def initialize(file_path, line_number)
|
|
23
|
+
@file_path = file_path
|
|
24
|
+
@line_number = line_number.to_i
|
|
25
|
+
@error = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read source code lines with context around the target line
|
|
29
|
+
#
|
|
30
|
+
# @param context [Integer] Number of lines before and after target line
|
|
31
|
+
# @return [Array<Hash>, nil] Array of line data hashes or nil if unavailable
|
|
32
|
+
def read_lines(context: 5)
|
|
33
|
+
context = [ [ context, MAX_CONTEXT_LINES ].min, 1 ].max
|
|
34
|
+
|
|
35
|
+
# Validate and resolve path
|
|
36
|
+
absolute_path = resolve_absolute_path
|
|
37
|
+
return nil unless absolute_path
|
|
38
|
+
|
|
39
|
+
# Validate path is safe
|
|
40
|
+
unless validate_path!(absolute_path)
|
|
41
|
+
@error = "Invalid or unsafe file path"
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check file exists and is readable
|
|
46
|
+
unless File.exist?(absolute_path) && File.readable?(absolute_path)
|
|
47
|
+
@error = "File not found or not readable"
|
|
48
|
+
return nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Check file size
|
|
52
|
+
file_size = File.size(absolute_path)
|
|
53
|
+
if file_size > MAX_FILE_SIZE
|
|
54
|
+
@error = "File too large (#{file_size} bytes, max #{MAX_FILE_SIZE})"
|
|
55
|
+
return nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check if file is binary
|
|
59
|
+
if binary_file?(absolute_path)
|
|
60
|
+
@error = "Binary file cannot be displayed"
|
|
61
|
+
return nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Read the specific lines
|
|
65
|
+
read_specific_lines(absolute_path, line_number - context, line_number + context)
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
@error = "Error reading file: #{e.message}"
|
|
68
|
+
RailsErrorDashboard::Logger.error("SourceCodeReader error for #{file_path}:#{line_number} - #{e.message}")
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Check if file exists on disk
|
|
73
|
+
#
|
|
74
|
+
# @return [Boolean]
|
|
75
|
+
def file_exists?
|
|
76
|
+
absolute_path = resolve_absolute_path
|
|
77
|
+
return false unless absolute_path
|
|
78
|
+
|
|
79
|
+
File.exist?(absolute_path) && File.readable?(absolute_path)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
false
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
# Resolve file path to absolute path within Rails.root
|
|
87
|
+
#
|
|
88
|
+
# @return [String, nil] Absolute path or nil if invalid
|
|
89
|
+
def resolve_absolute_path
|
|
90
|
+
return nil if file_path.blank?
|
|
91
|
+
|
|
92
|
+
# Handle relative paths from backtrace
|
|
93
|
+
if file_path.start_with?(Rails.root.to_s)
|
|
94
|
+
# Already absolute
|
|
95
|
+
file_path
|
|
96
|
+
elsif file_path.start_with?("/")
|
|
97
|
+
# Absolute but might be from different root (deployed location)
|
|
98
|
+
# Try to find relative to Rails.root
|
|
99
|
+
relative = file_path.sub(%r{^.*/}, "")
|
|
100
|
+
File.join(Rails.root, relative)
|
|
101
|
+
else
|
|
102
|
+
# Relative path
|
|
103
|
+
File.join(Rails.root, file_path)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Validate path is safe to read (security check)
|
|
108
|
+
#
|
|
109
|
+
# @param path [String] Absolute path to validate
|
|
110
|
+
# @return [Boolean]
|
|
111
|
+
# @raise [SecurityError] if path is unsafe
|
|
112
|
+
def validate_path!(path)
|
|
113
|
+
# Normalize path
|
|
114
|
+
normalized = File.expand_path(path)
|
|
115
|
+
rails_root = File.expand_path(Rails.root)
|
|
116
|
+
|
|
117
|
+
# Must be within Rails.root
|
|
118
|
+
unless normalized.start_with?(rails_root)
|
|
119
|
+
RailsErrorDashboard::Logger.warn("Path outside Rails.root: #{normalized}")
|
|
120
|
+
return false
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Prevent directory traversal
|
|
124
|
+
if path.include?("..") || normalized.include?("..")
|
|
125
|
+
RailsErrorDashboard::Logger.warn("Directory traversal attempt: #{path}")
|
|
126
|
+
return false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Check against sensitive file patterns
|
|
130
|
+
sensitive_patterns = [
|
|
131
|
+
/\.env/i,
|
|
132
|
+
/secrets\.yml$/i,
|
|
133
|
+
/credentials\.yml/i,
|
|
134
|
+
/database\.yml$/i,
|
|
135
|
+
/master\.key$/i,
|
|
136
|
+
/private_key/i,
|
|
137
|
+
/\.pem$/i,
|
|
138
|
+
/\.key$/i
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
if sensitive_patterns.any? { |pattern| normalized.match?(pattern) }
|
|
142
|
+
RailsErrorDashboard::Logger.warn("Attempt to read sensitive file: #{normalized}")
|
|
143
|
+
return false
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Only show app code if configured
|
|
147
|
+
if RailsErrorDashboard.configuration.only_show_app_code_source
|
|
148
|
+
# Block gem/vendor code
|
|
149
|
+
blocked_patterns = [
|
|
150
|
+
%r{/gems/},
|
|
151
|
+
%r{/vendor/bundle/},
|
|
152
|
+
%r{/vendor/ruby/},
|
|
153
|
+
%r{/.bundle/}
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
if blocked_patterns.any? { |pattern| normalized.match?(pattern) }
|
|
157
|
+
RailsErrorDashboard::Logger.debug("Skipping gem/vendor code: #{normalized}")
|
|
158
|
+
return false
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Read specific lines from file
|
|
166
|
+
#
|
|
167
|
+
# @param file_path [String] Path to file
|
|
168
|
+
# @param start_line [Integer] First line to read (1-indexed)
|
|
169
|
+
# @param end_line [Integer] Last line to read (1-indexed)
|
|
170
|
+
# @return [Array<Hash>] Array of line data
|
|
171
|
+
def read_specific_lines(file_path, start_line, end_line)
|
|
172
|
+
start_line = [ start_line, 1 ].max
|
|
173
|
+
lines = []
|
|
174
|
+
|
|
175
|
+
File.open(file_path, "r") do |file|
|
|
176
|
+
file.each_line.with_index(1) do |line, line_num|
|
|
177
|
+
break if line_num > end_line
|
|
178
|
+
|
|
179
|
+
next if line_num < start_line
|
|
180
|
+
|
|
181
|
+
lines << {
|
|
182
|
+
number: line_num,
|
|
183
|
+
content: line.chomp,
|
|
184
|
+
highlight: line_num == @line_number
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
lines
|
|
190
|
+
rescue StandardError => e
|
|
191
|
+
RailsErrorDashboard::Logger.error("Error reading specific lines from #{file_path}: #{e.message}")
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check if file is binary
|
|
196
|
+
#
|
|
197
|
+
# @param file_path [String] Path to file
|
|
198
|
+
# @return [Boolean]
|
|
199
|
+
def binary_file?(file_path)
|
|
200
|
+
# Read first 8KB to check for null bytes
|
|
201
|
+
sample_size = 8192
|
|
202
|
+
File.open(file_path, "rb") do |file|
|
|
203
|
+
sample = file.read(sample_size)
|
|
204
|
+
return false if sample.nil? || sample.empty?
|
|
205
|
+
|
|
206
|
+
# Binary files typically contain null bytes
|
|
207
|
+
sample.include?("\x00")
|
|
208
|
+
end
|
|
209
|
+
rescue StandardError
|
|
210
|
+
false # If we can't determine, assume text
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
require "rails_error_dashboard/version"
|
|
2
2
|
require "rails_error_dashboard/engine"
|
|
3
|
+
require "rails_error_dashboard/configuration_error"
|
|
3
4
|
require "rails_error_dashboard/configuration"
|
|
4
5
|
require "rails_error_dashboard/logger"
|
|
5
6
|
require "rails_error_dashboard/manual_error_reporter"
|
|
@@ -16,6 +17,7 @@ require "turbo-rails"
|
|
|
16
17
|
|
|
17
18
|
# Core library files
|
|
18
19
|
require "rails_error_dashboard/value_objects/error_context"
|
|
20
|
+
require "rails_error_dashboard/helpers/user_model_detector"
|
|
19
21
|
require "rails_error_dashboard/services/platform_detector"
|
|
20
22
|
require "rails_error_dashboard/services/backtrace_parser"
|
|
21
23
|
require "rails_error_dashboard/services/similarity_calculator"
|
|
@@ -23,6 +25,10 @@ require "rails_error_dashboard/services/cascade_detector"
|
|
|
23
25
|
require "rails_error_dashboard/services/baseline_calculator"
|
|
24
26
|
require "rails_error_dashboard/services/baseline_alert_throttler"
|
|
25
27
|
require "rails_error_dashboard/services/pattern_detector"
|
|
28
|
+
require "rails_error_dashboard/services/error_normalizer"
|
|
29
|
+
require "rails_error_dashboard/services/source_code_reader"
|
|
30
|
+
require "rails_error_dashboard/services/git_blame_reader"
|
|
31
|
+
require "rails_error_dashboard/services/github_link_generator"
|
|
26
32
|
require "rails_error_dashboard/queries/co_occurring_errors"
|
|
27
33
|
require "rails_error_dashboard/queries/error_cascades"
|
|
28
34
|
require "rails_error_dashboard/queries/baseline_stats"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails_error_dashboard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.30
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -267,13 +267,6 @@ files:
|
|
|
267
267
|
- MIT-LICENSE
|
|
268
268
|
- README.md
|
|
269
269
|
- Rakefile
|
|
270
|
-
- app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss
|
|
271
|
-
- app/assets/stylesheets/rails_error_dashboard/_components.scss
|
|
272
|
-
- app/assets/stylesheets/rails_error_dashboard/_layout.scss
|
|
273
|
-
- app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss
|
|
274
|
-
- app/assets/stylesheets/rails_error_dashboard/application.css
|
|
275
|
-
- app/assets/stylesheets/rails_error_dashboard/application.css.map
|
|
276
|
-
- app/assets/stylesheets/rails_error_dashboard/application.scss
|
|
277
270
|
- app/controllers/rails_error_dashboard/application_controller.rb
|
|
278
271
|
- app/controllers/rails_error_dashboard/errors_controller.rb
|
|
279
272
|
- app/helpers/rails_error_dashboard/application_helper.rb
|
|
@@ -298,11 +291,11 @@ files:
|
|
|
298
291
|
- app/models/rails_error_dashboard/error_logs_record.rb
|
|
299
292
|
- app/models/rails_error_dashboard/error_occurrence.rb
|
|
300
293
|
- app/views/layouts/rails_error_dashboard.html.erb
|
|
301
|
-
- app/views/layouts/rails_error_dashboard/application.html.erb
|
|
302
294
|
- app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb
|
|
303
295
|
- app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
|
|
304
296
|
- app/views/rails_error_dashboard/errors/_error_row.html.erb
|
|
305
297
|
- app/views/rails_error_dashboard/errors/_pattern_insights.html.erb
|
|
298
|
+
- app/views/rails_error_dashboard/errors/_source_code.html.erb
|
|
306
299
|
- app/views/rails_error_dashboard/errors/_stats.html.erb
|
|
307
300
|
- app/views/rails_error_dashboard/errors/_timeline.html.erb
|
|
308
301
|
- app/views/rails_error_dashboard/errors/_user_errors_table.html.erb
|
|
@@ -312,9 +305,11 @@ files:
|
|
|
312
305
|
- app/views/rails_error_dashboard/errors/overview.html.erb
|
|
313
306
|
- app/views/rails_error_dashboard/errors/platform_comparison.html.erb
|
|
314
307
|
- app/views/rails_error_dashboard/errors/settings.html.erb
|
|
308
|
+
- app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb
|
|
315
309
|
- app/views/rails_error_dashboard/errors/show.html.erb
|
|
316
310
|
- config/routes.rb
|
|
317
311
|
- db/development.sqlite3
|
|
312
|
+
- db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb
|
|
318
313
|
- db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb
|
|
319
314
|
- db/migrate/20251224081522_add_better_tracking_to_error_logs.rb
|
|
320
315
|
- db/migrate/20251224101217_add_controller_action_to_error_logs.rb
|
|
@@ -345,8 +340,10 @@ files:
|
|
|
345
340
|
- lib/rails_error_dashboard/commands/log_error.rb
|
|
346
341
|
- lib/rails_error_dashboard/commands/resolve_error.rb
|
|
347
342
|
- lib/rails_error_dashboard/configuration.rb
|
|
343
|
+
- lib/rails_error_dashboard/configuration_error.rb
|
|
348
344
|
- lib/rails_error_dashboard/engine.rb
|
|
349
345
|
- lib/rails_error_dashboard/error_reporter.rb
|
|
346
|
+
- lib/rails_error_dashboard/helpers/user_model_detector.rb
|
|
350
347
|
- lib/rails_error_dashboard/logger.rb
|
|
351
348
|
- lib/rails_error_dashboard/manual_error_reporter.rb
|
|
352
349
|
- lib/rails_error_dashboard/middleware/error_catcher.rb
|
|
@@ -372,9 +369,13 @@ files:
|
|
|
372
369
|
- lib/rails_error_dashboard/services/baseline_alert_throttler.rb
|
|
373
370
|
- lib/rails_error_dashboard/services/baseline_calculator.rb
|
|
374
371
|
- lib/rails_error_dashboard/services/cascade_detector.rb
|
|
372
|
+
- lib/rails_error_dashboard/services/error_normalizer.rb
|
|
373
|
+
- lib/rails_error_dashboard/services/git_blame_reader.rb
|
|
374
|
+
- lib/rails_error_dashboard/services/github_link_generator.rb
|
|
375
375
|
- lib/rails_error_dashboard/services/pattern_detector.rb
|
|
376
376
|
- lib/rails_error_dashboard/services/platform_detector.rb
|
|
377
377
|
- lib/rails_error_dashboard/services/similarity_calculator.rb
|
|
378
|
+
- lib/rails_error_dashboard/services/source_code_reader.rb
|
|
378
379
|
- lib/rails_error_dashboard/value_objects/error_context.rb
|
|
379
380
|
- lib/rails_error_dashboard/version.rb
|
|
380
381
|
- lib/tasks/error_dashboard.rake
|
|
@@ -386,8 +387,10 @@ metadata:
|
|
|
386
387
|
homepage_uri: https://github.com/AnjanJ/rails_error_dashboard
|
|
387
388
|
source_code_uri: https://github.com/AnjanJ/rails_error_dashboard
|
|
388
389
|
changelog_uri: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
|
|
390
|
+
documentation_uri: https://rails-error-dashboard.anjan.dev
|
|
391
|
+
demo_uri: https://rails-error-dashboard.anjan.dev
|
|
389
392
|
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
390
|
-
\ Rails Error Dashboard v0.1.
|
|
393
|
+
\ Rails Error Dashboard v0.1.30\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
|
|
391
394
|
First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
|
|
392
395
|
db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
|
|
393
396
|
=> '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n
|