rubber_duck 0.1.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/MIT-LICENSE +20 -0
- data/README.md +63 -0
- data/Rakefile +8 -0
- data/app/assets/images/rubber_duck/v-0-example.png +0 -0
- data/app/assets/javascripts/rubber_duck/modal.js +83 -0
- data/app/assets/stylesheets/rubber_duck/application.css +15 -0
- data/app/controllers/rubber_duck/application_controller.rb +4 -0
- data/app/controllers/rubber_duck/errors_controller.rb +18 -0
- data/app/helpers/rubber_duck/application_helper.rb +4 -0
- data/app/helpers/rubber_duck/errors_helper.rb +4 -0
- data/app/jobs/rubber_duck/application_job.rb +4 -0
- data/app/mailers/rubber_duck/application_mailer.rb +6 -0
- data/app/models/rubber_duck/application_record.rb +5 -0
- data/app/services/rubber_duck/ai_service.rb +72 -0
- data/app/views/layouts/rubber_duck/application.html.erb +17 -0
- data/app/views/rubber_duck/_button.html.erb +11 -0
- data/config/initializers/rubber_duck.rb +13 -0
- data/config/routes.rb +3 -0
- data/lib/generators/rubber_duck/install_generator.rb +40 -0
- data/lib/rubber_duck/configuration.rb +26 -0
- data/lib/rubber_duck/engine.rb +13 -0
- data/lib/rubber_duck/middleware.rb +140 -0
- data/lib/rubber_duck/version.rb +3 -0
- data/lib/rubber_duck.rb +8 -0
- data/lib/tasks/rubber_duck_tasks.rake +4 -0
- metadata +123 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 171bf433b6ed6a157adb397cb9e365b12531ef1631b06901282b870a2c9fafda
|
|
4
|
+
data.tar.gz: b2ad788c01a589196d6b502e87653935d15b04d569a78c750881b56189ff146f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: b092243fc9b50c76878a8a6b396888e39e856d0e9a814be6f258dcd9b4de04f773695f4ddca00bbce89d9e304b3d686c30cb62485ac183a939a7c474f3b1b952
|
|
7
|
+
data.tar.gz: b13dca3ab57855361634f8ccf5741386f260e778dfaccccd4a8ce7f2db44d0cf08d64599b701762f1869f782a821aae19be78a7f64ddf6fa737e9a720e2d7ef4
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Emmanuel Vernet
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# RubberDuck 🦆
|
|
2
|
+
|
|
3
|
+
RubberDuck is a Rails engine that provides an AI-powered debugging assistant directly on your native Rails development error pages.
|
|
4
|
+
When an error occurs, a rubber duck button is injected into the page.
|
|
5
|
+
Clicking it sends error details including log lines and http error to an AI service and displays a contextualized explanation in a modal.
|
|
6
|
+
|
|
7
|
+
Here's an example of the view it can generate currently
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
- this gem runs automatically when a standard rails error page shows. No rails commands needed!
|
|
13
|
+
|
|
14
|
+
> ⚠️ **Warning**
|
|
15
|
+
>
|
|
16
|
+
> Be aware that this gem uses your code and server logs to be sent to the AI model. Do not hardcode sensitive values such as tokens!!
|
|
17
|
+
However, the gem will not run in production, only in development
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
Add this line to your application's Gemfile:
|
|
21
|
+
|
|
22
|
+
> ⚠️ **Warning**
|
|
23
|
+
>
|
|
24
|
+
> Add this to your development gems. It will not run in production environments!
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
gem "rubber_duck"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
And then execute:
|
|
31
|
+
```ruby
|
|
32
|
+
bundle install
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install it yourself as:
|
|
36
|
+
```ruby
|
|
37
|
+
gem install rubber_duck
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Run the install generator to create the initializer file:
|
|
41
|
+
```ruby
|
|
42
|
+
rails generate rubber_duck:install
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
Before using the gem, you need to configure your AI service API key in the initializer file located at `config/initializers/rubber_duck.rb`.
|
|
47
|
+
```ruby
|
|
48
|
+
config/initializers/rubber_duck.rb
|
|
49
|
+
RubberDuck.configure do |config|
|
|
50
|
+
# Set your OpenAI API key
|
|
51
|
+
config.openai_api_key = ENV['OPENAI_API_KEY']
|
|
52
|
+
|
|
53
|
+
# (Optional) Specify a different AI model to use.
|
|
54
|
+
config.model = 'gpt-4'
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
Ensure you have set the `OPENAI_API_KEY` environment variable in your development environment.
|
|
58
|
+
|
|
59
|
+
## Contributing
|
|
60
|
+
To be determined...
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
Binary file
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// force align button in DOM
|
|
2
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
3
|
+
const h1 = document.querySelector("h1");
|
|
4
|
+
const duck = document.getElementById("rubber-duck-container");
|
|
5
|
+
|
|
6
|
+
if (!h1 || !duck) return;
|
|
7
|
+
|
|
8
|
+
h1.style.display = "flex";
|
|
9
|
+
h1.style.alignItems = "center";
|
|
10
|
+
|
|
11
|
+
h1.appendChild(duck);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// create modal & API call
|
|
15
|
+
(function () {
|
|
16
|
+
// 1. Load Marked (Markdown) and Prism (Syntax Highlighting) libs
|
|
17
|
+
const libs = [
|
|
18
|
+
"https://cdn.jsdelivr.net/npm/marked/marked.min.js",
|
|
19
|
+
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js",
|
|
20
|
+
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/components/prism-ruby.min.js", // Support for Ruby
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
libs.forEach((src) => {
|
|
24
|
+
const script = document.createElement("script");
|
|
25
|
+
script.src = src;
|
|
26
|
+
document.head.appendChild(script);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// 2. Load Prism CSS for the theme
|
|
30
|
+
const link = document.createElement("link");
|
|
31
|
+
link.rel = "stylesheet";
|
|
32
|
+
link.href =
|
|
33
|
+
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css";
|
|
34
|
+
document.head.appendChild(link);
|
|
35
|
+
|
|
36
|
+
const button = document.getElementById("rubber-duck-button");
|
|
37
|
+
const modal = document.getElementById("rubber-duck-modal");
|
|
38
|
+
const overlay = document.getElementById("rubber-duck-overlay");
|
|
39
|
+
const closeBtn = document.getElementById("rubber-duck-close");
|
|
40
|
+
const content = document.getElementById("rubber-duck-content");
|
|
41
|
+
|
|
42
|
+
button.addEventListener("click", async () => {
|
|
43
|
+
modal.style.display = "block";
|
|
44
|
+
overlay.style.display = "block";
|
|
45
|
+
content.innerHTML = "Analyzing error... This may take a few seconds.";
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const response = await fetch("/rubber_duck/analyze_error", {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "Content-Type": "application/json" },
|
|
51
|
+
body: JSON.stringify(errorData),
|
|
52
|
+
});
|
|
53
|
+
const result = await response.json();
|
|
54
|
+
|
|
55
|
+
if (result.success) {
|
|
56
|
+
content.innerHTML = window.marked.parse(result.response);
|
|
57
|
+
setTimeout(() => {
|
|
58
|
+
if (window.Prism) {
|
|
59
|
+
window.Prism.highlightAllUnder(content);
|
|
60
|
+
}
|
|
61
|
+
}, 100);
|
|
62
|
+
} else {
|
|
63
|
+
content.innerHTML =
|
|
64
|
+
'<span style="color: #DC2626;">Error: ' +
|
|
65
|
+
(result.error || "Unknown error") +
|
|
66
|
+
"</span>";
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
content.innerHTML =
|
|
70
|
+
'<span style="color: #DC2626;">Failed to connect: ' +
|
|
71
|
+
error.message +
|
|
72
|
+
"</span>";
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
function closeModal() {
|
|
77
|
+
modal.style.display = "none";
|
|
78
|
+
overlay.style.display = "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
closeBtn.addEventListener("click", closeModal);
|
|
82
|
+
overlay.addEventListener("click", closeModal);
|
|
83
|
+
})();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module RubberDuck
|
|
2
|
+
class ErrorsController < ApplicationController
|
|
3
|
+
skip_before_action :verify_authenticity_token, only: [:analyze]
|
|
4
|
+
|
|
5
|
+
def analyze
|
|
6
|
+
unless Rails.env.development?
|
|
7
|
+
return render json: { error: "Only available in development" }, status: :forbidden
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
result = AiService.analyze_error(
|
|
11
|
+
exception_message: params[:exception],
|
|
12
|
+
backtrace: params[:backtrace],
|
|
13
|
+
logs: params[:logs]
|
|
14
|
+
)
|
|
15
|
+
render json: result
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
require "faraday"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
module RubberDuck
|
|
5
|
+
class AiService
|
|
6
|
+
class << self
|
|
7
|
+
def analyze_error(exception_message:, backtrace:, logs:)
|
|
8
|
+
return { error: "No API key configured" } unless RubberDuck.configuration.openai_api_key
|
|
9
|
+
prompt = build_prompt(exception_message, backtrace, logs)
|
|
10
|
+
|
|
11
|
+
begin
|
|
12
|
+
response = call_openai(prompt)
|
|
13
|
+
{ success: true, response: response }
|
|
14
|
+
rescue => e
|
|
15
|
+
{ success: false, error: e.message }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_prompt(exception, backtrace, logs)
|
|
22
|
+
<<~PROMPT
|
|
23
|
+
INSTRUCTIONS:
|
|
24
|
+
- Keep verbosity at the minimum.
|
|
25
|
+
- Keep your response concise and actionable.
|
|
26
|
+
- Always return every single code snippet inside triple backticks with a language tag.
|
|
27
|
+
- Do not add <ul> or <li> tags.
|
|
28
|
+
You are a helpful Ruby on Rails debugging assistant. A developer encountered this error:
|
|
29
|
+
ERROR: #{exception}
|
|
30
|
+
BACKTRACE:
|
|
31
|
+
#{backtrace&.first(10)&.join("\n") || "No backtrace available"}
|
|
32
|
+
RECENT LOGS:
|
|
33
|
+
#{logs || "No logs available"}
|
|
34
|
+
OBJECTIVE:
|
|
35
|
+
1. Explain what this error means in simple terms
|
|
36
|
+
2. Explain the HTTP error in simple terms if any available
|
|
37
|
+
3. Identify the likely causes
|
|
38
|
+
4. Suggest specific fixes
|
|
39
|
+
PROMPT
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def call_openai(prompt)
|
|
43
|
+
conn = Faraday.new(url: "https://api.openai.com") do |f|
|
|
44
|
+
f.request :json
|
|
45
|
+
f.response :json
|
|
46
|
+
f.adapter Faraday.default_adapter
|
|
47
|
+
end
|
|
48
|
+
response = conn.post("/v1/responses") do |req|
|
|
49
|
+
req.headers["Authorization"] = "Bearer #{RubberDuck.configuration.openai_api_key}"
|
|
50
|
+
req.headers["Content-Type"] = "application/json"
|
|
51
|
+
req.body = {
|
|
52
|
+
model: RubberDuck.configuration.model || "gpt-5-nano",
|
|
53
|
+
input: prompt,
|
|
54
|
+
text: {
|
|
55
|
+
format: {
|
|
56
|
+
type: "text"
|
|
57
|
+
},
|
|
58
|
+
verbosity: "low"
|
|
59
|
+
},
|
|
60
|
+
reasoning: {
|
|
61
|
+
effort: "low"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
## debug output
|
|
66
|
+
# puts "======> OpenAI Response Body: #{response.body.inspect}"
|
|
67
|
+
##
|
|
68
|
+
response.body.dig("output", 1, "content", 0, "text")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>Rubber duck</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "rubber_duck/application", media: "all" %>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
|
|
14
|
+
<%= yield %>
|
|
15
|
+
|
|
16
|
+
</body>
|
|
17
|
+
</html>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div id="rubber-duck-container" style="display: flex; margin-left: 20px;">
|
|
2
|
+
<button id="rubber-duck-button" style="background: #8f9cc9; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
|
|
3
|
+
🦆 RubberDuck
|
|
4
|
+
</button>
|
|
5
|
+
<div id="rubber-duck-modal" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 24px; border-radius: 12px; box-shadow: 0 20px 25px rgba(0,0,0,0.2); max-width: 600px; max-height: 80vh; overflow-y: auto; z-index: 10001;">
|
|
6
|
+
<h3 style="margin: 0 0 16px 0; color: #1F2937;"><%= @model_name.capitalize %> Analysis</h3>
|
|
7
|
+
<div id="rubber-duck-content" style="color: #4B5563; line-height: 1.6;">Analyzing...</div>
|
|
8
|
+
<button id="rubber-duck-close" style="margin-top: 16px; background: #E5E7EB; color: #374151; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer;">Close</button>
|
|
9
|
+
</div>
|
|
10
|
+
<div id="rubber-duck-overlay" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 10000;"></div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
RubberDuck.configure do |config|
|
|
2
|
+
# Get your API key from https://platform.openai.com
|
|
3
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
4
|
+
|
|
5
|
+
# Model to use
|
|
6
|
+
config.model = "gpt-4o-mini"
|
|
7
|
+
|
|
8
|
+
# Enable/disable the helper
|
|
9
|
+
config.enabled = true
|
|
10
|
+
|
|
11
|
+
# Number of log lines to send to AI for context
|
|
12
|
+
config.log_lines = 50
|
|
13
|
+
end
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
|
|
3
|
+
module RubberDuck
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("templates", __dir__)
|
|
7
|
+
|
|
8
|
+
def create_initializer_file
|
|
9
|
+
create_file "config/initializers/rubber_duck.rb", <<~CONTENT
|
|
10
|
+
RubberDuck.configure do |config|
|
|
11
|
+
# Get your API key from https://platform.openai.com
|
|
12
|
+
config.openai_api_key = ENV["OPENAI_API_KEY"]
|
|
13
|
+
|
|
14
|
+
# Model to use
|
|
15
|
+
config.model = "gpt-5-nano"
|
|
16
|
+
|
|
17
|
+
# Enable/disable the helper
|
|
18
|
+
config.enabled = true
|
|
19
|
+
|
|
20
|
+
# Number of log lines to send to AI for context
|
|
21
|
+
config.log_lines = 50
|
|
22
|
+
end
|
|
23
|
+
CONTENT
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def add_route
|
|
27
|
+
say "Adding engine mount point to routes.rb", :green
|
|
28
|
+
route "mount RubberDuck::Engine => '/rubber_duck'"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show_instructions
|
|
32
|
+
say "\n✅ RubberDuck installed!", :green
|
|
33
|
+
say "\nNext steps:"
|
|
34
|
+
say " 1. Add OPENAI_API_KEY to your .env file"
|
|
35
|
+
say " 2. Restart your Rails server"
|
|
36
|
+
say " 3. Trigger an error and look for the 'Ask AI' button\n"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module RubberDuck
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :openai_api_key, :model, :enabled, :log_lines
|
|
4
|
+
|
|
5
|
+
def initialize
|
|
6
|
+
@openai_api_key = nil
|
|
7
|
+
@model = "gpt-5-nano"
|
|
8
|
+
@enabled = true
|
|
9
|
+
@log_lines = 50
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
class << self
|
|
13
|
+
|
|
14
|
+
def configuration
|
|
15
|
+
@configuration ||= Configuration.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(configuration)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def reset_configuration!
|
|
23
|
+
@configuration = Configuration.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require "rubber_duck/middleware"
|
|
2
|
+
module RubberDuck
|
|
3
|
+
class Engine < ::Rails::Engine
|
|
4
|
+
isolate_namespace RubberDuck
|
|
5
|
+
# config.app_middleware.use RubberDuck::Middleware
|
|
6
|
+
config.app_middleware.insert_before ActionDispatch::ShowExceptions, RubberDuck::Middleware
|
|
7
|
+
|
|
8
|
+
# Middleware injects UI on error pages
|
|
9
|
+
# initializer "rubber_duck.middleware" do |app|
|
|
10
|
+
# app.middleware.use RubberDuck::Middleware if Rails.env.development?
|
|
11
|
+
# end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require "rack/utils"
|
|
2
|
+
require "erb"
|
|
3
|
+
|
|
4
|
+
module RubberDuck
|
|
5
|
+
class Middleware
|
|
6
|
+
def initialize(app)
|
|
7
|
+
@app = app
|
|
8
|
+
@model_name = RubberDuck.configuration.model
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(env)
|
|
12
|
+
status, headers, response = @app.call(env)
|
|
13
|
+
|
|
14
|
+
# Debug logging
|
|
15
|
+
# Rails.logger.info "RubberDuck: status=#{status}, content_type=#{headers['Content-Type']}, dev=#{Rails.env.development?}, enabled=#{RubberDuck.configuration.enabled}"
|
|
16
|
+
|
|
17
|
+
# Only intercept in development mode
|
|
18
|
+
if should_inject?(env, status, headers)
|
|
19
|
+
# Rails.logger.info "RubberDuck: Replacing response with custom error page"
|
|
20
|
+
return inject_button_response(env, status, headers, response)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
[ status, headers, response ]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def should_inject?(env, status, headers)
|
|
29
|
+
# Check if this is an error response
|
|
30
|
+
return false unless Rails.env.development?
|
|
31
|
+
return false unless RubberDuck.configuration.enabled
|
|
32
|
+
return false unless status >= 400
|
|
33
|
+
|
|
34
|
+
# ONLY inject if request for HTML
|
|
35
|
+
# accept_header = env["HTTP_ACCEPT"].to_s
|
|
36
|
+
# return false unless accept_header.include?("text/html")
|
|
37
|
+
|
|
38
|
+
# Ignore specific background noise
|
|
39
|
+
# path = env["PATH_INFO"].to_s
|
|
40
|
+
# return false if path.match?(/\.(ico|json|map|png|jpg|js|css)$/)
|
|
41
|
+
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def inject_button_response(env, status, headers, response)
|
|
46
|
+
# Read original response body
|
|
47
|
+
original_body = ""
|
|
48
|
+
response.each { |part| original_body << part }
|
|
49
|
+
response.close if response.respond_to?(:close)
|
|
50
|
+
|
|
51
|
+
# Rails.logger.info "RubberDuck: Original body size: #{original_body.bytesize}"
|
|
52
|
+
# Rails.logger.info "RubberDuck: Has </body>?: #{original_body.include?('</body>')}"
|
|
53
|
+
|
|
54
|
+
# Don't inject if not HTML
|
|
55
|
+
unless original_body.include?('<html') || original_body.include?('</body>')
|
|
56
|
+
# Rails.logger.info "RubberDuck: Not HTML, skipping injection"
|
|
57
|
+
return [ status, headers, [ original_body ] ]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
exception = env["action_dispatch.exception"]
|
|
61
|
+
modified_body = inject_button_into_html(original_body, exception, env, status)
|
|
62
|
+
|
|
63
|
+
# Rails.logger.info "RubberDuck: Modified body size: #{modified_body.bytesize}"
|
|
64
|
+
# Rails.logger.info "RubberDuck: Button injected?: #{modified_body.include?('rubber-duck-button')}"
|
|
65
|
+
|
|
66
|
+
headers["Content-Type"] = "text/html; charset=utf-8"
|
|
67
|
+
headers["Content-Length"] = modified_body.bytesize.to_s
|
|
68
|
+
headers["X-RubberDuck-Handled"] = "true"
|
|
69
|
+
|
|
70
|
+
[ status, headers, [ modified_body ] ]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inject_button_into_html(html, exception, env, status)
|
|
74
|
+
gem_path = Gem.loaded_specs['rubber_duck'].full_gem_path
|
|
75
|
+
# button_html = ApplicationController.render(
|
|
76
|
+
# partial: "rubber_duck/button",
|
|
77
|
+
# locals: {
|
|
78
|
+
# model_name: @model_name
|
|
79
|
+
# }
|
|
80
|
+
# )
|
|
81
|
+
partial_path = File.join(gem_path, 'app/views/rubber_duck/_button.html.erb')
|
|
82
|
+
partial_content = File.read(partial_path)
|
|
83
|
+
button_html = ERB.new(partial_content).result(binding)
|
|
84
|
+
|
|
85
|
+
logs = capture_logs
|
|
86
|
+
error_data_script = build_error_data_script(exception, env, status, logs)
|
|
87
|
+
|
|
88
|
+
# Load JS from file
|
|
89
|
+
modal_js = File.read(File.join(gem_path, 'app/assets/javascripts/rubber_duck/modal.js'))
|
|
90
|
+
|
|
91
|
+
injection = <<~HTML
|
|
92
|
+
<div>
|
|
93
|
+
#{button_html}
|
|
94
|
+
</div>
|
|
95
|
+
<script>
|
|
96
|
+
#{error_data_script}
|
|
97
|
+
#{modal_js}
|
|
98
|
+
</script>
|
|
99
|
+
HTML
|
|
100
|
+
|
|
101
|
+
# Inject before </body> if exists, otherwise append
|
|
102
|
+
if html =~ /<\/header>/i
|
|
103
|
+
html.sub(/<\/header>/i, "#{injection}</header>")
|
|
104
|
+
else
|
|
105
|
+
html + injection
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_error_data_script(exception, env, status, logs)
|
|
110
|
+
# Prepare the data object in Ruby
|
|
111
|
+
data = if exception
|
|
112
|
+
{
|
|
113
|
+
exception: exception.message,
|
|
114
|
+
backtrace: exception.backtrace&.first(10) || [],
|
|
115
|
+
logs: logs
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
{
|
|
119
|
+
status: status,
|
|
120
|
+
path: env["PATH_INFO"],
|
|
121
|
+
logs: logs
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Inject it as a single, globally accessible JSON object
|
|
126
|
+
<<~JS
|
|
127
|
+
window.errorData = #{data.to_json};
|
|
128
|
+
JS
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def capture_logs
|
|
132
|
+
log_file = Rails.root.join("log", "development.log")
|
|
133
|
+
return "Logs not available" unless File.exist?(log_file)
|
|
134
|
+
lines = File.readlines(log_file).last(RubberDuck.configuration.log_lines)
|
|
135
|
+
lines.join
|
|
136
|
+
rescue => e
|
|
137
|
+
"Error reading logs: #{e.message}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/rubber_duck.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rubber_duck
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Emmanuel Vernet
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 8.0.2.1
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - "~>"
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.0'
|
|
29
|
+
- - ">="
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: 8.0.2.1
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '2.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '2.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: dotenv-rails
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
description: This gem allows the Rails developer to avoid switching context from Rails
|
|
61
|
+
error pages during development. When getting an error, you can send the error and
|
|
62
|
+
logs to an AI model of your choice and get a response to help you understand or
|
|
63
|
+
pin point the issue while avoiding copy pasting code or logs into an external AI
|
|
64
|
+
window. Perfect for those who prefer to code with minimal AI presence in their editor
|
|
65
|
+
of choice!
|
|
66
|
+
email:
|
|
67
|
+
- vernet.emmanuel@gmail.com
|
|
68
|
+
executables: []
|
|
69
|
+
extensions: []
|
|
70
|
+
extra_rdoc_files: []
|
|
71
|
+
files:
|
|
72
|
+
- MIT-LICENSE
|
|
73
|
+
- README.md
|
|
74
|
+
- Rakefile
|
|
75
|
+
- app/assets/images/rubber_duck/v-0-example.png
|
|
76
|
+
- app/assets/javascripts/rubber_duck/modal.js
|
|
77
|
+
- app/assets/stylesheets/rubber_duck/application.css
|
|
78
|
+
- app/controllers/rubber_duck/application_controller.rb
|
|
79
|
+
- app/controllers/rubber_duck/errors_controller.rb
|
|
80
|
+
- app/helpers/rubber_duck/application_helper.rb
|
|
81
|
+
- app/helpers/rubber_duck/errors_helper.rb
|
|
82
|
+
- app/jobs/rubber_duck/application_job.rb
|
|
83
|
+
- app/mailers/rubber_duck/application_mailer.rb
|
|
84
|
+
- app/models/rubber_duck/application_record.rb
|
|
85
|
+
- app/services/rubber_duck/ai_service.rb
|
|
86
|
+
- app/views/layouts/rubber_duck/application.html.erb
|
|
87
|
+
- app/views/rubber_duck/_button.html.erb
|
|
88
|
+
- config/initializers/rubber_duck.rb
|
|
89
|
+
- config/routes.rb
|
|
90
|
+
- lib/generators/rubber_duck/install_generator.rb
|
|
91
|
+
- lib/rubber_duck.rb
|
|
92
|
+
- lib/rubber_duck/configuration.rb
|
|
93
|
+
- lib/rubber_duck/engine.rb
|
|
94
|
+
- lib/rubber_duck/middleware.rb
|
|
95
|
+
- lib/rubber_duck/version.rb
|
|
96
|
+
- lib/tasks/rubber_duck_tasks.rake
|
|
97
|
+
homepage: https://github.com/EmmanuelVernet/rubber_duck
|
|
98
|
+
licenses:
|
|
99
|
+
- MIT
|
|
100
|
+
metadata:
|
|
101
|
+
allowed_push_host: https://rubygems.org
|
|
102
|
+
homepage_uri: https://github.com/EmmanuelVernet/rubber_duck
|
|
103
|
+
source_code_uri: https://github.com/EmmanuelVernet/rubber_duck
|
|
104
|
+
changelog_uri: https://github.com/EmmanuelVernet/rubber_duck/releases
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: '3.2'
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 3.6.9
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: RubberDuck is a Developer error helper gem to help you analyze errors with
|
|
122
|
+
AI in Rails
|
|
123
|
+
test_files: []
|