Sitelet https://github.com/errbit/errbit/pull/2832/files
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
bc7f6b9
Start porting with Claude code
biow0lf May 13, 2026
09dfe15
Update
biow0lf May 13, 2026
d633ab2
Update
biow0lf May 13, 2026
35781b7
Update
biow0lf May 13, 2026
b2c7391
Update
biow0lf May 13, 2026
4f96c24
Update
biow0lf May 13, 2026
ae2fe86
Update
biow0lf May 13, 2026
e4e8442
Update
biow0lf May 13, 2026
29f9897
Update
biow0lf May 15, 2026
147b16e
Update
biow0lf May 15, 2026
44faf82
Update
biow0lf May 15, 2026
ef37bd2
Fix
biow0lf May 15, 2026
6f33871
Fix
biow0lf May 15, 2026
d311f9d
Fix
biow0lf May 15, 2026
29b4ad3
Update
biow0lf May 15, 2026
e735340
Fix
biow0lf May 15, 2026
5d5cb3d
Update
biow0lf May 15, 2026
36d4c79
Update
biow0lf May 15, 2026
286b761
Update
biow0lf May 15, 2026
232ddab
Update
biow0lf May 15, 2026
3d489b9
Update
biow0lf May 15, 2026
056fd0e
Update
biow0lf May 15, 2026
f88207f
Update
biow0lf May 15, 2026
493fcb8
Update
biow0lf May 15, 2026
e930c97
Update
biow0lf May 15, 2026
0719edf
Add CLAUDE.md and AGENTS.md
biow0lf May 15, 2026
d95256f
Update
biow0lf May 15, 2026
ec02809
Update
biow0lf May 15, 2026
df9e0a5
Update
biow0lf May 15, 2026
423fee8
Update
biow0lf May 15, 2026
0ecee63
Update
biow0lf May 15, 2026
cd69064
Update
biow0lf May 15, 2026
1451973
Update
biow0lf May 15, 2026
6650507
Update
biow0lf May 15, 2026
2350bf4
Update
biow0lf May 15, 2026
9b4bab3
Update
biow0lf May 15, 2026
f307164
Update
biow0lf May 15, 2026
1a4fd0e
Update
biow0lf May 15, 2026
a90b202
Update
biow0lf May 15, 2026
92e241e
Update
biow0lf May 15, 2026
c0ffe18
Update
biow0lf May 16, 2026
d9e1c1b
Update
biow0lf May 16, 2026
ef8cba3
Update
biow0lf May 16, 2026
bcf773f
Update
biow0lf May 17, 2026
74753ae
Update
biow0lf May 17, 2026
5a92c9a
Update
biow0lf May 17, 2026
49f930d
Update
biow0lf May 17, 2026
5e4d380
Update
biow0lf May 17, 2026
c5106d2
Update
biow0lf May 17, 2026
127761f
Update
biow0lf May 17, 2026
ac7cb62
Update
biow0lf May 17, 2026
5f3acd7
Update
biow0lf May 17, 2026
8ed7674
Update
biow0lf May 17, 2026
acf0bc8
Update
biow0lf May 17, 2026
069d0d3
Update
biow0lf May 19, 2026
fdd238f
Fix rubocop warnings
biow0lf May 19, 2026
358e380
Update
biow0lf May 19, 2026
6a3437f
Warnings
biow0lf May 19, 2026
708b75b
Update
biow0lf May 19, 2026
a80998a
Update
biow0lf May 19, 2026
e425081
Update
biow0lf May 19, 2026
0601437
More tests
biow0lf May 25, 2026
7f8621e
More tests
biow0lf May 25, 2026
7a8e4c3
Update sqlite3 gem from 2.9.4 to 2.9.5
biow0lf Jul 5, 2026
0ddb650
Update shoulda-matchers gem from 7.0.1 to 8.0.1
biow0lf Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,7 @@ jobs:
- run: bundle exec bootsnap precompile app/ lib/ config/ Rakefile
- run: bundle exec rails zeitwerk:check
- run: bundle exec rails assets:precompile
- run: bundle exec rails db:create
- run: bundle exec rails db:schema:load
- run: bundle exec rails errbit:bootstrap
- run: bundle exec rspec
1 change: 1 addition & 0 deletions .github/workflows/standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:

