New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Actionable Errors #34788

Open
wants to merge 6 commits into
base: master
from

Conversation

Projects
None yet
6 participants
@gsamokovarov
Copy link
Contributor

gsamokovarov commented Dec 25, 2018

The idea of actionable errors has been floating around for a few years. First
introduced by @vipulnsward in #26542 (thanks Vipul) and later on expanded
by @causztic and myself in this year's GSoC. The current implementation is the
result of the lessons we learnt from the previous ones and, I hope, provides
the simplest API and implementation.


Actionable errors let's you dispatch actions from Rails' error pages. This
can help you save time if you have a clear action for the resolution of
common development errors.

The de-facto example are pending migrations. Every time pending migrations
are found, a middleware raises an error. With actionable errors, you can
run the migrations right from the error page. Other examples include Rails
plugins that need to run a rake task to setup themselves. They can now
raise actionable errors to run the setup straight from the error pages.

Here is how to define an actionable error:

class PendingMigrationError < MigrationError #:nodoc:
  include ActiveSupport::ActionableError

  action "Run pending migrations" do
    ActiveRecord::Tasks::DatabaseTasks.migrate
  end
end

To make an error actionable, include the ActiveSupport::ActionableError
module and invoke the action class macro to define the action. Action
needs a name and a procedure to execute. The name is shown as the name of a
button on the error pages. Once clicked, it will invoke the given
procedure. An error can have multiple actions as well.

The actions are dispatched by simple forms. No XHR's or any JS involved in
the process. The current page address is remembered and if the action was
successful, the dispatching middleware will issue a redirect back to it.

This is because to make an "action progress" page we'd need to either:

  1. Define an interface of the returned values of the action class macro.
  2. Capture STD{OUT,ERR} and display it.
  3. Probably something I'm missing... 馃槄

In any case, I think we can avoid all of the manutia around progress display and
do a simple post submission for now.

Here are a few screenshots of the error pages of actionable errors:

The builtin pending migrations error is now actionable

screen shot 2018-12-25 at 16 17 23

A custom actionable error with multiple actions

screen shot 2018-12-25 at 16 17 41

When an action fails

screen shot 2018-12-25 at 16 19 15

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch from afd77bb to 726fc8f Dec 25, 2018

@robin850
Copy link
Member

robin850 left a comment

Awesome ! 馃帀

Show resolved Hide resolved activesupport/test/actionable_error_test.rb Outdated

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch from 726fc8f to 90dc382 Dec 25, 2018

@simi

This comment has been minimized.

Copy link
Contributor

simi commented Dec 25, 2018

What's empty actionpack/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb for?

@nynhex

This comment has been minimized.

Copy link

nynhex commented Dec 26, 2018

^great holiday pr

@gmcgibbon
Copy link
Member

gmcgibbon left a comment

This is looking great @gsamokovarov! Really excited to see this in Rails 6!! 馃憦

Show resolved Hide resolved activesupport/lib/active_support/actionable_error.rb
Show resolved Hide resolved activesupport/lib/active_support/actionable_error.rb Outdated
Show resolved Hide resolved activesupport/lib/active_support/actionable_error.rb
Show resolved Hide resolved actionpack/test/dispatch/actionable_exceptions_test.rb Outdated
Show resolved Hide resolved actionpack/lib/action_dispatch/middleware/actionable_exceptions.rb
@@ -127,7 +128,13 @@ def initialize(name = nil)
end
end

class PendingMigrationError < MigrationError#:nodoc:
class PendingMigrationError < MigrationError #:nodoc:

This comment has been minimized.

@gmcgibbon

gmcgibbon Dec 26, 2018

Member

I don't see tests for this. Should we try to implement this with proper coverage in a separate commit?

This comment has been minimized.

@gsamokovarov

gsamokovarov Dec 26, 2018

Contributor

Now that we execute actions, seems like a good idea to test it. Maybe we can even tweak the message to hint the presence of the button and that you can click it?

This comment has been minimized.

@gmcgibbon

gmcgibbon Dec 27, 2018

Member

Yes, that sounds like a good idea. I think these tweaks should be made separate to the introduction of this feature though, so I'd be happy to see them in separate commits 馃榿

This comment has been minimized.

@gsamokovarov

gsamokovarov Dec 27, 2018

Contributor

Added the tests for the action dispatching. For now, I have added them alongside the database tasks tests, as they have the setup figured-out already. Will move it to the proper place once I have more focused time on it.

Show resolved Hide resolved activesupport/lib/active_support/actionable_error.rb Outdated

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch 2 times, most recently from aea4fa3 to 6dcdd3e Dec 26, 2018

gsamokovarov added a commit to gsamokovarov/rails that referenced this pull request Dec 26, 2018

Don't expect defined protect_against_forgery? in {token,csrf_meta}_tag
The `#csrf_meta_tags` and `#token_tag` Action View helper methods are
expecting the class in which are included to explicitly define the
method `#protect_against_forgery?` or else they will fail with
`NoMethodError`.

This is a problem if you want to use Action View outside of Rails
applications. For example, in rails#34788 I used the `#button_to` helper
inside of the error pages templates that have a custom
`ActionView::Base` subclass, which did not defined
`#protect_against_forgery?` and trying to call the button failed.

I had to dig inside of Action View to find-out what's was going on. I
think we should either set a default method implementation in the
helpers or check for the method definition, but don't explicitly require
the presence of `#protect_against_forgery?` in every `ActionViews::Base`
subclass as the errors are hard to figure out.

gsamokovarov added a commit to gsamokovarov/rails that referenced this pull request Dec 27, 2018

