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,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugsage
|
|
6
|
+
module HttpErrorCapture
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def capture?(status, env)
|
|
10
|
+
return false unless status.to_i >= 400
|
|
11
|
+
return false if bugsage_path?(env)
|
|
12
|
+
return false if ExceptionSupport.extract(env)
|
|
13
|
+
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_exception(env, status, body)
|
|
18
|
+
body_text = body.to_s.strip
|
|
19
|
+
location = location_for(env)
|
|
20
|
+
message = build_message(status, body_text, location)
|
|
21
|
+
|
|
22
|
+
HttpResponseError.new(
|
|
23
|
+
status: status.to_i,
|
|
24
|
+
message: message,
|
|
25
|
+
response_body: body_text.empty? ? nil : body_text,
|
|
26
|
+
location: location
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def location_for(env)
|
|
31
|
+
params = env["action_dispatch.request.path_parameters"]
|
|
32
|
+
return Bugsage.t("common.unknown") unless params.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
controller = params[:controller] || params["controller"]
|
|
35
|
+
action = params[:action] || params["action"]
|
|
36
|
+
return Bugsage.t("common.unknown") if controller.to_s.strip.empty?
|
|
37
|
+
|
|
38
|
+
"#{format_controller_name(controller)}Controller##{action}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def format_controller_name(controller)
|
|
42
|
+
controller.to_s.split("/").map do |segment|
|
|
43
|
+
segment.split("_").map(&:capitalize).join
|
|
44
|
+
end.join("::")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_message(status, body, location)
|
|
48
|
+
status_label = http_status_label(status)
|
|
49
|
+
detail = extract_detail(body)
|
|
50
|
+
base = Bugsage.t("http_errors.message.base", status: status, status_label: status_label)
|
|
51
|
+
unknown = Bugsage.t("common.unknown")
|
|
52
|
+
base += Bugsage.t("http_errors.message.at_location", location: location) unless location == unknown
|
|
53
|
+
detail.empty? ? base : "#{base}#{Bugsage.t("http_errors.message.with_detail", detail: detail)}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def extract_detail(body)
|
|
57
|
+
return "" if body.to_s.strip.empty?
|
|
58
|
+
|
|
59
|
+
payload = JSON.parse(body)
|
|
60
|
+
return body.strip unless payload.is_a?(Hash)
|
|
61
|
+
|
|
62
|
+
detail = payload["error"] || payload["message"] || payload["errors"]
|
|
63
|
+
case detail
|
|
64
|
+
when String
|
|
65
|
+
detail.strip
|
|
66
|
+
when Hash
|
|
67
|
+
detail.map { |key, value| "#{key}: #{value}" }.join(", ")
|
|
68
|
+
when Array
|
|
69
|
+
detail.join(", ")
|
|
70
|
+
else
|
|
71
|
+
body.strip
|
|
72
|
+
end
|
|
73
|
+
rescue JSON::ParserError
|
|
74
|
+
body.strip
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def bugsage_path?(env)
|
|
78
|
+
path = env["PATH_INFO"].to_s
|
|
79
|
+
path.start_with?("/bugsage")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def http_status_label(status)
|
|
83
|
+
if defined?(Rack::Utils)
|
|
84
|
+
Rack::Utils::HTTP_STATUS_CODES[status.to_i] || Bugsage.t("http_errors.status_label_fallback", status: status)
|
|
85
|
+
else
|
|
86
|
+
Bugsage.t("http_errors.status_label_fallback", status: status)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fixes_for_status(status)
|
|
91
|
+
fixes = Bugsage.t("http_errors.fixes.#{status.to_i}", default: nil)
|
|
92
|
+
fixes = Bugsage.t("http_errors.fixes.default", default: nil) if fixes.nil?
|
|
93
|
+
Array(fixes)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def confidence_for_status(status)
|
|
97
|
+
case status.to_i
|
|
98
|
+
when 400, 401, 422 then 86
|
|
99
|
+
when 403, 404 then 84
|
|
100
|
+
else 80
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsage
|
|
4
|
+
class HttpResponseError < StandardError
|
|
5
|
+
attr_reader :status, :response_body, :location
|
|
6
|
+
|
|
7
|
+
def initialize(status:, message:, response_body: nil, location: nil)
|
|
8
|
+
@status = status
|
|
9
|
+
@response_body = response_body
|
|
10
|
+
@location = location
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugsage
|
|
6
|
+
class InlineConsole
|
|
7
|
+
extend JsonEndpoint
|
|
8
|
+
|
|
9
|
+
def self.evaluate(code)
|
|
10
|
+
return error_response("Console is not available for this error.") unless ConsoleContext.available?
|
|
11
|
+
|
|
12
|
+
stripped = code.to_s.strip
|
|
13
|
+
return error_response("Enter a Ruby expression to evaluate.") if stripped.empty?
|
|
14
|
+
|
|
15
|
+
binding = ConsoleContext.binding_for_eval
|
|
16
|
+
result = eval(stripped, binding, "(bugsage-console)", 1) # rubocop:disable Security/Eval
|
|
17
|
+
|
|
18
|
+
success_response(result)
|
|
19
|
+
rescue SyntaxError, StandardError => e
|
|
20
|
+
error_response(format_exception(e))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.handle_request(env)
|
|
24
|
+
return not_found unless Bugsage.configuration.show_inline_console?
|
|
25
|
+
|
|
26
|
+
payload = parse_request_body(env)
|
|
27
|
+
return json_response(error_response("Console is not available for this error.")) unless prepare_context!(payload)
|
|
28
|
+
|
|
29
|
+
response = evaluate(payload["code"])
|
|
30
|
+
json_response(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.prepare_context!(payload)
|
|
34
|
+
return true unless payload.key?("index")
|
|
35
|
+
|
|
36
|
+
load_context_for_index(payload["index"])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.load_context_for_index(index)
|
|
40
|
+
event = Store.all[index.to_i]
|
|
41
|
+
ConsoleContext.load_from_event(event)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.render_panel(bug_index: nil, include_script: true)
|
|
45
|
+
return "" unless Bugsage.configuration.show_inline_console?
|
|
46
|
+
|
|
47
|
+
suffix = bug_index.nil? ? "" : "-bug-#{bug_index}"
|
|
48
|
+
output_id = "bugsage-console-output#{suffix}"
|
|
49
|
+
input_id = "bugsage-console-input#{suffix}"
|
|
50
|
+
index_attr = bug_index.nil? ? "" : %( data-bug-index="#{bug_index}")
|
|
51
|
+
|
|
52
|
+
<<~HTML
|
|
53
|
+
<div class="section inline-console">
|
|
54
|
+
<div class="label">Inline Rails Console</div>
|
|
55
|
+
<p class="console-help">
|
|
56
|
+
Evaluate Ruby in the context of this error. Available locals:
|
|
57
|
+
<code>exception</code>, <code>request_context</code>, <code>params</code>
|
|
58
|
+
</p>
|
|
59
|
+
<form
|
|
60
|
+
class="console-form bugsage-console-form"
|
|
61
|
+
data-output-target="#{output_id}"
|
|
62
|
+
data-input-target="#{input_id}"#{index_attr}
|
|
63
|
+
>
|
|
64
|
+
<label class="console-prompt" for="#{input_id}">>></label>
|
|
65
|
+
<input
|
|
66
|
+
id="#{input_id}"
|
|
67
|
+
class="console-input"
|
|
68
|
+
type="text"
|
|
69
|
+
name="code"
|
|
70
|
+
autocomplete="off"
|
|
71
|
+
spellcheck="false"
|
|
72
|
+
placeholder="exception.class"
|
|
73
|
+
/>
|
|
74
|
+
<button type="submit" class="console-submit">Run</button>
|
|
75
|
+
</form>
|
|
76
|
+
<div id="#{output_id}" class="console-output" aria-live="polite"></div>
|
|
77
|
+
</div>
|
|
78
|
+
#{render_script if include_script}
|
|
79
|
+
HTML
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def self.styles
|
|
83
|
+
<<~CSS
|
|
84
|
+
.inline-console {
|
|
85
|
+
background: #313244;
|
|
86
|
+
border: 1px solid #45475a;
|
|
87
|
+
border-radius: 8px;
|
|
88
|
+
padding: 16px;
|
|
89
|
+
margin-bottom: 20px;
|
|
90
|
+
}
|
|
91
|
+
.console-help {
|
|
92
|
+
color: #a6adc8;
|
|
93
|
+
font-size: 13px;
|
|
94
|
+
margin: 0 0 12px 0;
|
|
95
|
+
}
|
|
96
|
+
.console-help code {
|
|
97
|
+
color: #89b4fa;
|
|
98
|
+
}
|
|
99
|
+
.console-output {
|
|
100
|
+
background: #1e1e2e;
|
|
101
|
+
border: 1px solid #45475a;
|
|
102
|
+
border-radius: 6px;
|
|
103
|
+
min-height: 72px;
|
|
104
|
+
max-height: 260px;
|
|
105
|
+
overflow-y: auto;
|
|
106
|
+
padding: 12px;
|
|
107
|
+
margin-top: 12px;
|
|
108
|
+
}
|
|
109
|
+
.console-output:empty {
|
|
110
|
+
display: none;
|
|
111
|
+
}
|
|
112
|
+
.console-line {
|
|
113
|
+
white-space: pre-wrap;
|
|
114
|
+
margin-bottom: 6px;
|
|
115
|
+
font-size: 13px;
|
|
116
|
+
}
|
|
117
|
+
.console-line.input { color: #cdd6f4; }
|
|
118
|
+
.console-line.result { color: #a6e3a1; }
|
|
119
|
+
.console-line.error { color: #f38ba8; }
|
|
120
|
+
.console-form {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
}
|
|
125
|
+
.console-prompt {
|
|
126
|
+
color: #a6e3a1;
|
|
127
|
+
font-weight: bold;
|
|
128
|
+
}
|
|
129
|
+
.console-input {
|
|
130
|
+
flex: 1;
|
|
131
|
+
background: #1e1e2e;
|
|
132
|
+
color: #cdd6f4;
|
|
133
|
+
border: 1px solid #45475a;
|
|
134
|
+
border-radius: 6px;
|
|
135
|
+
padding: 10px 12px;
|
|
136
|
+
font-family: inherit;
|
|
137
|
+
font-size: 13px;
|
|
138
|
+
}
|
|
139
|
+
.console-input:focus {
|
|
140
|
+
outline: none;
|
|
141
|
+
border-color: #89b4fa;
|
|
142
|
+
}
|
|
143
|
+
.console-submit {
|
|
144
|
+
background: #89b4fa;
|
|
145
|
+
color: #1e1e2e;
|
|
146
|
+
border: 0;
|
|
147
|
+
border-radius: 6px;
|
|
148
|
+
padding: 10px 14px;
|
|
149
|
+
font-family: inherit;
|
|
150
|
+
font-size: 13px;
|
|
151
|
+
font-weight: bold;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
}
|
|
154
|
+
CSS
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def self.render_script
|
|
158
|
+
<<~HTML
|
|
159
|
+
<script>
|
|
160
|
+
(function () {
|
|
161
|
+
if (window.__bugsageConsoleInitialized) return;
|
|
162
|
+
window.__bugsageConsoleInitialized = true;
|
|
163
|
+
|
|
164
|
+
function appendLine(output, text, type) {
|
|
165
|
+
var line = document.createElement("div");
|
|
166
|
+
line.className = "console-line" + (type ? " " + type : "");
|
|
167
|
+
line.textContent = text;
|
|
168
|
+
output.appendChild(line);
|
|
169
|
+
output.scrollTop = output.scrollHeight;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
document.addEventListener("submit", function (event) {
|
|
173
|
+
var form = event.target;
|
|
174
|
+
if (!form.classList || !form.classList.contains("bugsage-console-form")) return;
|
|
175
|
+
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
var input = document.getElementById(form.dataset.inputTarget);
|
|
178
|
+
var output = document.getElementById(form.dataset.outputTarget);
|
|
179
|
+
if (!input || !output) return;
|
|
180
|
+
|
|
181
|
+
var code = input.value.trim();
|
|
182
|
+
if (!code) return;
|
|
183
|
+
|
|
184
|
+
appendLine(output, ">> " + code, "input");
|
|
185
|
+
input.value = "";
|
|
186
|
+
|
|
187
|
+
var payload = { code: code };
|
|
188
|
+
if (form.dataset.bugIndex !== undefined) {
|
|
189
|
+
payload.index = parseInt(form.dataset.bugIndex, 10);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fetch("/bugsage/console", {
|
|
193
|
+
method: "POST",
|
|
194
|
+
headers: { "Content-Type": "application/json" },
|
|
195
|
+
body: JSON.stringify(payload)
|
|
196
|
+
})
|
|
197
|
+
.then(function (response) { return response.json(); })
|
|
198
|
+
.then(function (result) {
|
|
199
|
+
appendLine(output, result.output, result.ok ? "result" : "error");
|
|
200
|
+
})
|
|
201
|
+
.catch(function (error) {
|
|
202
|
+
appendLine(output, error.message, "error");
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
})();
|
|
206
|
+
</script>
|
|
207
|
+
HTML
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.success_response(result)
|
|
211
|
+
{ ok: true, output: "=> #{format_value(result)}" }
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def self.error_response(message)
|
|
215
|
+
{ ok: false, output: message.to_s }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def self.format_value(value)
|
|
219
|
+
value.inspect
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def self.format_exception(exception)
|
|
223
|
+
"#{exception.class}: #{exception.message}"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bugsage
|
|
4
|
+
# Single source of truth for installing BugSage in a Rails application.
|
|
5
|
+
# Referenced by the README, install generator, CLI, and initializer template.
|
|
6
|
+
module Installation
|
|
7
|
+
STEPS = [
|
|
8
|
+
{
|
|
9
|
+
title: "Add the gem to your Gemfile",
|
|
10
|
+
command: 'gem "bugsage"',
|
|
11
|
+
note: "Use a git/path source while developing locally if needed."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
title: "Install dependencies",
|
|
15
|
+
command: "bundle install",
|
|
16
|
+
note: nil
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: "Start your Rails server",
|
|
20
|
+
command: "bin/rails server",
|
|
21
|
+
note: "BugSage auto-wires via Bugsage::Railtie — no routes.rb or middleware changes required."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
title: "Trigger an error to verify",
|
|
25
|
+
command: "Visit any route that raises an exception in development.",
|
|
26
|
+
note: "You should see the BugSage error page instead of the default Rails page."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
title: "Open the session dashboard (optional)",
|
|
30
|
+
command: "http://localhost:3000/bugsage",
|
|
31
|
+
note: "Lists all errors caught during the current server session."
|
|
32
|
+
}
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
OPTIONAL_STEPS = [
|
|
36
|
+
{
|
|
37
|
+
title: "Generate a commented initializer for custom overrides",
|
|
38
|
+
command: "bundle exec rails generate bugsage:install",
|
|
39
|
+
alternative: "bundle exec bugsage install"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
title: "Enable AI-enhanced suggestions (optional)",
|
|
43
|
+
command: "export OPENAI_API_KEY=sk-your-key",
|
|
44
|
+
alternative: "export CURSOR_API_KEY=crsr_your-key",
|
|
45
|
+
note: "BugSage auto-detects the provider from the key prefix on boot."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
title: "Control enabled environments (optional)",
|
|
49
|
+
command: "export BUGSAGE_ENABLED_ENVIRONMENTS=development,test,staging",
|
|
50
|
+
note: "Defaults to development and test."
|
|
51
|
+
}
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
AUTO_WIRED = [
|
|
55
|
+
"Exception capture middleware",
|
|
56
|
+
"BugSage HTML error pages in development",
|
|
57
|
+
"Session dashboard at /bugsage",
|
|
58
|
+
"Inline Rails console at /bugsage/console",
|
|
59
|
+
"Routing error capture via exceptions_app",
|
|
60
|
+
"AI provider auto-detection when API keys are present"
|
|
61
|
+
].freeze
|
|
62
|
+
|
|
63
|
+
module_function
|
|
64
|
+
|
|
65
|
+
def guide_lines
|
|
66
|
+
lines = ["BugSage — Rails installation steps", ""]
|
|
67
|
+
|
|
68
|
+
STEPS.each_with_index do |step, index|
|
|
69
|
+
lines << "#{index + 1}. #{step[:title]}"
|
|
70
|
+
lines << " #{step[:command]}"
|
|
71
|
+
lines << " #{step[:note]}" if step[:note]
|
|
72
|
+
lines << ""
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
lines << "Optional:"
|
|
76
|
+
OPTIONAL_STEPS.each_with_index do |step, index|
|
|
77
|
+
lines << "#{index + 1}. #{step[:title]}"
|
|
78
|
+
lines << " #{step[:command]}"
|
|
79
|
+
lines << " or: #{step[:alternative]}" if step[:alternative]
|
|
80
|
+
lines << " #{step[:note]}" if step[:note]
|
|
81
|
+
lines << ""
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
lines << "Auto-wired on boot (no manual setup):"
|
|
85
|
+
AUTO_WIRED.each { |item| lines << " - #{item}" }
|
|
86
|
+
|
|
87
|
+
lines
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def print_guide
|
|
91
|
+
guide_lines.each { |line| puts line }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Bugsage
|
|
6
|
+
class Installer
|
|
7
|
+
INITIALIZER_PATH = "config/initializers/bugsage.rb"
|
|
8
|
+
TEMPLATE_PATH = File.expand_path("../../generators/bugsage/install/templates/bugsage.rb", __dir__)
|
|
9
|
+
|
|
10
|
+
def self.run(destination: Dir.pwd, force: false)
|
|
11
|
+
new(destination: destination, force: force).run
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(destination:, force: false)
|
|
15
|
+
@destination = File.expand_path(destination)
|
|
16
|
+
@force = force
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
validate_rails_app!
|
|
21
|
+
|
|
22
|
+
path = File.join(@destination, INITIALIZER_PATH)
|
|
23
|
+
created_initializer = write_initializer(path)
|
|
24
|
+
config = Bugsage::AutoConfigurator.apply!
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
initializer_path: path,
|
|
28
|
+
created_initializer: created_initializer,
|
|
29
|
+
summary: Bugsage::AutoConfigurator.summary(config)
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def validate_rails_app!
|
|
36
|
+
application_file = File.join(@destination, "config/application.rb")
|
|
37
|
+
return if File.exist?(application_file)
|
|
38
|
+
|
|
39
|
+
raise Error, "Could not find a Rails app at #{@destination}. Run this from your app root."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write_initializer(path)
|
|
43
|
+
return false if File.exist?(path) && !@force
|
|
44
|
+
|
|
45
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
46
|
+
File.write(path, initializer_template)
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def initializer_template
|
|
51
|
+
if File.exist?(TEMPLATE_PATH)
|
|
52
|
+
File.read(TEMPLATE_PATH)
|
|
53
|
+
else
|
|
54
|
+
default_initializer_template
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def default_initializer_template
|
|
59
|
+
<<~RUBY
|
|
60
|
+
# frozen_string_literal: true
|
|
61
|
+
|
|
62
|
+
# See Bugsage::Installation or run: bundle exec bugsage install --guide-only
|
|
63
|
+
RUBY
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Bugsage
|
|
6
|
+
# Shared Rack JSON helpers for BugSage HTTP endpoints.
|
|
7
|
+
# Extend on endpoint classes: `extend JsonEndpoint`
|
|
8
|
+
module JsonEndpoint
|
|
9
|
+
def parse_request_body(env)
|
|
10
|
+
body = env["rack.input"]
|
|
11
|
+
raw = body.respond_to?(:read) ? body.read : body.to_s
|
|
12
|
+
body.rewind if body.respond_to?(:rewind)
|
|
13
|
+
|
|
14
|
+
return {} if raw.to_s.strip.empty?
|
|
15
|
+
|
|
16
|
+
JSON.parse(raw)
|
|
17
|
+
rescue JSON::ParserError
|
|
18
|
+
{}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def json_response(payload = nil, status: 200, **attrs)
|
|
22
|
+
body = payload.nil? ? attrs : payload
|
|
23
|
+
[status, { "Content-Type" => "application/json" }, [JSON.generate(body)]]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def error_response(message)
|
|
27
|
+
{ ok: false, error: message.to_s }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def not_found
|
|
31
|
+
[404, { "Content-Type" => "text/plain" }, [Bugsage.t("common.not_found")]]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|