- run: |
bin/standardrb \
db/migrate/*.rb \
app/controllers/users_controller.rb \
app/controllers/unlink_githubs_controller.rb \
app/controllers/unlink_googles_controller.rb \
Expand Down
48 changes: 48 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Errbit — agent guide

## Project status: MongoDB → SQL port in progress

Errbit is being ported from MongoDB/Mongoid to SQL/ActiveRecord on branch `claude-code-port-to-sql`.

- **Old Mongoid models** live at `app/models/*.rb` (e.g. `app/models/user.rb` → `User`). They stay in place.
- **New AR models** live under the `Errbit::` namespace at `app/models/errbit/*.rb` (e.g. `app/models/errbit/user.rb` → `Errbit::User`).
- Both ORMs coexist during the port. Devise is wired only to `Errbit::User` (AR); the Mongoid `User` keeps its legacy fields as plain data for the migration rake task but is no longer a Devise model.

## Conventions for ported models

- Tables use the `errbit_` prefix via `Errbit.table_name_prefix` in `app/models/errbit.rb`.
- AR models inherit from `Errbit::ApplicationRecord` (uses `primary_abstract_class`).
- **Every ported table gets a `bson_id` string column with a unique index.** This stores the original Mongo BSON id so a future ETL step can link records back to their Mongo origin. Don't skip it.
- Foreign keys use the `errbit_` prefix: `errbit_app_id`, `errbit_user_id`, etc.
- Test infra: `database_rewinder` for cleanup (`spec/support/active_record.rb`), `shoulda-matchers` available for validation matchers.
- Factories live under `spec/factories/errbit/`, specs under `spec/models/errbit/`. Factory names are prefixed (e.g. `:errbit_user`, `:errbit_app`, `:errbit_watcher`).

## Porting a new model

When asked to port the next Mongoid model:

1. Add a migration `db/migrate/<timestamp>_create_errbit_<plural>.rb` — include `bson_id` + unique index, foreign keys with `errbit_` prefix, all data fields from the Mongoid model.
2. Create `app/models/errbit/<name>.rb` — port fields/validations/callbacks/methods. Skip methods that depend on still-unported models; leave them for when the dependency is ported.
3. If a previously-ported model has an association to this one, wire it up now (e.g. when porting `Watcher`, add `has_many :watchers` to `Errbit::App`).
4. Add `spec/factories/errbit/<name>_factory.rb` and `spec/models/errbit/<name>_spec.rb`.
5. Run migrations in both envs and run the spec.
6. **Don't delete the old Mongoid model** — both must work until the full port is done.

## Commands

```sh
bundle exec rails db:migrate # development
RAILS_ENV=test bundle exec rails db:migrate # test

bundle exec rspec # full suite
bundle exec rspec spec/models/errbit/ # all ported AR models
bundle exec rspec spec/models/errbit/<name>_spec.rb # one spec
```

Tests require a running MongoDB (the Mongoid models are still loaded on boot). The user runs mongo via Docker.

## Things to check before running tests

- `bundle install` after Gemfile changes.
- Run migrations in **both** `development` and `test` envs — they're separate sqlite files (`storage/development.sqlite3`, `storage/test.sqlite3`).
- After porting, run the namespaced spec **and** the old Mongoid spec to confirm no regression.
9 changes: 9 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
See [AGENTS.md](AGENTS.md) for project conventions, the Mongo→SQL port plan, and commands.

## Spec conventions

- In RSpec, every `context` block description **must** start with `when`, `with`, or `without`. Examples: `context "when user is an admin"`, `context "with valid params"`, `context "without a tracker configured"`. Do not start a context with `as`, `for`, a participle, or any other word.

## Ruby block style

- Use `{ ... }` only for **single-line** blocks. For any block that spans more than one line, use `do ... end`. This applies everywhere — including `expect { ... }.to …`, `lambda { ... }`, `Proc.new { ... }`, `Hash#each { ... }`, `before { ... }`, etc. A multi-line `expect` becomes `expect do … end.to …`.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ gem "uri"
gem "rack-timeout"
gem "puma"
gem "ostruct"
gem "sqlite3", ">= 2.1"

# Please don't update hoptoad_notifier to airbrake.
# It's for internal use only, and we monkeypatch certain methods
Expand Down Expand Up @@ -98,6 +99,8 @@ group :test do
gem "rspec-rebound", require: false
gem "rspec-activemodel-mocks"
gem "mongoid-rspec"
gem "shoulda-matchers"
gem "database_rewinder"
gem "pundit-matchers"
gem "capybara"
gem "selenium-webdriver"
Expand Down
31 changes: 31 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ GEM
addressable
ssrf_filter (~> 1.5)
csv (3.3.5)
database_rewinder (1.1.0)
date (3.5.1)
decent_exposure (3.0.4)
activesupport (>= 4.0)
Expand Down Expand Up @@ -580,6 +581,8 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
shoulda-matchers (8.0.1)
activesupport (>= 7.2)
simplecov (0.22.0)
docile (~> 1.1)
simplecov-html (~> 0.11)
Expand All @@ -597,6 +600,18 @@ GEM
actionpack (>= 6.1)
activesupport (>= 6.1)
sprockets (>= 3.0.0)
sqlite3 (2.9.5)
mini_portile2 (~> 2.8.0)
sqlite3 (2.9.5-aarch64-linux-gnu)
sqlite3 (2.9.5-aarch64-linux-musl)
sqlite3 (2.9.5-arm-linux-gnu)
sqlite3 (2.9.5-arm-linux-musl)
sqlite3 (2.9.5-arm64-darwin)
sqlite3 (2.9.5-x86-linux-gnu)
sqlite3 (2.9.5-x86-linux-musl)
sqlite3 (2.9.5-x86_64-darwin)
sqlite3 (2.9.5-x86_64-linux-gnu)
sqlite3 (2.9.5-x86_64-linux-musl)
ssrf_filter (1.5.0)
standard (1.55.0)
language_server-protocol (~> 3.17.0.2)
Expand Down Expand Up @@ -676,6 +691,7 @@ DEPENDENCIES
bundler-audit
campy
capybara
database_rewinder
decent_exposure
devise
dotenv
Expand Down Expand Up @@ -736,8 +752,10 @@ DEPENDENCIES
rubocop-rspec_rails
rubocop-thread_safety
selenium-webdriver
shoulda-matchers
simplecov
sprockets-rails
sqlite3 (>= 2.1)
standard (= 1.55.0)
stimulus-rails
super_diff
Expand Down Expand Up @@ -789,6 +807,7 @@ CHECKSUMS
crass (1.0.7) sha256=94868719948664c89ddcaf0a37c65048413dfcb1c869470a5f7a7ceb5390b295
css_parser (3.0.0) sha256=eaf0e9283fd581d06e815235ceef4f0910c0b394c606355dbc69f93e84443885
csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f
database_rewinder (1.1.0) sha256=950398ba97bb20fb4653b6ee502a7d6658a6333a61d88ba756217f32bfa04510
date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
decent_exposure (3.0.4) sha256=5ce9e3df24f8d77f094b2e612d1c43a78a2de879847ccbb57c0e596e6c303cf1
devise (5.0.4) sha256=d605f2b85854e74e56ee789e2d398702bc2d06e6bcd894717a670a3199c74cc1
Expand Down Expand Up @@ -963,12 +982,24 @@ CHECKSUMS
sawyer (0.9.3) sha256=0d0f19298408047037638639fe62f4794483fb04320269169bd41af2bdcf5e41
securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
selenium-webdriver (4.45.0) sha256=ecac65a4df86ac6f7d707e6dcbacaa9c08b6cf2b966babecfb9653c5aa13e2d1
shoulda-matchers (8.0.1) sha256=5dbb46e5765b9da225111b085e0819e8c8a121ff94bba430a153eb1ea2c60288
simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
snaky_hash (2.0.6) sha256=3663cae48cdef582b517025cf8a39d8789996eaf0b4ed89e2f0624836505654a
sprockets (4.2.2) sha256=761e5a49f1c288704763f73139763564c845a8f856d52fba013458f8af1b59b1
sprockets-rails (3.5.2) sha256=a9e88e6ce9f8c912d349aa5401509165ec42326baf9e942a85de4b76dbc4119e
sqlite3 (2.9.5) sha256=04572973a3f943ad50a8adfffc8dd752a5f06e4c3db2026f71838fed8a982606
sqlite3 (2.9.5-aarch64-linux-gnu) sha256=78075b6337d3d182c6d2b4691049ed45cd220826160c9ea18946bf6a1de200dc
sqlite3 (2.9.5-aarch64-linux-musl) sha256=18c801185deb4adc01ddb281e8f672a39e3d1729979ca91e39439cd3eac0402d
sqlite3 (2.9.5-arm-linux-gnu) sha256=1bdfca0c7d63998c60b0f4a8e3c8df2d33800ccc4abd2d612eddbbbc92a4c48b
sqlite3 (2.9.5-arm-linux-musl) sha256=bae1109d12b2e9f588455967729b008e1ff4feb7761749df695019c9079913c6
sqlite3 (2.9.5-arm64-darwin) sha256=d0cf444a70fc9395d513cfbcc1e6719e224aa645314e3824cb0474c721425aa2
sqlite3 (2.9.5-x86-linux-gnu) sha256=c94b96b16f17796be6fa099d15218b52e396f55690c4760faaaefa21ebab9dd5
sqlite3 (2.9.5-x86-linux-musl) sha256=063a8c13cbadfe7f29453b1706cbdf91fca4a78d244f816ff20bac4fb259f1e4
sqlite3 (2.9.5-x86_64-darwin) sha256=8e9caae38bd7ebb29cbeee3e7ab1d12dc2327d9a1b92c7fcf0dda05589627a81
sqlite3 (2.9.5-x86_64-linux-gnu) sha256=233dbcb6714148dd23bc5aeb33e8efd6eac974969564ddd5794c23d5f52b231e
sqlite3 (2.9.5-x86_64-linux-musl) sha256=e7d3a7474e8af0f96150c21abc203fbab5437206bfcdf11deab7741c0ca516f2
ssrf_filter (1.5.0) sha256=e03dcdb9d1730d7f6710532a606b3543df2a448a0293ce04a2d995523c5a97f6
standard (1.55.0) sha256=8a8f2c3e681a4db3aafde1b301561b0f3d7c5f06c160167cb744a4d7baf0426e
standard-custom (1.0.2) sha256=424adc84179a074f1a2a309bb9cf7cd6bfdb2b6541f20c6bf9436c0ba22a652b
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* Notification Service inactive, select, create and goto icons */
<% notification_services = NotificationService.subclasses.map{|t| t.label } << 'none' %>
<% notification_services = Errbit::NotificationService.subclasses.map { |t| t.label } << "none" %>

