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.
Files changed (69) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +442 -0
  3. data/CHANGELOG.md +28 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +301 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +344 -0
  8. data/ROADMAP.md +217 -0
  9. data/SECURITY.md +83 -0
  10. data/docs/AI.md +167 -0
  11. data/docs/Configuration.md +168 -0
  12. data/docs/GettingStarted.md +146 -0
  13. data/docs/RELEASE_CHECKLIST.md +346 -0
  14. data/docs/Troubleshooting.md +181 -0
  15. data/docs/images/BadRequest.png +0 -0
  16. data/docs/images/BadRequest2.png +0 -0
  17. data/docs/images/BugSage_Logo.png +0 -0
  18. data/docs/images/BugSage_Social_Preview.png +0 -0
  19. data/docs/images/NoMethodError.png +0 -0
  20. data/docs/images/NoMethodError2.png +0 -0
  21. data/docs/images/README.md +38 -0
  22. data/docs/releases/README.md +21 -0
  23. data/docs/releases/v0.2.0.md +40 -0
  24. data/exe/bugsage +7 -0
  25. data/lib/bugsage/ai_analyzer.rb +145 -0
  26. data/lib/bugsage/ai_chat.rb +388 -0
  27. data/lib/bugsage/ai_context.rb +69 -0
  28. data/lib/bugsage/ai_panel.rb +708 -0
  29. data/lib/bugsage/ai_support.rb +36 -0
  30. data/lib/bugsage/auto_configurator.rb +97 -0
  31. data/lib/bugsage/cli.rb +59 -0
  32. data/lib/bugsage/code_context.rb +81 -0
  33. data/lib/bugsage/code_patch.rb +147 -0
  34. data/lib/bugsage/configuration.rb +149 -0
  35. data/lib/bugsage/console_context.rb +51 -0
  36. data/lib/bugsage/cursor_client.rb +165 -0
  37. data/lib/bugsage/dashboard.rb +627 -0
  38. data/lib/bugsage/editor_links.rb +34 -0
  39. data/lib/bugsage/error_page.rb +298 -0
  40. data/lib/bugsage/exception_handler.rb +66 -0
  41. data/lib/bugsage/exception_support.rb +95 -0
  42. data/lib/bugsage/exceptions_app.rb +31 -0
  43. data/lib/bugsage/fix_applicator.rb +107 -0
  44. data/lib/bugsage/formatter.rb +37 -0
  45. data/lib/bugsage/http_error_capture.rb +104 -0
  46. data/lib/bugsage/http_response_error.rb +14 -0
  47. data/lib/bugsage/inline_console.rb +226 -0
  48. data/lib/bugsage/installation.rb +94 -0
  49. data/lib/bugsage/installer.rb +66 -0
  50. data/lib/bugsage/json_endpoint.rb +34 -0
  51. data/lib/bugsage/locales/en.yml +439 -0
  52. data/lib/bugsage/middleware.rb +152 -0
  53. data/lib/bugsage/openai_client.rb +103 -0
  54. data/lib/bugsage/page_actions.rb +255 -0
  55. data/lib/bugsage/railtie.rb +49 -0
  56. data/lib/bugsage/request_context.rb +56 -0
  57. data/lib/bugsage/rule.rb +271 -0
  58. data/lib/bugsage/session_clear.rb +35 -0
  59. data/lib/bugsage/store.rb +60 -0
  60. data/lib/bugsage/suggestion.rb +58 -0
  61. data/lib/bugsage/trace_cleaner.rb +24 -0
  62. data/lib/bugsage/translations.rb +85 -0
  63. data/lib/bugsage/version.rb +5 -0
  64. data/lib/bugsage.rb +65 -0
  65. data/lib/generators/bugsage/install/install_generator.rb +21 -0
  66. data/lib/generators/bugsage/install/templates/bugsage.rb +22 -0
  67. data/scripts/publish-github-release.sh +38 -0
  68. data/sig/bugsage.rbs +4 -0
  69. 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
@@ -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