bugsage 0.2.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 +7 -0
- data/ARCHITECTURE.md +442 -0
- data/CHANGELOG.md +28 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +301 -0
- data/LICENSE.txt +21 -0
- data/README.md +344 -0
- data/ROADMAP.md +217 -0
- data/SECURITY.md +83 -0
- data/docs/AI.md +167 -0
- data/docs/Configuration.md +168 -0
- data/docs/GettingStarted.md +146 -0
- data/docs/RELEASE_CHECKLIST.md +346 -0
- data/docs/Troubleshooting.md +181 -0
- data/docs/images/BadRequest.png +0 -0
- data/docs/images/BadRequest2.png +0 -0
- data/docs/images/BugSage_Logo.png +0 -0
- data/docs/images/BugSage_Social_Preview.png +0 -0
- data/docs/images/NoMethodError.png +0 -0
- data/docs/images/NoMethodError2.png +0 -0
- data/docs/images/README.md +38 -0
- data/docs/releases/README.md +21 -0
- data/docs/releases/v0.2.0.md +40 -0
- data/exe/bugsage +7 -0
- data/lib/bugsage/ai_analyzer.rb +145 -0
- data/lib/bugsage/ai_chat.rb +388 -0
- data/lib/bugsage/ai_context.rb +69 -0
- data/lib/bugsage/ai_panel.rb +708 -0
- data/lib/bugsage/ai_support.rb +36 -0
- data/lib/bugsage/auto_configurator.rb +97 -0
- data/lib/bugsage/cli.rb +59 -0
- data/lib/bugsage/code_context.rb +81 -0
- data/lib/bugsage/code_patch.rb +147 -0
- data/lib/bugsage/configuration.rb +149 -0
- data/lib/bugsage/console_context.rb +51 -0
- data/lib/bugsage/cursor_client.rb +165 -0
- data/lib/bugsage/dashboard.rb +627 -0
- data/lib/bugsage/editor_links.rb +34 -0
- data/lib/bugsage/error_page.rb +298 -0
- data/lib/bugsage/exception_handler.rb +66 -0
- data/lib/bugsage/exception_support.rb +95 -0
- data/lib/bugsage/exceptions_app.rb +31 -0
- data/lib/bugsage/fix_applicator.rb +107 -0
- data/lib/bugsage/formatter.rb +37 -0
- data/lib/bugsage/http_error_capture.rb +104 -0
- data/lib/bugsage/http_response_error.rb +14 -0
- data/lib/bugsage/inline_console.rb +226 -0
- data/lib/bugsage/installation.rb +94 -0
- data/lib/bugsage/installer.rb +66 -0
- data/lib/bugsage/json_endpoint.rb +34 -0
- data/lib/bugsage/locales/en.yml +439 -0
- data/lib/bugsage/middleware.rb +152 -0
- data/lib/bugsage/openai_client.rb +103 -0
- data/lib/bugsage/page_actions.rb +255 -0
- data/lib/bugsage/railtie.rb +49 -0
- data/lib/bugsage/request_context.rb +56 -0
- data/lib/bugsage/rule.rb +271 -0
- data/lib/bugsage/session_clear.rb +35 -0
- data/lib/bugsage/store.rb +60 -0
- data/lib/bugsage/suggestion.rb +58 -0
- data/lib/bugsage/trace_cleaner.rb +24 -0
- data/lib/bugsage/translations.rb +85 -0
- data/lib/bugsage/version.rb +5 -0
- data/lib/bugsage.rb +65 -0
- data/lib/generators/bugsage/install/install_generator.rb +21 -0
- data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
- data/scripts/publish-github-release.sh +38 -0
- data/sig/bugsage.rbs +4 -0
- metadata +157 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugsage
|
|
6
|
+
module PageActions
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def render_clear_button(label: Bugsage.t("ui.page_actions.clear_session_logs"))
|
|
10
|
+
<<~HTML
|
|
11
|
+
<button type="button" class="bugsage-clear-logs secondary-button">#{CodeContext.escape_html(label)}</button>
|
|
12
|
+
HTML
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render_fix_actions(location:, suffix: "", hidden: true)
|
|
16
|
+
editor = EditorLinks.for_location(location)
|
|
17
|
+
return "" if editor.empty?
|
|
18
|
+
|
|
19
|
+
hidden_class = hidden ? " hidden" : ""
|
|
20
|
+
location_attr = CodeContext.escape_html(location)
|
|
21
|
+
|
|
22
|
+
<<~HTML
|
|
23
|
+
<div class="fix-actions#{hidden_class}" id="bugsage-fix-actions#{suffix}" data-location="#{location_attr}">
|
|
24
|
+
<button type="button" class="secondary-button bugsage-open-editor" data-editor="cursor" data-url="#{CodeContext.escape_html(editor[:cursor])}">
|
|
25
|
+
#{CodeContext.escape_html(Bugsage.t("ui.page_actions.open_in_cursor"))}
|
|
26
|
+
</button>
|
|
27
|
+
<button type="button" class="secondary-button bugsage-open-editor" data-editor="vscode" data-url="#{CodeContext.escape_html(editor[:vscode])}">
|
|
28
|
+
#{CodeContext.escape_html(Bugsage.t("ui.page_actions.open_in_vscode"))}
|
|
29
|
+
</button>
|
|
30
|
+
<button type="button" class="secondary-button bugsage-copy-fix">#{CodeContext.escape_html(Bugsage.t("ui.page_actions.copy_fix"))}</button>
|
|
31
|
+
<button type="button" class="secondary-button bugsage-apply-fix">#{CodeContext.escape_html(Bugsage.t("ui.page_actions.apply_fix_to_file"))}</button>
|
|
32
|
+
</div>
|
|
33
|
+
HTML
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def styles
|
|
37
|
+
<<~CSS
|
|
38
|
+
.page-actions {
|
|
39
|
+
display: flex;
|
|
40
|
+
justify-content: flex-end;
|
|
41
|
+
gap: 8px;
|
|
42
|
+
margin-bottom: 16px;
|
|
43
|
+
}
|
|
44
|
+
.fix-actions {
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-wrap: wrap;
|
|
47
|
+
gap: 8px;
|
|
48
|
+
margin-top: 12px;
|
|
49
|
+
}
|
|
50
|
+
.fix-actions.hidden {
|
|
51
|
+
display: none;
|
|
52
|
+
}
|
|
53
|
+
.secondary-button {
|
|
54
|
+
background: #45475a;
|
|
55
|
+
color: #cdd6f4;
|
|
56
|
+
border: 1px solid #585869;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
padding: 8px 12px;
|
|
59
|
+
font-family: inherit;
|
|
60
|
+
font-size: 12px;
|
|
61
|
+
font-weight: bold;
|
|
62
|
+
cursor: pointer;
|
|
63
|
+
}
|
|
64
|
+
.secondary-button:hover {
|
|
65
|
+
border-color: #89b4fa;
|
|
66
|
+
color: #89b4fa;
|
|
67
|
+
}
|
|
68
|
+
.bugsage-clear-logs {
|
|
69
|
+
background: rgba(243, 139, 168, 0.15);
|
|
70
|
+
border-color: #f38ba8;
|
|
71
|
+
color: #f5c2e7;
|
|
72
|
+
}
|
|
73
|
+
CSS
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_script
|
|
77
|
+
empty_list_html = <<~HTML.strip.gsub("\n", "")
|
|
78
|
+
<div class="empty-list"><p>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_list_title"))}</p><p class="empty-hint">#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_list_hint"))}</p></div>
|
|
79
|
+
HTML
|
|
80
|
+
empty_detail_html = <<~HTML.strip.gsub("\n", "")
|
|
81
|
+
<div class="detail-placeholder"><h2>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_detail_title"))}</h2><p>#{CodeContext.escape_html(Bugsage.t("ui.dashboard.empty_detail_body"))}</p></div>
|
|
82
|
+
HTML
|
|
83
|
+
|
|
84
|
+
<<~HTML
|
|
85
|
+
<script>
|
|
86
|
+
(function () {
|
|
87
|
+
if (window.__bugsagePageActionsInitialized) return;
|
|
88
|
+
window.__bugsagePageActionsInitialized = true;
|
|
89
|
+
|
|
90
|
+
var EMPTY_LIST_HTML = #{JSON.generate(empty_list_html)};
|
|
91
|
+
var EMPTY_DETAIL_HTML = #{JSON.generate(empty_detail_html)};
|
|
92
|
+
var CAUGHT_COUNT_ZERO = #{JSON.generate(Bugsage.t("ui.dashboard.caught_count", count: 0))};
|
|
93
|
+
var AVG_EMPTY = #{JSON.generate(Bugsage.t("ui.dashboard.avg_empty"))};
|
|
94
|
+
var CONFIRM_CLEAR = #{JSON.generate(Bugsage.t("ui.page_actions.confirm_clear"))};
|
|
95
|
+
var COULD_NOT_CLEAR = #{JSON.generate(Bugsage.t("ui.page_actions.could_not_clear"))};
|
|
96
|
+
var QUICK_FIX_TITLE = #{JSON.generate(Bugsage.t("ui.page_actions.quick_fix_title"))};
|
|
97
|
+
var QUICK_FIX_LOCATION = #{JSON.generate(Bugsage.t("ui.page_actions.quick_fix_location"))};
|
|
98
|
+
var QUICK_FIX_SUGGESTION = #{JSON.generate(Bugsage.t("ui.page_actions.quick_fix_suggestion"))};
|
|
99
|
+
var COPIED = #{JSON.generate(Bugsage.t("ui.page_actions.copied"))};
|
|
100
|
+
var COPY_FIX = #{JSON.generate(Bugsage.t("ui.page_actions.copy_fix"))};
|
|
101
|
+
var CONFIRM_APPLY = #{JSON.generate(Bugsage.t("ui.page_actions.confirm_apply"))};
|
|
102
|
+
var APPLYING = #{JSON.generate(Bugsage.t("ui.page_actions.applying"))};
|
|
103
|
+
var COULD_NOT_APPLY = #{JSON.generate(Bugsage.t("ui.page_actions.could_not_apply"))};
|
|
104
|
+
var APPLIED = #{JSON.generate(Bugsage.t("ui.page_actions.applied"))};
|
|
105
|
+
|
|
106
|
+
function selectedFixText(actions) {
|
|
107
|
+
var panel = actions.closest(".bug-detail, .container");
|
|
108
|
+
if (!panel) return "";
|
|
109
|
+
|
|
110
|
+
var activeFix = panel.querySelector(".fixes li.selected, .fixes li");
|
|
111
|
+
return activeFix ? activeFix.textContent.trim() : "";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function clipboardCopy(text) {
|
|
115
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
116
|
+
return navigator.clipboard.writeText(text);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
var area = document.createElement("textarea");
|
|
120
|
+
area.value = text;
|
|
121
|
+
document.body.appendChild(area);
|
|
122
|
+
area.select();
|
|
123
|
+
document.execCommand("copy");
|
|
124
|
+
document.body.removeChild(area);
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function clearDashboardUi() {
|
|
129
|
+
var bugList = document.querySelector(".bug-list");
|
|
130
|
+
var detailPanel = document.querySelector(".detail-panel");
|
|
131
|
+
var clearButton = document.querySelector(".bugsage-clear-logs");
|
|
132
|
+
var statPills = document.querySelectorAll(".sidebar-stats .stat-pill");
|
|
133
|
+
|
|
134
|
+
if (bugList) {
|
|
135
|
+
bugList.innerHTML = EMPTY_LIST_HTML;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (detailPanel) {
|
|
139
|
+
detailPanel.innerHTML = EMPTY_DETAIL_HTML;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (statPills[0]) statPills[0].textContent = CAUGHT_COUNT_ZERO;
|
|
143
|
+
if (statPills[1]) statPills[1].textContent = AVG_EMPTY;
|
|
144
|
+
if (clearButton) clearButton.hidden = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function openEditorUrl(url) {
|
|
148
|
+
var link = document.createElement("a");
|
|
149
|
+
link.href = url;
|
|
150
|
+
link.rel = "noopener";
|
|
151
|
+
link.style.display = "none";
|
|
152
|
+
document.body.appendChild(link);
|
|
153
|
+
link.click();
|
|
154
|
+
document.body.removeChild(link);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.addEventListener("click", function (event) {
|
|
158
|
+
var clearButton = event.target.closest(".bugsage-clear-logs");
|
|
159
|
+
if (clearButton) {
|
|
160
|
+
event.preventDefault();
|
|
161
|
+
if (!window.confirm(CONFIRM_CLEAR)) return;
|
|
162
|
+
|
|
163
|
+
clearButton.disabled = true;
|
|
164
|
+
fetch("#{SessionClear::ENDPOINT}", {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: { "Content-Type": "application/json", "Accept": "application/json" }
|
|
167
|
+
})
|
|
168
|
+
.then(function (response) { return response.json(); })
|
|
169
|
+
.then(function () { clearDashboardUi(); })
|
|
170
|
+
.catch(function (error) {
|
|
171
|
+
clearButton.disabled = false;
|
|
172
|
+
window.alert(error.message || COULD_NOT_CLEAR);
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var openButton = event.target.closest(".bugsage-open-editor");
|
|
178
|
+
if (openButton) {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
var url = openButton.dataset.url;
|
|
181
|
+
if (url) openEditorUrl(url);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
var copyButton = event.target.closest(".bugsage-copy-fix");
|
|
186
|
+
if (copyButton) {
|
|
187
|
+
var actions = copyButton.closest(".fix-actions");
|
|
188
|
+
var fix = selectedFixText(actions);
|
|
189
|
+
var location = actions.dataset.location || "";
|
|
190
|
+
var prompt = [
|
|
191
|
+
QUICK_FIX_TITLE,
|
|
192
|
+
QUICK_FIX_LOCATION.replace("%{location}", location),
|
|
193
|
+
QUICK_FIX_SUGGESTION.replace("%{fix}", fix)
|
|
194
|
+
].join("\\n");
|
|
195
|
+
clipboardCopy(prompt).then(function () {
|
|
196
|
+
copyButton.textContent = COPIED;
|
|
197
|
+
setTimeout(function () { copyButton.textContent = COPY_FIX; }, 1500);
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
var applyButton = event.target.closest(".bugsage-apply-fix");
|
|
203
|
+
if (applyButton) {
|
|
204
|
+
event.preventDefault();
|
|
205
|
+
var actionBar = applyButton.closest(".fix-actions");
|
|
206
|
+
var fixText = selectedFixText(actionBar);
|
|
207
|
+
var fixLocation = actionBar.dataset.location || "";
|
|
208
|
+
if (!fixText) return;
|
|
209
|
+
if (!window.confirm(CONFIRM_APPLY)) return;
|
|
210
|
+
|
|
211
|
+
var originalText = applyButton.textContent;
|
|
212
|
+
applyButton.disabled = true;
|
|
213
|
+
applyButton.textContent = APPLYING;
|
|
214
|
+
fetch("#{FixApplicator::ENDPOINT}", {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json" },
|
|
217
|
+
body: JSON.stringify({ location: fixLocation, fix: fixText })
|
|
218
|
+
})
|
|
219
|
+
.then(function (response) { return response.json(); })
|
|
220
|
+
.then(function (result) {
|
|
221
|
+
applyButton.disabled = false;
|
|
222
|
+
applyButton.textContent = originalText;
|
|
223
|
+
if (!result.ok) {
|
|
224
|
+
window.alert(result.error || COULD_NOT_APPLY);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
applyButton.textContent = APPLIED;
|
|
229
|
+
setTimeout(function () { applyButton.textContent = originalText; }, 2000);
|
|
230
|
+
})
|
|
231
|
+
.catch(function (error) {
|
|
232
|
+
applyButton.disabled = false;
|
|
233
|
+
applyButton.textContent = originalText;
|
|
234
|
+
window.alert(error.message);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
document.addEventListener("click", function (event) {
|
|
240
|
+
var fixItem = event.target.closest(".fixes li");
|
|
241
|
+
if (!fixItem) return;
|
|
242
|
+
|
|
243
|
+
var panel = fixItem.closest(".bug-detail, .container");
|
|
244
|
+
if (!panel) return;
|
|
245
|
+
|
|
246
|
+
panel.querySelectorAll(".fixes li").forEach(function (item) {
|
|
247
|
+
item.classList.toggle("selected", item === fixItem);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
})();
|
|
251
|
+
</script>
|
|
252
|
+
HTML
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# BugSage Rails integration.
|
|
4
|
+
#
|
|
5
|
+
# Installation (zero-config):
|
|
6
|
+
# 1. gem "bugsage" in Gemfile
|
|
7
|
+
# 2. bundle install
|
|
8
|
+
# 3. bin/rails server
|
|
9
|
+
#
|
|
10
|
+
# See Bugsage::Installation for the full guide.
|
|
11
|
+
module Bugsage
|
|
12
|
+
class Railtie < Rails::Railtie
|
|
13
|
+
config.bugsage = ActiveSupport::OrderedOptions.new
|
|
14
|
+
|
|
15
|
+
initializer "bugsage.configuration" do |app|
|
|
16
|
+
Bugsage.configure do |bugsage_config|
|
|
17
|
+
if app.config.bugsage.respond_to?(:each_pair)
|
|
18
|
+
app.config.bugsage.each_pair do |key, value|
|
|
19
|
+
bugsage_config.public_send("#{key}=", value) if bugsage_config.respond_to?("#{key}=")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Bugsage::AutoConfigurator.apply!
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
initializer "bugsage.i18n" do
|
|
28
|
+
Bugsage::Translations.load!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
initializer "bugsage.middleware" do |app|
|
|
32
|
+
app.config.middleware.use Bugsage::Middleware
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
initializer "bugsage.exceptions_app" do |app|
|
|
36
|
+
Bugsage.configuration.fallback_exceptions_app ||= app.config.exceptions_app
|
|
37
|
+
app.config.exceptions_app = Bugsage::ExceptionsApp
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
initializer "bugsage.debug_interceptor" do
|
|
41
|
+
ActionDispatch::DebugExceptions.register_interceptor do |request, exception|
|
|
42
|
+
next unless Bugsage.configuration.enabled?
|
|
43
|
+
next unless Bugsage.configuration.capture_errors?
|
|
44
|
+
|
|
45
|
+
Bugsage::ExceptionHandler.store_exception(request.env, exception)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsage
|
|
4
|
+
# Builds a labeled Rails request context hash from a Rack env.
|
|
5
|
+
module RequestContext
|
|
6
|
+
class << self
|
|
7
|
+
def from_env(env)
|
|
8
|
+
return {} unless env.is_a?(Hash)
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
Bugsage.t("context.request_method") => env["REQUEST_METHOD"],
|
|
12
|
+
Bugsage.t("context.path") => env["PATH_INFO"],
|
|
13
|
+
Bugsage.t("context.query_string") => env["QUERY_STRING"],
|
|
14
|
+
Bugsage.t("context.host") => env["HTTP_HOST"] || env["SERVER_NAME"],
|
|
15
|
+
Bugsage.t("context.request_id") => env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"],
|
|
16
|
+
Bugsage.t("context.controller") => path_parameter_value(env, "controller"),
|
|
17
|
+
Bugsage.t("context.action") => path_parameter_value(env, "action"),
|
|
18
|
+
Bugsage.t("context.path_parameters") => path_parameters(env),
|
|
19
|
+
Bugsage.t("context.request_parameters") => request_parameters(env),
|
|
20
|
+
Bugsage.t("context.query_parameters") => query_parameters(env),
|
|
21
|
+
Bugsage.t("context.form_parameters") => form_parameters(env),
|
|
22
|
+
Bugsage.t("context.user_agent") => env["HTTP_USER_AGENT"]
|
|
23
|
+
}.compact.reject { |_, value| blank?(value) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def path_parameter_value(env, key)
|
|
29
|
+
params = path_parameters(env)
|
|
30
|
+
return nil unless params.is_a?(Hash)
|
|
31
|
+
|
|
32
|
+
params[key.to_s] || params[key.to_sym]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def path_parameters(env)
|
|
36
|
+
env["action_dispatch.request.path_parameters"]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def request_parameters(env)
|
|
40
|
+
env["action_dispatch.request.parameters"]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def query_parameters(env)
|
|
44
|
+
env["action_dispatch.request.query_parameters"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def form_parameters(env)
|
|
48
|
+
env["action_dispatch.request.form_parameters"]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def blank?(value)
|
|
52
|
+
value.respond_to?(:empty?) ? value.empty? : value.nil? || value == ""
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
data/lib/bugsage/rule.rb
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsage
|
|
4
|
+
class Rule
|
|
5
|
+
# Ordering matters: more specific classes must be listed before their
|
|
6
|
+
# parents because matching walks the exception's ancestor chain.
|
|
7
|
+
MATCHERS = [
|
|
8
|
+
{ classes: ["ActionController::RoutingError"], message: /No route matches/i, handler: :match_routing_error },
|
|
9
|
+
{ classes: ["NoMethodError"], handler: :match_no_method_error },
|
|
10
|
+
{ classes: ["ActiveRecord::RecordNotFound"], handler: :match_record_not_found },
|
|
11
|
+
{ classes: ["ActionController::ParameterMissing"], handler: :match_parameter_missing },
|
|
12
|
+
{ classes: ["ActionController::UnpermittedParameters"], handler: :match_unpermitted_parameters },
|
|
13
|
+
{ classes: ["ActionController::BadRequest"], handler: :match_bad_request },
|
|
14
|
+
{ classes: ["Bugsage::HttpResponseError"], handler: :match_http_response_error },
|
|
15
|
+
{ classes: ["ActionController::InvalidAuthenticityToken"], handler: :match_invalid_authenticity_token },
|
|
16
|
+
{ classes: ["ActionDispatch::Http::Parameters::ParseError"], handler: :match_parse_error },
|
|
17
|
+
{ classes: ["ActiveJob::DeserializationError"], handler: :match_deserialization_error },
|
|
18
|
+
{ classes: ["ActionView::Template::Error"], handler: :match_template_error },
|
|
19
|
+
{ classes: ["ActionController::MissingExactTemplate"], handler: :match_missing_exact_template },
|
|
20
|
+
{ classes: ["ActionController::UnknownFormat"], handler: :match_unknown_format },
|
|
21
|
+
{ classes: ["ActionController::InvalidCrossOriginRequest"], handler: :match_invalid_cross_origin_request },
|
|
22
|
+
{ classes: ["ActionDispatch::Flash::FlashError"], handler: :match_flash_error },
|
|
23
|
+
{ classes: ["ActiveStorage::FileNotFoundError"], handler: :match_active_storage_error },
|
|
24
|
+
{ classes: ["ActiveStorage::IntegrityError"], handler: :match_active_storage_integrity_error },
|
|
25
|
+
{ classes: ["ActiveSupport::MessageVerifier::InvalidSignature"], handler: :match_invalid_signature },
|
|
26
|
+
# Database — specific violations before the generic StatementInvalid.
|
|
27
|
+
{ classes: ["ActiveRecord::RecordNotUnique"], handler: :match_record_not_unique },
|
|
28
|
+
{ classes: ["ActiveRecord::NotNullViolation"], handler: :match_not_null_violation },
|
|
29
|
+
{ classes: ["ActiveRecord::StatementInvalid"], handler: :match_statement_invalid },
|
|
30
|
+
{ classes: ["ActiveRecord::ConnectionNotEstablished"], handler: :match_connection_not_established },
|
|
31
|
+
{ classes: ["ActiveRecord::StaleObjectError"], handler: :match_stale_object },
|
|
32
|
+
{ classes: ["ActiveRecord::RecordInvalid"], handler: :match_record_invalid },
|
|
33
|
+
{ classes: ["PG::Error"], handler: :match_pg_error },
|
|
34
|
+
{ classes: ["Redis::BaseError"], handler: :match_redis_error },
|
|
35
|
+
{ classes: ["Faraday::Error"], handler: :match_faraday_error },
|
|
36
|
+
# Ruby core — NoMethodError (above) is a subclass of NameError, so it wins first.
|
|
37
|
+
{ classes: ["NameError"], handler: :match_name_error },
|
|
38
|
+
{ classes: ["KeyError"], handler: :match_key_error },
|
|
39
|
+
{ classes: ["ArgumentError"], handler: :match_argument_error },
|
|
40
|
+
{ classes: ["TypeError"], handler: :match_type_error },
|
|
41
|
+
{ classes: ["ZeroDivisionError"], handler: :match_zero_division_error },
|
|
42
|
+
{ classes: ["JSON::ParserError"], handler: :match_json_parse_error },
|
|
43
|
+
{ classes: ["Net::OpenTimeout", "Net::ReadTimeout", "Timeout::Error"],
|
|
44
|
+
message: /timed out|timeout|execution expired/i, handler: :match_timeout_error },
|
|
45
|
+
{ classes: ["FrozenError"], handler: :match_frozen_error },
|
|
46
|
+
{ classes: ["RuntimeError"], handler: :match_runtime_error }
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
def self.match(exception)
|
|
50
|
+
exception = ExceptionSupport.unwrap(exception)
|
|
51
|
+
return nil unless exception.is_a?(Exception)
|
|
52
|
+
|
|
53
|
+
handler = matcher_for(exception)
|
|
54
|
+
send(handler, exception)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.matcher_for(exception)
|
|
58
|
+
definition = MATCHERS.find { |matcher| matches_definition?(exception, matcher) }
|
|
59
|
+
definition&.fetch(:handler) || :match_generic_exception
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def self.matches_definition?(exception, definition)
|
|
63
|
+
return true if definition[:classes]&.any? { |class_name| ExceptionSupport.matches_class?(exception, class_name) }
|
|
64
|
+
|
|
65
|
+
return true if definition[:message] && ExceptionSupport.message_matches?(exception, definition[:message])
|
|
66
|
+
|
|
67
|
+
false
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.rule_fixes(key)
|
|
71
|
+
Array(Bugsage.t("rules.#{key}.fixes"))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def self.rule_issue(key, exception)
|
|
75
|
+
issue = Bugsage.t("rules.#{key}.issue", default: nil)
|
|
76
|
+
return issue if issue
|
|
77
|
+
|
|
78
|
+
class_name = exception.class.name.to_s
|
|
79
|
+
return class_name unless class_name.empty?
|
|
80
|
+
|
|
81
|
+
Bugsage.t("rules.#{key}.issue_fallback", default: class_name)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.from_rule(key, exception, confidence:, root_cause: nil)
|
|
85
|
+
build_suggestion(
|
|
86
|
+
exception: exception,
|
|
87
|
+
issue: rule_issue(key, exception),
|
|
88
|
+
root_cause: root_cause.nil? ? exception.message : root_cause,
|
|
89
|
+
fixes: rule_fixes(key),
|
|
90
|
+
confidence: confidence
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def self.match_no_method_error(exception)
|
|
95
|
+
root_cause = if exception.message.to_s.empty?
|
|
96
|
+
Bugsage.t("rules.no_method_error.root_cause_default")
|
|
97
|
+
else
|
|
98
|
+
exception.message
|
|
99
|
+
end
|
|
100
|
+
from_rule(:no_method_error, exception, confidence: 95, root_cause: root_cause)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.match_record_not_found(exception)
|
|
104
|
+
from_rule(:record_not_found, exception, confidence: 90)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.match_routing_error(exception)
|
|
108
|
+
from_rule(:routing_error, exception, confidence: 92)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.match_parameter_missing(exception)
|
|
112
|
+
from_rule(:parameter_missing, exception, confidence: 91)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.match_unpermitted_parameters(exception)
|
|
116
|
+
from_rule(:unpermitted_parameters, exception, confidence: 90)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def self.match_bad_request(exception)
|
|
120
|
+
from_rule(:bad_request, exception, confidence: 88)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def self.match_http_response_error(exception)
|
|
124
|
+
status = exception.status
|
|
125
|
+
build_suggestion(
|
|
126
|
+
exception: exception,
|
|
127
|
+
issue: Bugsage.t("http_errors.issue", status: status),
|
|
128
|
+
root_cause: exception.message,
|
|
129
|
+
fixes: HttpErrorCapture.fixes_for_status(status),
|
|
130
|
+
confidence: HttpErrorCapture.confidence_for_status(status)
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.match_invalid_authenticity_token(exception)
|
|
135
|
+
from_rule(:invalid_authenticity_token, exception, confidence: 91)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def self.match_parse_error(exception)
|
|
139
|
+
from_rule(:parse_error, exception, confidence: 88)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.match_deserialization_error(exception)
|
|
143
|
+
from_rule(:deserialization_error, exception, confidence: 87)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.match_template_error(exception)
|
|
147
|
+
from_rule(:template_error, exception, confidence: 88)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.match_missing_exact_template(exception)
|
|
151
|
+
from_rule(:missing_exact_template, exception, confidence: 88)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def self.match_active_storage_error(exception)
|
|
155
|
+
from_rule(:active_storage_error, exception, confidence: 87)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def self.match_active_storage_integrity_error(exception)
|
|
159
|
+
from_rule(:active_storage_integrity_error, exception, confidence: 88)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.match_redis_error(exception)
|
|
163
|
+
from_rule(:redis_error, exception, confidence: 86)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def self.match_unknown_format(exception)
|
|
167
|
+
from_rule(:unknown_format, exception, confidence: 87)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.match_invalid_cross_origin_request(exception)
|
|
171
|
+
from_rule(:invalid_cross_origin_request, exception, confidence: 88)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.match_flash_error(exception)
|
|
175
|
+
from_rule(:flash_error, exception, confidence: 86)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def self.match_invalid_signature(exception)
|
|
179
|
+
from_rule(:invalid_signature, exception, confidence: 88)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.match_faraday_error(exception)
|
|
183
|
+
from_rule(:faraday_error, exception, confidence: 85)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.match_record_invalid(exception)
|
|
187
|
+
from_rule(:record_invalid, exception, confidence: 89)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.match_record_not_unique(exception)
|
|
191
|
+
from_rule(:record_not_unique, exception, confidence: 90)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.match_not_null_violation(exception)
|
|
195
|
+
from_rule(:not_null_violation, exception, confidence: 89)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.match_statement_invalid(exception)
|
|
199
|
+
from_rule(:statement_invalid, exception, confidence: 86)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def self.match_connection_not_established(exception)
|
|
203
|
+
from_rule(:connection_not_established, exception, confidence: 88)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def self.match_stale_object(exception)
|
|
207
|
+
from_rule(:stale_object, exception, confidence: 87)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.match_pg_error(exception)
|
|
211
|
+
from_rule(:pg_error, exception, confidence: 85)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.match_name_error(exception)
|
|
215
|
+
from_rule(:name_error, exception, confidence: 88)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.match_key_error(exception)
|
|
219
|
+
from_rule(:key_error, exception, confidence: 86)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.match_argument_error(exception)
|
|
223
|
+
from_rule(:argument_error, exception, confidence: 85)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def self.match_type_error(exception)
|
|
227
|
+
from_rule(:type_error, exception, confidence: 85)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def self.match_zero_division_error(exception)
|
|
231
|
+
from_rule(:zero_division_error, exception, confidence: 90)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def self.match_json_parse_error(exception)
|
|
235
|
+
from_rule(:json_parse_error, exception, confidence: 88)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.match_timeout_error(exception)
|
|
239
|
+
from_rule(:timeout_error, exception, confidence: 84)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def self.match_frozen_error(exception)
|
|
243
|
+
from_rule(:frozen_error, exception, confidence: 87)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def self.match_runtime_error(exception)
|
|
247
|
+
from_rule(:runtime_error, exception, confidence: 80)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def self.match_generic_exception(exception)
|
|
251
|
+
from_rule(:generic_exception, exception, confidence: 75)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def self.build_suggestion(exception:, issue:, root_cause:, fixes:, confidence:)
|
|
255
|
+
Suggestion.new(
|
|
256
|
+
issue: issue,
|
|
257
|
+
location: location_for(exception),
|
|
258
|
+
root_cause: root_cause,
|
|
259
|
+
fixes: fixes,
|
|
260
|
+
confidence: confidence
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def self.location_for(exception)
|
|
265
|
+
return exception.location if exception.respond_to?(:location) && !exception.location.to_s.strip.empty?
|
|
266
|
+
|
|
267
|
+
frame = TraceCleaner.first_application_frame(exception.backtrace) || exception.backtrace&.first
|
|
268
|
+
frame || Bugsage.t("common.unknown")
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsage
|
|
4
|
+
class SessionClear
|
|
5
|
+
extend JsonEndpoint
|
|
6
|
+
|
|
7
|
+
ENDPOINT = "/bugsage/clear"
|
|
8
|
+
|
|
9
|
+
def self.handle_request(env)
|
|
10
|
+
return not_found unless Bugsage.configuration.enabled?
|
|
11
|
+
|
|
12
|
+
Store.clear!
|
|
13
|
+
ConsoleContext.clear!
|
|
14
|
+
AiContext.clear!
|
|
15
|
+
|
|
16
|
+
redirect_to = "/bugsage"
|
|
17
|
+
|
|
18
|
+
if json_request?(env)
|
|
19
|
+
json_response({ ok: true, message: Bugsage.t("errors.session_logs_cleared") })
|
|
20
|
+
else
|
|
21
|
+
redirect_response(redirect_to)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.json_request?(env)
|
|
26
|
+
accept = env["HTTP_ACCEPT"].to_s
|
|
27
|
+
content_type = env["CONTENT_TYPE"].to_s
|
|
28
|
+
accept.include?("application/json") || content_type.include?("application/json")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.redirect_response(path)
|
|
32
|
+
[302, { "Location" => path, "Content-Type" => "text/plain" }, [Bugsage.t("errors.session_logs_cleared")]]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|