<% notification_services.each do |notification_service| %>
div.notification_service.nested label.<%= notification_service %> {
Expand Down
25 changes: 19 additions & 6 deletions app/controllers/api/v1/comments_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ class CommentsController < ApplicationController
skip_before_action :authenticate_user!

respond_to :json, :xml
FIELDS = ["_id", "err_id", "user_id", "body"].freeze

def index
results = benchmark("[api/v1/comments_controller/index] query time") do
Comment.where(err_id: params[:problem_id]).only(FIELDS).to_a
Errbit::Comment.where(errbit_problem_id: params[:problem_id])
.to_a
.map { |comment| serialize_comment(comment) }
end

respond_to do |format|
Expand All @@ -21,10 +22,10 @@ def index
end

def create
comment = Comment.new(comment_params)
comment = Errbit::Comment.new(comment_params)

if comment.save
render status: :created, json: comment
render status: :created, json: serialize_comment(comment)
else
render(
body: {errors: comment.errors.full_messages}.to_json,
Expand All @@ -36,8 +37,20 @@ def create
private

def comment_params
# merge makes a copy, merge! edits in place
params.require(:comment).permit(:body).merge!(user_id: current_user.id, err_id: params[:problem_id])
params.require(:comment).permit(:body).merge(
errbit_user_id: current_user.id,
errbit_problem_id: params[:problem_id]
)
end

# Preserve the Mongoid-era API contract: keys `_id`, `err_id`, `user_id`.
def serialize_comment(comment)
{
"_id" => comment.id.to_s,
"err_id" => comment.errbit_problem_id.to_s,
"user_id" => comment.errbit_user_id.to_s,
"body" => comment.body
}
end
end
end
Expand Down
19 changes: 15 additions & 4 deletions app/controllers/api/v1/notices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,35 @@ class NoticesController < ApplicationController
respond_to :json, :xml

def index
query = {}
fields = ["created_at", "message", "error_class"]
scope = Errbit::Notice.all

if params.key?(:start_date) && params.key?(:end_date)
start_date = Time.zone.parse(params[:start_date]).utc
end_date = Time.zone.parse(params[:end_date]).utc
query = {created_at: {"$lte" => end_date, "$gte" => start_date}}
scope = scope.where(created_at: start_date..end_date)
end

results = benchmark("[api/v1/notices_controller] query time") do
Notice.where(query).only(fields).to_a
scope.to_a.map { |notice| serialize_notice(notice) }
end

respond_to do |format|
format.any(:html, :json) { render json: results } # render JSON if no extension specified on path
format.xml { render xml: results }
end
end

private

# Preserve the Mongoid-era API contract: key `_id` (string).
def serialize_notice(notice)
{
"_id" => notice.id.to_s,
"created_at" => notice.created_at,
"message" => notice.message,
"error_class" => notice.error_class
}
end
end
end
end
38 changes: 27 additions & 11 deletions app/controllers/api/v1/problems_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,18 @@ module V1
class ProblemsController < ApplicationController
respond_to :json, :xml

FIELDS = [
"_id", "app_id", "app_name", "environment", "message", "where",
"first_notice_at", "last_notice_at", "resolved", "resolved_at",
"notices_count"
].freeze

def index
query = {}
scope = Errbit::Problem.all

if params.key?(:start_date) && params.key?(:end_date)
start_date = Time.parse(params[:start_date]).utc
end_date = Time.parse(params[:end_date]).utc
query = {:first_notice_at => {"$lte" => end_date}, "$or" => [{resolved_at: nil}, {resolved_at: {"$gte" => start_date}}]}
scope = scope.where("first_notice_at <= ?", end_date)
.where("resolved_at IS NULL OR resolved_at >= ?", start_date)
end

results = benchmark("[api/v1/problems_controller/index] query time") do
Problem.where(query).only(FIELDS).to_a
scope.to_a.map { |problem| serialize_problem(problem) }
end

respond_to do |format|
Expand All @@ -32,8 +27,8 @@ def index

def show
result = benchmark("[api/v1/problems_controller/show] query time") do
Problem.only(FIELDS).find(params.expect(:id))
rescue Mongoid::Errors::DocumentNotFound
serialize_problem(Errbit::Problem.find(params.expect(:id)))
rescue ActiveRecord::RecordNotFound
head :not_found
return false
end
Expand All @@ -43,6 +38,27 @@ def show
format.xml { render xml: result }
end
end

private

# Preserve the Mongoid-era API contract (keys `_id` and `app_id` are
# string-typed). Clients depending on this v1 shape keep working after
# the SQL port.
def serialize_problem(problem)
{
"_id" => problem.id.to_s,
"app_id" => problem.errbit_app_id.to_s,
"app_name" => problem.app_name,
"environment" => problem.environment,
"message" => problem.message,
"where" => problem.where,
"first_notice_at" => problem.first_notice_at,
"last_notice_at" => problem.last_notice_at,
"resolved" => problem.resolved,
"resolved_at" => problem.resolved_at,
"notices_count" => problem.notices_count
}
end
end
end
end
4 changes: 2 additions & 2 deletions app/controllers/api/v1/stats_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class StatsController < ApplicationController
before_action :require_api_key_or_authenticate_user!

def app
if (problem = @app.problems.order_by(:last_notice_at.desc).first)
if (problem = @app.problems.order(last_notice_at: :desc).first)
@last_error_time = problem.last_notice_at
end

Expand All @@ -31,7 +31,7 @@ def app

def require_api_key_or_authenticate_user!
if params[:api_key].present?
return true if (@app = App.where(api_key: params[:api_key]).first)
return true if (@app = Errbit::App.where(api_key: params[:api_key]).first)
end

authenticate_user!
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/api/v3/notices_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
module V3
class NoticesController < ApplicationController
VERSION_TOO_OLD = "Notice for old app version ignored"
UNKNOWN_API_KEY = "Your API key is unknown"

Check warning on line 7 in app/controllers/api/v3/notices_controller.rb

View workflow job for this annotation

GitHub Actions / run

Hardcoded value for `UNKNOWN_API_KEY` in source code

skip_before_action :verify_authenticity_token
skip_before_action :authenticate_user!
Expand Down Expand Up @@ -32,7 +32,7 @@

report.generate_notice!
render status: :created, json: {
id: report.notice.id,
id: report.notice.id.to_s,
url: report.problem.url
}
rescue AirbrakeApi::ParamsError
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def set_time_zone
end

def authenticate_user_from_token!
user_token = params[User.token_authentication_key].presence
user = user_token && User.find_by(authentication_token: user_token)
user_token = params[Errbit::User.token_authentication_key].presence
user = user_token && Errbit::User.find_by(authentication_token: user_token)

sign_in user, store: false if user
end
Expand Down
Loading
Loading