5
11
Fork
You've already forked tomato
1

Add HttpRequest for debugging #85

Merged
edwardloveall merged 1 commit from el-http-requests into main 2026年02月03日 16:15:24 +01:00

We've tried some debugging recently where we capture the state of models as they
change. This is somewhat useful but in practice it's been difficult to figure
out what the user actually did to trigger based on the state alone. It also
takes up a massive amount of space in the database compared to all other data.

Instead, I want to try recording all requests that are likely to change the
database (POST, PATCH, PUT, DELETE). Recording the request path, method, params,
and current user are hopefully enough to replay what they've done so we can have
reproducible test cases.

To actually reproduce these, you need to get the database into a state where it
has the old state, but the current requests

# restore current prod backup
bin/restore_db
# dump just the http_requests table
pg_dump --format custom --data-only --table http_requests tomato_development > tmp/http_requests.dump
# restore old backup
bin/restore_db some/old/backup.dump
rails db:migrate
# restore just the http_requests table
cat tmp/http_requests.dump | pg_restore --dbname tomato_development

Then, to replay the tests, use this script with rails runner and select the
HttpRequest object you want to replay:

class Replay
 include Warden::Test::Helpers
 attr_reader :session
 def initialize
 app = Rails.application
 app.reload_routes_unless_loaded
 @session = ActionDispatch::Integration::Session.new(app)
 @session.host = "localhost"
 ActionController::Base.allow_forgery_protection = false
 end
 def call
 requests = HttpRequest.where(id: [1, 2, 3])
 requests.each do |req|
 puts "processing: #{req.method}#{req.params}"
 login_as(req.user)
 session.send(req.method.downcase.to_sym, req.path, params: req.params)
 end
 end
end
Replay.new.call

The above code took a while to workout. There may be better options but I this
is the best I could find. https://stackoverflow.com/q/79881226/638966

We've tried some debugging recently where we capture the state of models as they change. This is somewhat useful but in practice it's been difficult to figure out what the user actually did to trigger based on the state alone. It also takes up a massive amount of space in the database compared to all other data. Instead, I want to try recording all requests that are likely to change the database (POST, PATCH, PUT, DELETE). Recording the request path, method, params, and current user are hopefully enough to replay what they've done so we can have reproducible test cases. To actually reproduce these, you need to get the database into a state where it has the old state, but the current requests ```sh # restore current prod backup bin/restore_db # dump just the http_requests table pg_dump --format custom --data-only --table http_requests tomato_development > tmp/http_requests.dump # restore old backup bin/restore_db some/old/backup.dump rails db:migrate # restore just the http_requests table cat tmp/http_requests.dump | pg_restore --dbname tomato_development ``` Then, to replay the tests, use this script with `rails runner` and select the `HttpRequest` object you want to replay: ```rb class Replay include Warden::Test::Helpers attr_reader :session def initialize app = Rails.application app.reload_routes_unless_loaded @session = ActionDispatch::Integration::Session.new(app) @session.host = "localhost" ActionController::Base.allow_forgery_protection = false end def call requests = HttpRequest.where(id: [1, 2, 3]) requests.each do |req| puts "processing: #{req.method} #{req.params}" login_as(req.user) session.send(req.method.downcase.to_sym, req.path, params: req.params) end end end Replay.new.call ``` The above code took a while to workout. There may be better options but I this is the best I could find. https://stackoverflow.com/q/79881226/638966
jsilasbailey left a comment
Copy link

This is pretty cool, can't really test it out but the concept seems fairly solid. Last thought is leaving room in the future for "system" user (e.g. non HTTP, scheduled job) style modifications if you are interested in the whole data story impacting the user requests. It doesn't feel like the app has anything like that right now but where there is a webapp there are also, eventually, scheduled jobs 😄

This is pretty cool, can't really test it out but the concept seems fairly solid. Last thought is leaving room in the future for "system" user (e.g. non HTTP, scheduled job) style modifications if you are interested in the whole data story impacting the user requests. It doesn't _feel_ like the app has anything like that right now but where there is a webapp there are also, eventually, scheduled jobs 😄
@ -43,0 +48,4 @@
params = request.request_parameters
method = params.delete("_method")&.upcase || request.method
params.delete("authenticity_token")
param_filter = ActiveSupport::ParameterFilter.new(%i[
First-time contributor
Copy link

Not sure of the best way to make this more visible but perhaps a separate constant in screaming snake case with a big ole' comment about how important it is to update the filter with security-centered params?

Also, does adding authenticity_token to this list not work?

Not sure of the best way to make this more visible but perhaps a separate constant in screaming snake case with a big ole' comment about how important it is to update the filter with security-centered params? Also, does adding `authenticity_token` to this list not work?
Author
Owner
Copy link

You know, I actually think I can re-use the one from config/initializers/filter_parameter_logging.rb. At some point iterating on this I wanted to extend that list and couldn't so I made my own, but my list is a subset now so I can re-use it.

Also, does adding authenticity_token to this list not work?

Don't need to. ParameterFilter is fuzzy, so token also filters authenticity_token, my_token, tokenize, etc:

> param_filter = ActiveSupport::ParameterFilter.new([:token])
=> #<ActiveSupport::ParameterFilter:0x00000001215fd1c0 @blocks=nil, @deep_regexps=nil, @mask="[FILTERED]", @no_filters=false, @regexps=[/token/i]>
> param_filter.filter({authenticity_token: "foobar"})
=> {:authenticity_token=>"[FILTERED]"}
You know, I actually think I can re-use the one from `config/initializers/filter_parameter_logging.rb`. At some point iterating on this I wanted to extend that list and couldn't so I made my own, but my list is a subset now so I can re-use it. > Also, does adding authenticity_token to this list not work? Don't need to. ParameterFilter is fuzzy, so `token` also filters `authenticity_token`, `my_token`, `tokenize`, etc: ```rb > param_filter = ActiveSupport::ParameterFilter.new([:token]) => #<ActiveSupport::ParameterFilter:0x00000001215fd1c0 @blocks=nil, @deep_regexps=nil, @mask="[FILTERED]", @no_filters=false, @regexps=[/token/i]> > param_filter.filter({authenticity_token: "foobar"}) => {:authenticity_token=>"[FILTERED]"} ```
@ -43,0 +54,4 @@
token
])
sanitized_params = param_filter.filter(params)
HttpRequest.create!(
First-time contributor
Copy link

How important is failing the request if you can save off the http request? Might consider logging and still allowing? Does the FE implement any retry behavior?

How important is failing the request if you can save off the http request? Might consider logging and still allowing? Does the FE implement any retry behavior?
Author
Owner
Copy link

Not important, good catch! I'll remove the !

Not important, good catch! I'll remove the `!`
@ -0,0 +2,4 @@
def change
create_table :http_requests do |table|
table.datetime(:created_at)
table.references(:user)
First-time contributor
Copy link

(削除) For historical logging is there any consideration here of not storing a reference to the user table in case accounts are removed? I suppose to replay the request you would need to have an 'auth-able' user account but it might be worth considering storing the user id without the foreign key and then checking in the runner if the account is present anymore before replaying the request. That way you have the action that was taken forever. (削除ここまで) (I forgot that rails doesn't use foreign keys by default 😄, still curious about the following question)

I suppose this all depends on how long lived you want these request records to be as well?

~For historical logging is there any consideration here of not storing a reference to the user table in case accounts are removed? I suppose to replay the request you would need to have an 'auth-able' user account but it might be worth considering storing the user id without the foreign key and then checking in the runner if the account is present anymore before replaying the request. That way you have the action that was taken forever.~ (I forgot that rails doesn't use foreign keys by default 😄, still curious about the following question) I suppose this all depends on how long lived you want these request records to be as well?
Author
Owner
Copy link

It's a good question and I did consider it. Right now if you destroy the associated user, the request remains and is a dangling reference. Not ideal, obviously, but I'm okay with that since this is for debug purposes. The script I have to replay requests would need to be updated, but that's fine.

It's a good question and I did consider it. Right now if you destroy the associated user, the request remains and is a dangling reference. Not ideal, obviously, but I'm okay with that since this is for debug purposes. The script I have to replay requests would need to be updated, but that's fine.
@ -0,0 +4,4 @@
table.datetime(:created_at)
table.references(:user)
table.string(:method)
table.string(:path)
First-time contributor
Copy link

Are there any special headers you need to store as well for application logic to work?

Are there any special headers you need to store as well for application logic to work?
Author
Owner
Copy link

Hmmm... I don't think we use any special headers. We can ignore the cookie, and the Content-Type header is taken care of by adding .json to the end of the url. I think I'm okay with adding this as-is because if I'm wrong it means my automatic replay won't work, but I can still go manually make the request.

Hmmm... I don't _think_ we use any special headers. We can ignore the cookie, and the `Content-Type` header is taken care of by adding `.json` to the end of the url. I think I'm okay with adding this as-is because if I'm wrong it means my automatic replay won't work, but I can still go manually make the request.
edwardloveall force-pushed el-http-requests from 392f7067a7
All checks were successful
Setup Successful
Jest Successful
Static Analysis Successful
RSpec Successful
to e4d3a589cc
All checks were successful
Setup Successful
Jest Successful
Static Analysis Successful
RSpec Successful
2026年02月03日 15:44:54 +01:00
Compare
Author
Owner
Copy link

@jsilasbailey wrote in #85 (comment):

Last thought is leaving room in the future for "system" user (e.g. non HTTP, scheduled job) style modifications if you are interested in the whole data story impacting the user requests.

Yeah, that's not a bad idea. We already have the concept of a "deleted user" and could easily add a "system user". No background jobs at the moment though; it's a gap I hope to fill at some point.

@jsilasbailey wrote in https://codeberg.org/rootable/tomato/pulls/85#issuecomment-10341786: > Last thought is leaving room in the future for "system" user (e.g. non HTTP, scheduled job) style modifications if you are interested in the whole data story impacting the user requests. Yeah, that's not a bad idea. We already have the concept of a "deleted user" and could easily add a "system user". No background jobs at the moment though; it's a gap I hope to fill at some point.
edwardloveall deleted branch el-http-requests 2026年02月03日 16:15:25 +01:00
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rootable/tomato!85
Reference in a new issue
rootable/tomato
No description provided.
Delete branch "el-http-requests"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?