Don't expect defined protect_against_forgery? in {token,csrf_meta}_tag
The `#csrf_meta_tags` and `#token_tag` Action View helper methods are
expecting the class in which are included to explicitly define the
method `#protect_against_forgery?` or else they will fail with
`NoMethodError`.

This is a problem if you want to use Action View outside of Rails
applications. For example, in rails#34788 I used the `#button_to` helper
inside of the error pages templates that have a custom
`ActionView::Base` subclass, which did not defined
`#protect_against_forgery?` and trying to call the button failed.

I had to dig inside of Action View to find-out what's was going on. I
think we should either set a default method implementation in the
helpers or check for the method definition, but don't explicitly require
the presence of `#protect_against_forgery?` in every `ActionViews::Base`
subclass as the errors are hard to figure out.

gsamokovarov added a commit to gsamokovarov/rails that referenced this pull request Dec 27, 2018

Don't expect defined protect_against_forgery? in {token,csrf_meta}_tag
The `#csrf_meta_tags` and `#token_tag` Action View helper methods are
expecting the class in which are included to explicitly define the
method `#protect_against_forgery?` or else they will fail with
`NoMethodError`.

This is a problem if you want to use Action View outside of Rails
applications. For example, in rails#34788 I used the `#button_to` helper
inside of the error pages templates that have a custom
`ActionView::Base` subclass, which did not defined
`#protect_against_forgery?` and trying to call the button failed.

I had to dig inside of Action View to find-out what's was going on. I
think we should either set a default method implementation in the
helpers or check for the method definition, but don't explicitly require
the presence of `#protect_against_forgery?` in every `ActionViews::Base`
subclass as the errors are hard to figure out.

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch from 6dcdd3e to e4a7885 Dec 27, 2018

@matthewd

This comment has been minimized.

Copy link
Member

matthewd commented Dec 27, 2018

I'm disappointed to leave behind both stateful actions and any progress/output channel, but getting a solid foundation in sounds great.

end

def self.===(other) # :nodoc:
super || Module === other && other.ancestors.include?(self)

This comment has been minimized.

@matthewd

matthewd Dec 27, 2018

Member

馃憥 this is needlessly clever, and AFAICS it only obscures the intent of the call in actions below.

This comment has been minimized.

@gsamokovarov

gsamokovarov Dec 29, 2018

Contributor

Okay, I have dropped the ambiguous .=== and moved the checks into .action. There is still one "trick", though 馃槄

.actions now returns an empty actions hash for exception instances that are not actionable, so I can iterate over them in the Action Dispatch templates. This is still a bit unexpected behaviour IMO, but I didn't wanted to introduce more methods here.

This comment has been minimized.

@gsamokovarov

gsamokovarov Jan 1, 2019

Contributor

Redefined the .actions and .dispatch boundaries. Think it turned-up better than the last change.

Show resolved Hide resolved activesupport/lib/active_support/actionable_error.rb
Show resolved Hide resolved actionpack/lib/action_dispatch/middleware/debug_exceptions.rb Outdated
end

private
def actionable_request?(request)

This comment has been minimized.

@matthewd

matthewd Dec 27, 2018

Member

This has to be checked this way, and not mounted as an ordinary route, because we expect other pre-routing middlewares to raise actionable errors, right?

This comment has been minimized.

@gsamokovarov

gsamokovarov Dec 29, 2018

Contributor

Yes. I tried calling the dispatcher from an internal controller, but eventually, it ended up as this middleware. The pending migrations check is inserted quite early:

initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
ActiveRecord::Migration::CheckPending
end
end

This may even be a pattern for rails plugins inserting a feature checking middleware that raises an actionable error.

@rails-bot rails-bot bot added the railties label Dec 28, 2018

# To make an error actionable, include the <tt>ActiveSupport::ActionableError</tt>
# module and invoke the +action+ class macro to define the action.
#
# An action needs a name and a procedure to execute. The name can be shown by

This comment has been minimized.

@gmcgibbon

gmcgibbon Dec 31, 2018

Member

"An action needs a name and a block to execute" sounds better, I think. Also, I'm not sure what you mean by "The name can be shown by the action dispatching mechanism". Can you try to clarify this?

This comment has been minimized.

@gsamokovarov

gsamokovarov Jan 1, 2019

Contributor

Thanks! Dropped the dispatching sentence. May not be needed to explain how the dispatching works here, maybe only how to define actionable errors.

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch 3 times, most recently from 36eaef8 to 65e47ac Jan 1, 2019

gsamokovarov added some commits Dec 25, 2018

Introduce Actionable Errors
Actionable errors let's you dispatch actions from Rails' error pages. This
can help you save time if you have a clear action for the resolution of
common development errors.

The de-facto example are pending migrations. Every time pending migrations
are found, a middleware raises an error. With actionable errors, you can
run the migrations right from the error page. Other examples include Rails
plugins that need to run a rake task to setup themselves. They can now
raise actionable errors to run the setup straight from the error pages.

Here is how to define an actionable error:

```ruby
class PendingMigrationError < MigrationError #:nodoc:
  include ActiveSupport::ActionableError

  action "Run pending migrations" do
    ActiveRecord::Tasks::DatabaseTasks.migrate
  end
end
```

To make an error actionable, include the `ActiveSupport::ActionableError`
module and invoke the `action` class macro to define the action. An action
needs a name and a procedure to execute. The name is shown as the name of a
button on the error pages. Once clicked, it will invoke the given
procedure.

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch from 65e47ac to 1d3e589 Jan 15, 2019

@gsamokovarov gsamokovarov force-pushed the gsamokovarov:actionable-errors branch from 1d3e589 to 03ba01e Jan 15, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment