Skip to content

Add Rails.application.deprecators #46049

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

jonathanhefner
Copy link
Member

@jonathanhefner jonathanhefner commented Sep 15, 2022

This adds a Rails.application.deprecators method that returns a managed collection of deprecators.

Individual deprecators can be added and retrieved from the collection:

Rails.application.deprecators[:my_gem] = ActiveSupport::Deprecation.new("2.0", "MyGem")
Rails.application.deprecators[:other_gem] = ActiveSupport::Deprecation.new("3.0", "OtherGem")

And the collection's configuration methods affect all deprecators in the collection:

Rails.application.deprecators.debug = true

Rails.application.deprecators[:my_gem].debug
# => true
Rails.application.deprecators[:other_gem].debug
# => true

Additionally, all deprecators in the collection can be silenced for the duration of a given block:

Rails.application.deprecators.silence do
  Rails.application.deprecators[:my_gem].warn    # => silenced (no warning)
  Rails.application.deprecators[:other_gem].warn # => silenced (no warning)
end


#
def [](name)
# TODO? automatically instantiate a deprecator when it does not exist?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, but I'd not do it now and only do it later if it makes sense for the framework's own usage.


#
def []=(name, deprecator)
# TODO? save configuration from setters (e.g. `#silenced=`) and apply
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense.

@jonathanhefner jonathanhefner force-pushed the add-rails_application_deprecators branch 4 times, most recently from 01add7e to c4e99ae Compare September 26, 2022 21:44
@jonathanhefner jonathanhefner marked this pull request as ready for review September 26, 2022 22:03
@jonathanhefner jonathanhefner force-pushed the add-rails_application_deprecators branch from c4e99ae to f3b53c7 Compare September 29, 2022 17:14
@jonathanhefner jonathanhefner force-pushed the add-rails_application_deprecators branch from f3b53c7 to e9ba80e Compare October 5, 2022 19:26
This adds a `Rails.application.deprecators` method that returns a
managed collection of deprecators.

Individual deprecators can be added and retrieved from the collection:

  ```ruby
  Rails.application.deprecators[:my_gem] = ActiveSupport::Deprecation.new("2.0", "MyGem")
  Rails.application.deprecators[:other_gem] = ActiveSupport::Deprecation.new("3.0", "OtherGem")
  ```

And the collection's configuration methods affect all deprecators in the
collection:

  ```ruby
  Rails.application.deprecators.debug = true

  Rails.application.deprecators[:my_gem].debug
  # => true
  Rails.application.deprecators[:other_gem].debug
  # => true
  ```

Additionally, all deprecators in the collection can be silenced for the
duration of a given block:

  ```ruby
  Rails.application.deprecators.silence do
    Rails.application.deprecators[:my_gem].warn    # => silenced (no warning)
    Rails.application.deprecators[:other_gem].warn # => silenced (no warning)
  end
  ```
@jonathanhefner jonathanhefner force-pushed the add-rails_application_deprecators branch from e9ba80e to d240e8a Compare October 5, 2022 19:37
@jonathanhefner jonathanhefner merged commit 32aaf11 into rails:main Oct 5, 2022
@p8
Copy link
Member

p8 commented Oct 8, 2022

Hi @jonathanhefner.
I'm working on this weeks newsletter and want to include this PR.
Could you explain the use-case for this?
Is it for deprecating gem versions used by Rails?
So for example if we stop supporting dalli version 2, we add:
Rails.application.deprecators[:dalli] = ActiveSupport::Deprecation.new("2.0", "dalli")

@jonathanhefner
Copy link
Member Author

@p8 Close, but it would be Dalli registering Rails.application.deprecators[:dalli] and generating warnings using that deprecator instance.

Before this PR, if a library created its own deprecator instance, users of the library would have to seek out and configure that instance separately from the global ActiveSupport::Deprecation instance. For example, if a library had:

MyGem.deprecator = ActiveSupport::Deprecation.new("2.0", "my_gem")

and a user wanted to set behavior = :raise for all deprecators, they would have to write:

ActiveSupport::Deprecation.behavior = :raise
MyGem.deprecator.behavior = :raise

And so on for each library with its own deprecator instance.

With this PR, each library can have an initializer like:

module MyGem
  class Railtie < ::Rails::Railtie
    initializer :deprecator do |app|
      app.deprecators[:my_gem] = MyGem.deprecator
    end
  end
end

And then the user will only have to write:

Rails.application.deprecators.behavior = :raise

Alternatively, if the user wants to change the behavior for a single deprecator, they now have a standard location to do so:

Rails.application.deprecators[:my_gem].behavior = :raise

And all the above also applies to the debug, disallowed_behavior, and silenced settings, as well as silence blocks.

@p8
Copy link
Member

p8 commented Oct 9, 2022

Ok clear. Thanks @jonathanhefner !

jonathanhefner added a commit to jonathanhefner/rails that referenced this pull request Apr 11, 2023
Follow-up to rails#46049 and rails#47354.

Since `Rails.deprecator` is now a new `ActiveSupport::Deprecation`
instance instead of `ActiveSupport::Deprecation.instance`, it makes
sense to register it as `Rails.application.deprecators[:railties]`
instead of `Rails.application.deprecators[:rails]`.
danielvdao pushed a commit to danielvdao/rails that referenced this pull request May 1, 2023
Follow-up to rails#46049 and rails#47354.

Since `Rails.deprecator` is now a new `ActiveSupport::Deprecation`
instance instead of `ActiveSupport::Deprecation.instance`, it makes
sense to register it as `Rails.application.deprecators[:railties]`
instead of `Rails.application.deprecators[:rails]`.
danielvdao pushed a commit to danielvdao/rails that referenced this pull request May 1, 2023
Follow-up to rails#46049 and rails#47354.

Since `Rails.deprecator` is now a new `ActiveSupport::Deprecation`
instance instead of `ActiveSupport::Deprecation.instance`, it makes
sense to register it as `Rails.application.deprecators[:railties]`
instead of `Rails.application.deprecators[:rails]`.
danielvdao pushed a commit to danielvdao/rails that referenced this pull request May 1, 2023
Follow-up to rails#46049 and rails#47354.

Since `Rails.deprecator` is now a new `ActiveSupport::Deprecation`
instance instead of `ActiveSupport::Deprecation.instance`, it makes
sense to register it as `Rails.application.deprecators[:railties]`
instead of `Rails.application.deprecators[:rails]`.
zzak referenced this pull request in carrierwaveuploader/carrierwave Jul 2, 2024
Irving-Betterment added a commit to Irving-Betterment/uncruft that referenced this pull request Jan 23, 2025
rzane pushed a commit to Betterment/uncruft that referenced this pull request Jan 23, 2025
[`Rails.application.deprecators` were introduced on Rails
7.1](rails/rails#46049); this pull request
opt-outs the inclusion of the bundled deprecator in previous versions
still supported by this gem (6.1, 7.0).

Dependencies were updated (`bundle install && bundle exec appraisal
update`) and pinned to supported Ruby versioning bracket.

/task https://app.asana.com/0/1207383500465368/1209229335575544/f
@fxn
Copy link
Member

fxn commented Apr 25, 2025

I am thinking of a use case that may not fit well with the current design/APIs.

I am implementing support for deprecating associations:

class User < ApplicationRecord
  has_many :posts, deprecated: true
end

The idea is that whenever the association is used (say, User.includes(:posts).where(...)), you get a warning. That would be a preliminary step before being able to delete an association.

OK. The deprecation is checked and issued by AR, but it is not a deprecation of the library itself, nor does it have an horizon. It is an application-level deprecation issued by AR.

Does it make sense to extend deprecations to support this use case?

@fxn
Copy link
Member

fxn commented Apr 25, 2025

I have this working as a monkey patch in a client. That implementation just publishes an AS notification, so it is the subscriber in the application who has full control about what to do. It has also the benefit of receiving the reflection in the payload, instead of a string.

I like the flexibility of this approach, it has been helpful in different ways. But, while upstreaming, I am wondering if users would expect a regular deprecation instead.

@jonathanhefner
Copy link
Member Author

@fxn

OK. The deprecation is checked and issued by AR, but it is not a deprecation of the library itself, nor does it have an horizon. It is an application-level deprecation issued by AR.

Does it make sense to extend deprecations to support this use case?

I think it makes sense. Thinking of deprecator objects as communication channels, it seems reasonable to have a deprecator for each "class" of message a user may be interested in. So we could have a Rails.application.deprecators[:active_record_associations] deprecator that a user could configure independently of Rails.application.deprecators[:active_record].

I like the flexibility of this approach, it has been helpful in different ways. But, while upstreaming, I am wondering if users would expect a regular deprecation instead.

I guess it depends on how the user wants to track / handle the deprecation. In particular, do they want the DX of a regular deprecation when running their test suite?

@fxn
Copy link
Member

fxn commented Apr 25, 2025

I guess it depends on how the user wants to track / handle the deprecation. In particular, do they want the DX of a regular deprecation when running their test suite?

In my real usage, in production there is no warning issued. Metadata about the reflection and santizied backtrace are sent to Datadog as a structured event.

Use case is that I want to delete the association, CI issues no warnings, but I want to watch production before deleting.

Logs are capped due to their volume, so a warning could be dropped and you'd miss usage. Additionally, the event allows for fine structured queries in Datadog.

@fxn
Copy link
Member

fxn commented Apr 29, 2025

One possibility would be a config option per environment that can be :warn or :notify.

@fxn
Copy link
Member

fxn commented Apr 29, 2025

Though, I'd like to explore if I can make an application deprecator receive a payload with a reflection object.

@jonathanhefner
Copy link
Member Author

Though, I'd like to explore if I can make an application deprecator receive a payload with a reflection object.

One way would be as an additional argument to the behavior callback, but we currently accommodate multiple arities, and adding another argument might be challenging. 😬

Another way would be to pass the payload as the first argument (e.g. as { message: "...", reflection: x }), but obviously that has backward compatibility concerns.

Perhaps we could add an attribute to ActiveSupport::Deprecation that indicates whether the deprecator's behavior callbacks support rich payloads. The default would be false, in which case, if the deprecator receives a Hash, it would extract :message before passing it to behavior callbacks. Otherwise, the deprecator would pass the payload as is. (And we would update the DEFAULT_BEHAVIORS callbacks to handle either case.)

@fxn
Copy link
Member

fxn commented Apr 30, 2025

Yes, I saw that support for multiple signatures which complicates things.

I have not thought about this in detail, but an idea that I had in mind was to introduce a new type of deprecator, an application deprecator. Because here, the responsibilities are kind of reversed, it is not Active Record deprecating something, it is the application deprecating something, only the logic for triggering that is within AR.

So, an application deprecator would not have a horizon, and would have some API that receives a payload. The deprecator would be responsible for warning, or sending an event, building a usage backlog in CI, or whatever logic it wants.

That is a raw idea, don't know if it is any good.

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

Successfully merging this pull request may close these issues.

4 participants