Giter VIP home page Giter VIP logo

noticed's Introduction

Noticed

🎉 Notifications for your Ruby on Rails app.

Build Status Gem Version

Important

⚠️ Upgrading from V1? Read the Upgrade Guide!

Noticed is a gem that allows your application to send notifications of varying types, over various mediums, to various recipients. Be it a Slack notification to your own team when some internal event occurs or a notification to your user, sent as a text message, email, and real-time UI element in the browser, Noticed supports all of the above (at the same time)!

Noticed implements two top-level types of delivery methods:

  1. Individual Deliveries: Where each recipient gets their own notification
Show Example

Let’s use a car dealership as an example here. When someone purchases a car, a notification will be sent to the buyer with some contract details (“Congrats on your new 2024 XYZ Model...”), another to the car sales-person with different details (“You closed X deal; your commission is Y”), and another to the bank handling the loan with financial details (“New loan issued; amount $20,000...”). The event (the car being sold) necessitates multiple notifications being sent out to different recipients, but each contains its own unique information and should be separate from the others. These are individual deliveries.

  1. Bulk Deliveries: One notification for all recipients. This is useful for sending a notification to your Slack team, for example.
Show Example Let’s continue with the car-sale example here. Consider that your development team created the car-sales application that processed the deal above and sent out the notifications to the three parties. For the sake of team morale and feeling the ‘wins’, you may want to implement a notification that notifies your internal development team whenever a car sells through your platform. In this case, you’ll be notifying many people (your development team, maybe others at your company) but with the same content (“someone just bought a car through our platform!”). This is a bulk delivery. It’s generally a single notification that many people just need to be made aware of.

Bulk deliveries are typically used to push notifications to other platforms where users are managed (Slack, Discord, etc.) instead of your own.

Delivery methods we officially support:

Bulk delivery methods we support:

🎬 Screencast

Watch Screencast

🚀 Installation

Run the following command to add Noticed to your Gemfile:

bundle add "noticed"

Generate then run the migrations:

rails noticed:install:migrations
rails db:migrate

📝 Usage

Noticed operates with a few constructs: Notifiers, delivery methods, and Notification records.

To start, generate a Notifier:

rails generate noticed:notifier NewCommentNotifier

Usage Contents

Notifier Objects

Notifiers are essentially the controllers of the Noticed ecosystem and represent an Event. As such, we recommend naming them with the event they model in mind — be it a NewSaleNotifier, ChargeFailureNotifier, etc.

Notifiers must inherit from Noticed::Event. This provides all of their functionality.

A Notifier exists to declare the various delivery methods that should be used for that event and any notification helper methods necessary in those delivery mechanisms. In this example we’ll deliver by :action_cable to provide real-time UI updates to users’ browsers, :email if they’ve opted into email notifications, and a bulk notification to :discord to tell everyone on the Discord server there’s been a new comment.

# ~/app/notifiers/new_comment_notifier.rb

class NewCommentNotifier < Noticed::Event
  deliver_by :action_cable do |config|
    config.channel = "NotificationsChannel"
    config.stream = :some_stream
  end

  deliver_by :email do |config|
    config.mailer = "CommentMailer"
    config.if = ->(recipient) { !!recipient.preferences[:email] }
  end

  bulk_deliver_by :discord do |config|
    config.url = "https://discord.com/xyz/xyz/123"
    config.json = -> {
      {
        message: message,
        channel: :general
      }
    }
  end

  notification_methods do
    # I18n helpers
    def message
      t(".message")
    end

    # URL helpers are accessible in notifications
    # Don't forget to set your default_url_options so Rails knows how to generate urls
    def url
      user_post_path(recipient, params[:post])
    end
  end
end

For deeper specifics on setting up the :action_cable, :email, and :discord (bulk) delivery methods, refer to their docs: action_cable, email, and discord (bulk).

Delivery Method Configuration

Each delivery method can be configured with a block that yields a config object.

Procs/Lambdas will be evaluated when needed and symbols can be used to call a method.

When a lambda is passed, it will not pass any arguments and evaluates the Proc in the context of the Noticed::Notification

If you are using a symbol to call a method, we pass the notification object as an argument to the method. This allows you to access the notification object within the method. Your method must accept a single argument. If you don't need to use the object you can just use (*).

Show Example
class CommentNotifier < Noticed::Event
  deliver_by :ios do |config|
    config.format = :ios_format
    config.apns_key = :ios_cert
    config.key_id = :ios_key_id
    config.team_id = :ios_team_id
    config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_identifier)
    config.device_tokens = :ios_device_tokens
    config.if = -> { recipient.send_ios_notification? }
  end

  def ios_format(apn)
    apn.alert = { title:, body: }
    apn.mutable_content = true
    apn.content_available = true
    apn.sound = "notification.m4r"
    apn.custom_payload = {
      url:,
      type: self.class.name,
      id: record.id,
      image_url: "" || image_url,
      params: params.to_json
    }
  end

  def ios_cert(*)
    Rails.application.credentials.dig(:ios, Rails.env.to_sym, :apns_token_cert)
  end

  def ios_key_id(*)
    Rails.application.credentials.dig(:ios, Rails.env.to_sym, :key_id)
  end

  def ios_team_id(*)
    Rails.application.credentials.dig(:ios, Rails.env.to_sym, :team_id)
  end

  def ios_bundle_id(*)
    Rails.application.credentials.dig(:ios, Rails.env.to_sym, :bundle_identifier)
  end

  def ios_device_tokens(notification)
    notification.recipient.ios_device_tokens
  end

  def url
    comment_thread_path(record.thread)
  end
end

class Recipient < ApplicationRecord # or whatever your recipient model is
  has_many :ios_device_tokens

  def send_ios_notification?
    # some logic
  end
end

More examples are in the docs for each delivery method.

Required Params

While explicit / required parameters are completely optional, Notifiers are able to opt in to required parameters via the required_params method:

class CarSaleNotifier < Noticed::Event
  deliver_by :email { |c| c.mailer = "BranchMailer" }

  # `record` is the Car record, `Branch` is the dealership
  required_params :branch

  # To validate the `:record` param, add a validation since it is an association on the Noticed::Event
  validates :record, presence: true
end

Which will validate upon any invocation that the specified parameters are present:

CarSaleNotifier.with(record: Car.last).deliver(Branch.last)
#=> Noticed::ValidationError("Param `branch` is required for CarSaleNotifier")

CarSaleNotifier.with(record: Car.last, branch: Branch.last).deliver(Branch.hq)
#=> OK

Helper Methods

Notifiers can implement various helper methods, within a notification_methods block, that make it easier to render the resulting notification directly. These helpers can be helpful depending on where and how you choose to render notifications. A common use is rendering a user’s notifications in your web UI as standard ERB. These notification helper methods make that rendering much simpler:

<div>
  <% @user.notifications.each do |notification| %>
    <%= link_to notification.message, notification.url %>
  <% end %>
</div>

On the other hand, if you’re using email delivery, ActionMailer has its own full stack for setting up objects and rendering. Your notification helper methods will always be available from the notification object, but using ActionMailer’s own paradigms may fit better for that particular delivery method. YMMV.

URL Helpers

Rails url helpers are included in Notifiers by default so you have full access to them in your notification helper methods, just like you would in your controllers and views.

But don't forget, you'll need to configure default_url_options in order for Rails to know what host and port to use when generating URLs.

Rails.application.routes.default_url_options[:host] = 'localhost:3000'

Translations

We've also included Rails’ translate and t helpers for you to use in your notification helper methods. This also provides an easy way of scoping translations. If the key starts with a period, it will automatically scope the key under notifiers, the underscored name of the notifier class, and notification. For example:

From the above Notifier...

class NewCommentNotifier < Noticed::Event
  # ...

  notification_methods do
    def message
      t(".message")
    end
  end

  # ...
end

Calling the message helper in an ERB view:

<%= @user.notifications.last.message %>

Will look for the following translation path:

# ~/config/locales/en.yml

en:
  notifiers:
    new_comment_notifier:
      notification:
        message: "Someone posted a new comment!"

Or, if you have your Notifier within another module, such as Admin::NewCommentNotifier, the resulting lookup path will be en.notifiers.admin.new_comment_notifier.notification.message (modules become nesting steps).

Tip: Capture User Preferences

You can use the if: and unless: options on your delivery methods to check the user's preferences and skip processing if they have disabled that type of notification.

For example:

class CommentNotifier < Noticed::Event
  deliver_by :email do |config|
    config.mailer = 'CommentMailer'
    config.method = :new_comment
    config.if = ->{ recipient.email_notifications? }
  end
end

If you would like to skip the delivery job altogether, for example if you know that a user doesn't support the platform and you would like to save resources by not enqueuing the job, you can use before_enqueue.

For example:

class IosNotifier < Noticed::Event
  deliver_by :ios do |config|
    # ...
    config.before_enqueue = ->{ throw(:abort) unless recipient.registered_ios? }
  end
end

Tip: Extracting Delivery Method Configurations

If you want to reuse delivery method configurations across multiple Notifiers, you can extract them into a module and include them in your Notifiers.

# /app/notifiers/notifiers/comment_notifier.rb
class CommentNotifier < Noticed::Event
  include IosNotifier
  include AndriodNotifer
  include EmailNotifier

  validates :record, presence: true
end

# /app/notifiers/concerns/ios_notifier.rb
module IosNotifier
  extend ActiveSupport::Concern

  included do
    deliver_by :ios do |config|
      config.device_tokens = ->(recipient) { recipient.notification_tokens.where(platform: :iOS).pluck(:token) }
      config.format = ->(apn) {
        apn.alert = "Hello world"
        apn.custom_payload = {url: root_url(host: "example.org")}
      }
      config.bundle_identifier = Rails.application.credentials.dig(:ios, :bundle_id)
      config.key_id = Rails.application.credentials.dig(:ios, :key_id)
      config.team_id = Rails.application.credentials.dig(:ios, :team_id)
      config.apns_key = Rails.application.credentials.dig(:ios, :apns_key)
      config.if = -> { recipient.ios_notifications? }
    end
  end
end

Shared Delivery Method Options

Each of these options are available for every delivery method (individual or bulk). The value passed may be a lambda, a symbol that represents a callable method, a symbol value, or a string value.

  • config.if — Intended for a lambda or method; runs after the wait if configured; cancels the delivery method if returns falsey
  • config.unless — Intended for a lambda or method; runs after the wait if configured; cancels the delivery method if returns truthy
  • config.wait — (Should yield an ActiveSupport::Duration) Delays the job that runs this delivery method for the given duration of time
  • config.wait_until — (Should yield a specific time object) Delays the job that runs this delivery method until the specific time specified
  • config.queue — Sets the ActiveJob queue name to be used for the job that runs this delivery method

📨 Sending Notifications

Following the NewCommentNotifier example above, here’s how we might invoke the Notifier to send notifications to every author in the thread about a new comment being added:

NewCommentNotifier.with(record: @comment, foo: "bar").deliver(@comment.thread.all_authors)

This instantiates a new NewCommentNotifier with params (similar to ActiveJob, any serializable params are permitted), then delivers notifications to all authors in the thread.

✨ The record: param is a special param that gets assigned to the record polymorphic association in the database. You should try to set the record: param where possible. This may be best understood as ‘the record/object this notification is about’, and allows for future queries from the record-side: “give me all notifications that were generated from this comment”.

This invocation will create a single Noticed::Event record and a Noticed::Notification record for each recipient. A background job will then process the Event and fire off a separate background job for each bulk delivery method and each recipient + individual-delivery-method combination. In this case, that’d be the following jobs kicked off from this event:

  • A bulk delivery job for :discord bulk delivery
  • An individual delivery job for :action_cable method to the first thread author
  • An individual delivery job for :email method to the first thread author
  • An individual delivery job for :action_cable method to the second thread author
  • An individual delivery job for :email method to the second thread author
  • Etc...

Custom Noticed Model Methods

In order to extend the Noticed models you'll need to use a concern and a to_prepare block:

# config/initializers/noticed.rb
module NotificationExtensions
  extend ActiveSupport::Concern

  included do
    belongs_to :organization

    scope :filter_by_type, ->(type) { where(type:) }
    scope :exclude_type, ->(type) { where.not(type:) }
  end

  # You can also add instance methods here
end

Rails.application.config.to_prepare do
  # You can extend Noticed::Event or Noticed::Notification here
  Noticed::Event.include EventExtensions
  Noticed::Notification.include NotificationExtensions
end

The NotificationExtensions class could be separated into it's own file and live somewhere like app/models/concerns/notification_extensions.rb.

If you do this, the to_prepare block will need to be in application.rb instead of an initializer.

# config/application.rb
module MyApp
  class Application < Rails::Application

    # ...

    config.to_prepare do
      Noticed::Event.include Noticed::EventExtensions
      Noticed::Notification.include Noticed::NotificationExtensions
    end
  end
end

✅ Best Practices

Renaming Notifiers

If you rename a Notifier class your existing data and Noticed setup may break. This is because Noticed serializes the class name and sets it to the type column on the Noticed::Event record and the type column on the Noticed::Notification record.

When renaming a Notifier class you will need to backfill existing Events and Notifications to reference the new name.

Noticed::Event.where(type: "OldNotifierClassName").update_all(type: NewNotifierClassName.name)
# and
Noticed::Notification.where(type: "OldNotifierClassName::Notification").update_all(type: "#{NewNotifierClassName.name}::Notification")

🚛 Delivery Methods

The delivery methods are designed to be modular so you can customize the way each type gets delivered.

For example, emails will require a subject, body, and email address while an SMS requires a phone number and simple message. You can define the formats for each of these in your Notifier and the delivery method will handle the processing of it.

Individual delivery methods:

Bulk delivery methods:

No Delivery Methods

It’s worth pointing out that you can have a fully-functional and useful Notifier that has no delivery methods. This means that invoking the Notifier and ‘sending’ the notification will only create new database records (no external surfaces like email, sms, etc.). This is still useful as it’s the database records that allow your app to render a user’s (or other object’s) notifications in your web UI.

So even with no delivery methods set, this example is still perfectly available and helpful:

<div>
  <% @user.notifications.each do |notification| %>
    <%= link_to notification.message, notification.url %>
  <% end %>
</div>

Sending a notification is entirely an internal-to-your-app function. Delivery methods just get the word out! But many apps may be fully satisfied without that extra layer.

Fallback Notifications

A common pattern is to deliver a notification via a real (or real-ish)-time service, then, after some time has passed, email the user if they have not yet read the notification. You can implement this functionality by combining multiple delivery methods, the wait option, and the conditional if / unless option.

class NewCommentNotifier < Noticed::Event
  deliver_by :action_cable
  deliver_by :email do |config|
    config.mailer = "CommentMailer"
    config.wait = 15.minutes
    config.unless = -> { read? }
  end
end

Here a notification will be created immediately in the database (for display directly in your app’s web interface) and sent via ActionCable. If the notification has not been marked read after 15 minutes, the email notification will be sent. If the notification has already been read in the app, the email will be skipped.

A note here: notifications expose a #mark_as_read method, but your app must choose when and where to call that method.

You can mix and match the options and delivery methods to suit your application specific needs.

🚚 Custom Delivery Methods

If you want to build your own delivery method to deliver notifications to a specific service or medium that Noticed doesn’t (or doesn’t yet) support, you’re welcome to do so! To generate a custom delivery method, simply run

rails generate noticed:delivery_method Discord

This will generate a new ApplicationDeliveryMethod and DeliveryMethods::Discord class inside the app/notifiers/delivery_methods folder, which can be used to deliver notifications to Discord.

class DeliveryMethods::Discord < ApplicationDeliveryMethod
  # Specify the config options your delivery method requires in its config block
  required_options # :foo, :bar

  def deliver
    # Logic for sending the notification
  end
end

You can use the custom delivery method thus created by adding a deliver_by line with a unique name and class option in your notification class.

class MyNotifier < Noticed::Event
  deliver_by :discord, class: "DeliveryMethods::Discord"
end
Turbo Stream Custom Delivery Method Example

A common custom delivery method in the Rails world might be to Delivery to the web via turbo stream.

Note: This example users custom methods that extend the Noticed::Notification class.

See the Custom Noticed Model Methods section for more information.

# app/notifiers/delivery_methods/turbo_stream.rb

class DeliveryMethods::TurboStream < ApplicationDeliveryMethod
  def deliver
    return unless recipient.is_a?(User)

    notification.broadcast_update_to_bell
    notification.broadcast_replace_to_index_count
    notification.broadcast_prepend_to_index_list
  end
end
# app/models/concerns/noticed/notification_extensions.rb

module Noticed::NotificationExtensions
  extend ActiveSupport::Concern

  def broadcast_update_to_bell
    broadcast_update_to(
      "notifications_#{recipient.id}",
      target: "notification_bell",
      partial: "navbar/notifications/bell",
      locals: { user: recipient }
    )
  end

  def broadcast_replace_to_index_count
    broadcast_replace_to(
      "notifications_index_#{recipient.id}",
      target: "notification_index_count",
      partial: "notifications/notifications_count",
      locals: { count: recipient.reload.notifications_count, unread: recipient.reload.unread_notifications_count }
    )
  end

  def broadcast_prepend_to_index_list
    broadcast_prepend_to(
      "notifications_index_list_#{recipient.id}",
      target: "notifications",
      partial: "notifications/notification",
      locals: { notification: self }
    )
  end
end

Delivery methods have access to the following methods and attributes:

  • event — The Noticed::Event record that spawned the notification object currently being delivered
  • record — The object originally passed into the Notifier as the record: param (see the ✨ note above)
  • notification — The Noticed::Notification instance being delivered. All notification helper methods are available on this object
  • recipient — The individual recipient object being delivered to for this notification (remember that each recipient gets their own instance of the Delivery Method #deliver)
  • config — The hash of configuration options declared by the Notifier that generated this notification and delivery
  • params — The parameters given to the Notifier in the invocation (via .with())

Validating config options passed to Custom Delivery methods

The presence of delivery method config options are automatically validated when declaring them with the required_options method. In the following example, Noticed will ensure that any Notifier using deliver_by :email will specify the mailer and method config keys:

class DeliveryMethods::Email < Noticed::DeliveryMethod
  required_options :mailer, :method

  def deliver
    # ...
    method = config.method
  end
end

If you’d like your config options to support dynamic resolution (set config.foo to a lambda or symbol of a method name etc.), you can use evaluate_option:

class NewSaleNotifier < Noticed::Event
  deliver_by :whats_app do |config|
    config.day = -> { is_tuesday? "Tuesday" : "Not Tuesday" }
  end
end

class DeliveryMethods::WhatsApp < Noticed::DeliveryMethod
  required_options :day

  def deliver
    # ...
    config.day #=> #<Proc:0x000f7c8 (lambda)>
    evaluate_option(:day) #=> "Tuesday"
  end
end

Callbacks

Callbacks for delivery methods wrap the actual delivery of the notification. You can use before_deliver, around_deliver and after_deliver in your custom delivery methods.

class DeliveryMethods::Discord < Noticed::DeliveryMethod
  after_deliver do
    # Do whatever you want
  end
end

📦 Database Model

The Noticed database models include several helpful features to make working with notifications easier.

Notification

Class methods/scopes

(Assuming your user has_many :notifications, as: :recipient, class_name: "Noticed::Notification")

Sorting notifications by newest first:

@user.notifications.newest_first

Query for read or unread notifications:

user.notifications.read
user.notifications.unread

Marking all notifications as read or unread:

user.notifications.mark_as_read
user.notifications.mark_as_unread

Instance methods

Mark notification as read / unread:

@notification.mark_as_read
@notification.mark_as_read!
@notification.mark_as_unread
@notification.mark_as_unread!

Check if read / unread:

@notification.read?
@notification.unread?

Associating Notifications

Adding notification associations to your models makes querying, rendering, and managing notifications easy (and is a pretty critical feature of most applications).

There are two ways to associate your models to notifications:

  1. Where your object has_many notifications as the recipient (who you sent the notification to)
  2. Where your object has_many notifications as the record (what the notifications were about)

In the former, we’ll use a has_many to :notifications. In the latter, we’ll actually has_many to :events, since records generate notifiable events (and events generate notifications).

We can illustrate that in the following:

class User < ApplicationRecord
  has_many :notifications, as: :recipient, dependent: :destroy, class_name: "Noticed::Notification"
end

# All of the notifications the user has been sent
# @user.notifications.each { |n| render(n) }

class Post < ApplicationRecord
  has_many :noticed_events, as: :record, dependent: :destroy, class_name: "Noticed::Event"
  has_many :notifications, through: :noticed_events, class_name: "Noticed::Notification"
end

# All of the notification events this post generated
# @post.notifications

ActiveJob Parent Class

Noticed uses its own Noticed::ApplicationJob as the base job for all notifications. In the event that you would like to customize the parent job class, there is a parent_class attribute that can be overridden with your own class. This should be done in a noticed.rb initializer.

Noticed.parent_class = "ApplicationJob"

Handling Deleted Records

Generally we recommend using a dependent: ___ relationship on your models to avoid cases where Noticed Events or Notifications are left lingering when your models are destroyed. In the case that they are or data becomes mis-matched, you’ll likely run into deserialization issues. That may be globally alleviated with the following snippet, but use with caution.

class ApplicationJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError
end

Customizing the Database Models

You can modify the database models by editing the generated migrations.

One common adjustment is to change the IDs to UUIDs (if you're using UUIDs in your app).

You can also add additional columns to the Noticed::Event and Noticed::Notification models.

# This migration comes from noticed (originally 20231215190233)
class CreateNoticedTables < ActiveRecord::Migration[7.1]
  def change
    create_table :noticed_events, id: :uuid do |t|
      t.string :type
      t.belongs_to :record, polymorphic: true, type: :uuid
      t.jsonb :params

      # Custom Fields
      t.string :organization_id, type: :uuid, as: "((params ->> 'organization_id')::uuid)", stored: true
      t.virtual :action_type, type: :string, as: "((params ->> 'action_type'))", stored: true
      t.virtual :url, type: :string, as: "((params ->> 'url'))", stored: true

      t.timestamps
    end

    create_table :noticed_notifications, id: :uuid do |t|
      t.string :type
      t.belongs_to :event, null: false, type: :uuid
      t.belongs_to :recipient, polymorphic: true, null: false, type: :uuid
      t.datetime :read_at
      t.datetime :seen_at

      t.timestamps
    end

    add_index :noticed_notifications, :read_at
  end
end

The custom fields in the above example are stored as virtual columns. These are populated from values passed in the params hash when creating the notifier.

🙏 Contributing

This project uses Standard for formatting Ruby code. Please make sure to run standardrb before submitting pull requests.

Running tests against multiple databases locally:

DATABASE_URL=sqlite3:noticed_test rails test
DATABASE_URL=trilogy://root:@127.0.0.1/noticed_test rails test
DATABASE_URL=postgres://127.0.0.1/noticed_test rails test

📝 License

The gem is available as open source under the terms of the MIT License.

noticed's People

Contributors

avanrielly avatar cjilbert504 avatar dependabot[bot] avatar excid3 avatar jespr avatar joemasilotti avatar jon-sully avatar jotolo avatar justwiebe avatar klaaspieter avatar kylekeesling avatar leighhalliday avatar liroyleshed avatar lorint avatar manojmj92 avatar marcelokanzaki avatar marckohlbrugge avatar marcoroth avatar mark-kraemer avatar martinjaimem avatar michaelbaudino avatar michaellambs avatar mike-burns avatar mikelkew avatar patmisch avatar phil-6 avatar quadule avatar rbague avatar rromanchuk avatar spone avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

noticed's Issues

Defaults options to delivery methods

To favor sub-classing, allow default options to be defined for a notification. Here goes a suggestion:

class ApplicationNotification < Noticed::Base
  deliver_by_defaults :database, format: :database_format
  deliver_by_defaults :action_cable, format: :action_cable_format
  deliver_by_defaults :twilio, credentials: :twilio_credentials

  def database_format
    {
      'type': self.class.name,
      'params': params
    }
  end

  def action_cable_format
    params
  end

  def twilio_credentials
    {
      account_sid: ENV['TWILIO_ACCOUNT_SID'],
      auth_token: ENV['TWILIO_AUTH_TOKEN'],
      phone_number: ENV['TWILIO_PHONE_NUMBER']
    }
  end
end


class CommentCreatedNotification < ApplicationNotification
  deliver_by :database
  deliver_by :action_cable
  deliver_by :twilio
end

This wold work nice with #94 as well.

uninitialized constant CommentNotification

In trying to learn this gem, I created a brand new Rails app and walked thru the installation instructions. However, after having run the generator to get a Notification class, I can't access it from the rails console. You can find my repo here: https://github.com/fractaledmind/noticed-sandbox

And here is the output from my console

± bundle exec rails c
Running via Spring preloader in process 87965
Loading development environment (Rails 6.0.3.4)
irb(main):001:0> CommentNotification
Traceback (most recent call last):
        1: from (irb):1
NameError (uninitialized constant CommentNotification)
irb(main):002:0> require_relative 'app/notifications/comment_notification'
=> true
irb(main):003:0> CommentNotification
=> CommentNotification

It appears that the app/notifications/ directory isn't being auto-loaded. But I was under the assumption that everything under app/ got auto-loaded, so I'm confused.

The gem noticed causes failure on Apple Silicon Ruby on Rails apps.

Installing of the gem noticed causes, in a jumpstart saas template, on Apple Silicon Ruby on Rails application to throw the following error when running the rails server, and or console commands.

"gems/http-parser-1.2.2/ext/aarch64-darwin/libhttp-parser-ext.bundle: mach-o, but wrong architecture"

Commenting out:

gem "noticed", "~> 1.2"

Solve this issue and rails s, c will then run.

Error connecting controller

I have not implemented any notifications yet. I just included the 'noticed' gem in my application (note: I am using JumpStart Pro and merged the update).

In my console, I receive this error on every page:
Screenshot 2020-08-04 10 17 07

I'm not sure if there is something wrong with my project setup only or if other users could encounter this same issue.

How can we go about fixing this?

Thanks!

Checking if notification is read before sending email/text

Hi, I'm not sure if there is a better way to do this, but I wanted to see if there is a standard way to delay the deliver_by email/twilio methods and check if the notification is read off the database before sending. Right now, I have it built into my conditional check in the delivery method.

deliver_by :email, mailer: "UserMailer", method: :message_notification, if: :email_notifications?

def email_notifications?
  sleep(60)
  record.reload
  if record.read?
    return false
  else
...

Send to multiple recipients

Allow passing multiple users into deliver and deliver_later. We can loop through each of them and send notifications for them.

We will need to figure out what to do with callbacks. They could run once for each recipient or for the entire block? Or do we introduce a different interface for sending to multiple?

  • Accepts one or many recipients (use Array.wrap to make them consistent)
  • Calls uniq on the list of recipients
  • Correctly handle with callbacks

Batch Deliveries

From @SirRawlins

Batching is something I'd love to see.

If we have a notification to deliver to several thousand recipients, then some mail services such a Postmark/Sendgrid/Mailgun allow you to batch send 1,000 messages at a time, converting 000s of API requests into a single call.

Being able to defineDeliveryMethod classes that were instantiated and enqueued with a collection of recipients, instead of just a single recipient would be nice.

Something like this pseudocode is what I imagine:

class DeliveryMethods::Postmark < Noticed::DeliveryMethods::Base

    # Define batch size on the delivery method class.
    deliver_in_batches_of 1_000

    def deliver
        post "https://api.postmarkapp.com/email/send", {
            # Has access to 1_000 recipients, instead of single recipient.
            to: recipients.map(&:email),
            subject: notification.subject
        }
    end

end

class GroupNotification < Noticed::Base

    deliveryby :database
    deliver_by :twilio
    deliver_by :postmark

end

User.all.count
# => 5000

GroupNotification.deliver_later(User.all)
# => Enqueues DeliveryMethods::Database.deliver 5,000 times.
# => Enqueues DeliveryMethods::Twilio.deliver 5,000 times.
# => Enqueues DeliveryMethods::Postmark.deliver 5 times.

This would require some refactoring of the delivery, deliver_later and run_delivery methods in Noticed::Base to batch and loop recipients if the DeliveryMethod supported it.

Not sure if the gains in efficiency and scalability would make it worthwhile though?

Originally posted by @SirRawlins in #83 (comment)

ArgumentError (Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true)

Trying to use url helpers to get the full url inside a Notification returns an error because, apparently, default_url_options is not defined inside a Notification object.

def url
   video_show_url(slug: params[:video].slug)
 end

image

I fixed this by adding the following code inside my NewVideoNotification class.

private
  def default_url_options
    Rails.application.config.default_url_options
  end

Maybe this could be added to Noticed::Base.

How to handle Notifications that have had a param deleted

I've ran into a problem that I'm hoping we can brainstorm on.

I have a Comment::PostedNotifcation that is created anytime someone posts a comment, and it looks something like this:

class Comment::PostedNotification < Noticed::Base
  deliver_by :database
  deliver_by :email, mailer: "CommentMailer", method: "posted"

  param :comment

  def message
    "new comment"
  end

  def url
    params[:comment].commentable_path
  end
end

It works great most of the time, but I allow users to delete their comments, and once that happens, and I attempt to load a user's notifications in a view I run into the following error:

ActionView::Template::Error - Couldn't find Comment with 'id'=

I take that to mean that while the view is re-hydrating the Notification record, it's attempting to look up the now-deleted comment via its SGID, but it's no longer there, and 💥 goes the dynamite.

I've begun to dig into the code and from the looks of it the ActiveJob::Arguments deserializier is the point of contention, but I'm not sure where to go beyond that.

My first thought was to add a after_destroy callback to my Comment model to go back and destroy the related Notification records, but I'm not sure the best way to query based upon the serialized params field.

Can you think of anyway to address this problem? Happy to help with the solution if we can come up with one.

`mark_as_unread!` does not work for association collections

Hey!

This gem is a blessing and saved so much time already. I ran into this obscure use case where I'd need to mark a bunch of notifications as unread and the gem doesn't seem to support it (yet). 🤷‍♂️

Gem version: 1.2.19

Expected Behavior

Running user.notifications.mark_as_unread! should mark the notification records as not read.

Actual Behavior

Running user.notifications.mark_as_unread! throws an undefined method 'mark_as_unread!' for #<Notification::ActiveRecord_Associations_CollectionProxy:0x0000000000000> error.

Additional Information

  • Running user.notifications.mark_as_read! works as one would expect.
  • Documenting association methods would be a great add as well! 📚

Problem when deploying gem and ActionCable is not configured

Last night I finally deployed my implementation of your gem to production 🎉

That said I ran into an issue that caused me some downtime, so I'm hoping to bringing it to your attention to see if there is any kind of safeguard/solution we can come up with to prevent this for other folks.

My app pre-dates ActionCable, and I've yet to actually use it for anything, including the integration with this gem, so I don't have anything built out in the channels directory.

Turns out though, that there is code in the gem (found here) that assumes that channels/application_cable/channel.rb exists, and since eager loading/caching is only enabled in production I didn't find that out until my code was deployed 😅

I know that this identifies a gap in my deployment/testing strategy but I'm wondering if there's a way we could have some sort of check (potentially upon the gem's installation/in one of the generators) that would make sure that all necessary files/classes exist, or is it possible that this code can be optional dependent about whether the app utilizes ActionCable?

Bug: delay: 5.minutes, unless: :read? shows undefined method read?

Hello, first of all, cool stuff with this gem. I just moved away from Activity Notifications (a does-it-all gem) to this one, because in the end an all inclusive gem never does exactly what you want.

So, maybe I'm doing something wrong, but I'm following what's explained here: https://github.com/excid3/noticed#fallback-notifications.

This gives me an error:

NoMethodError - undefined method `read?' for #<ReportNotification:0x000000001cf7bdd8>

Or what ActiveJob / Sidekiq is throwing:

[ActiveJob] [NotifyAboutReportJob] [8f6b2ac5-0dde-4c8e-8d8c-6cb8f64c1bb5] [Noticed::DeliveryMethods::Email] [c340b4f2-bdee-44a0-a1cc-7e45b3452304] Error performing Noticed::DeliveryMethods::Email (Job ID: c340b4f2-bdee-44a0-a1cc-7e45b3452304) from Sidekiq(default) in 5.94ms: NoMethodError (undefined method `read?' for #<ReportNotification:0x000000001cf7bdd8>

Trying to debug this a little bit, it seems like the notification class isn't inheriting the functions of its parent:

class ReportNotification < Noticed::Base
  deliver_by :database
  deliver_by :email, mailer: "UserMailer", method: :report_summary, format: :format_for_email, delay: 5.seconds, unless: :is_read
  param :report

  def is_read
    pp self # <-- prints okay
    pp self.read_at # <-- throws error undefined method
    pp self.read? # <-- undefined method
    self.read?
  end
end

The weird thing is that printing self does show read_at as an attribute:

#<ReportNotification:0x000000001f3fd260
 @params=
  {:report=>
    #<Report:0x000000001f3ac478 [...]>},
 @recipient=
  #<User [...]>,
 @record=
  #<Notification:0x000000001f3eeeb8
   id: 15,
   recipient_type: "User",
   recipient_id: 2,
   type: "ReportNotification",
    {:report=>
      #<Report:0x000000001f470468>},
   read_at: nil,
   created_at: Mon, 28 Dec 2020 11:02:45 UTC +00:00,
   updated_at: Mon, 28 Dec 2020 11:02:45 UTC +00:00>>

While writing this I see I can solve the issue by doing:

def is_read?
  self.record.read?
end

But that's not in line with the documentation.

Possibly I have a use case that's not tested:

  1. Trigger an Active Job to find all users to be notified
  2. Trigger the ReportNotification.deliver_later from there
  3. Delay sending an email unless the notification is read withing 5 minutes.

Anyway, what do you think? Let me know if you need more (structured) details.

Add delay to entire notification

We just added delays for individual delivery methods which is great (thanks @rbague!).

@gathuku mentioned it would be great to delay the entire notification: #61 (comment)

In my use case am using a custom delivery to send SMS, I wanted a way where you can schedule an SMS to be sent later(at a specific dateTime). This means you will pick dateTime when you want the SMS to be delivered. What I tried is to get the chosen DateTime difference with Time.now in minutes pass it to my notification instance as params and finally set it as delay in the delivery method.

I think this makes sense to do and think we could probably allow similar syntax as ActiveJob:

Job.set(wait: 1.week).perform_later
Job.set(wait_until: Date.tomorrow.noon).perform_later

We should support .set(**options) syntax and simply pass those along on a deliver_later call. This would keep the interface nicely similar to ActiveJob and flexible.

If we collect all the options and simply pass them along to ActiveJob, we can safely support all future features/changes that ActiveJob might have.

Strategy for fallback notifications?

Would be great to support (or document) how to handle this common use-case:

  • Notify a user via database / in-app notification
  • If user has not marked notification as read / seen after a certain time, notify via email or other channel.

Run database delivery method first

For convenience, it'd be great to have the Database delivery method run before the others (and before they run as a job).

The reason for this is if we want a notification written to the database, we can pass this record as a param to the other methods and they can link to this notification if they want.

An example:

  • Send a notification by Database and Websocket
  • Write the database record
  • Render the notification as a partial and then broadcast the partial over websocket
  • Insert the partial into the navbar notifications menu
  • By having the database record's ID in the HTML, we can have Javascript that marks the record as read when it's interacted with.

Caveat:

This needs to run inline before the other methods since each delivery is now a background job. We need to make sure it's written to the db and then passed in as an argument to each.

NameError (undefined local variable or method `params' for Noticed::DeliveryMethods::Slack)

Nice gem!

I'm starting off with a trivial (perhaps too trivial) test of noticed by attempting to send a notification via Slack. However, I'm getting an exception like the following:

NameError (undefined local variable or method `params' for #<Noticed::DeliveryMethods::Slack:0x00007fe84c209270>)

My bare-bones notification class:

class CommentNotification < Noticed::Base
  deliver_by :database
  deliver_by :slack

  param :text
end

And output from Rails console:

2.6.4 :008 > notification = CommentNotification.with(text: "this is the comment text")
 => #<CommentNotification:0x00007fe84c351420 @params={:text=>"this is the comment text"}> 
2.6.4 :009 > notification.deliver(User.all)
  User Load (0.2ms)  SELECT "users".* FROM "users"
Performing Noticed::DeliveryMethods::Database (Job ID: 9dfa7ae6-3273-4bb6-b0cf-fb15a0c0bb4c) from Async(default) enqueued at  with arguments: {:notification_class=>"CommentNotification", :options=>{}, :params=>{:text=>"this is the comment text"}, :recipient=>#<GlobalID:0x00007fe84c368fd0 @uri=#<URI::GID gid://rails-noticed/User/1>>, :record=>nil}
   (0.1ms)  begin transaction
  Notification Create (0.4ms)  INSERT INTO "notifications" ("recipient_type", "recipient_id", "type", "params", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["recipient_type", "User"], ["recipient_id", 1], ["type", "CommentNotification"], ["params", "{\"text\":\"this is the comment text\",\"_aj_symbol_keys\":[\"text\"]}"], ["created_at", "2020-08-06 18:50:33.269880"], ["updated_at", "2020-08-06 18:50:33.269880"]]
   (1.5ms)  commit transaction
Performed Noticed::DeliveryMethods::Database (Job ID: 9dfa7ae6-3273-4bb6-b0cf-fb15a0c0bb4c) from Async(default) in 4.37ms
Performing Noticed::DeliveryMethods::Slack (Job ID: 5e06cff1-5057-4ea7-bcdb-f7c0c1f637ef) from Async(default) enqueued at  with arguments: {:notification_class=>"CommentNotification", :options=>{}, :params=>{:text=>"this is the comment text"}, :recipient=>#<GlobalID:0x00007fe848649e10 @uri=#<URI::GID gid://rails-noticed/User/1>>, :record=>#<GlobalID:0x00007fe8486491e0 @uri=#<URI::GID gid://rails-noticed/Notification/6>>}
Error performing Noticed::DeliveryMethods::Slack (Job ID: 5e06cff1-5057-4ea7-bcdb-f7c0c1f637ef) from Async(default) in 3.33ms: NameError (undefined local variable or method `params' for #<Noticed::DeliveryMethods::Slack:0x00007fe84864bc88>):
/Users/dlite/.rvm/gems/ruby-2.6.4/gems/noticed-1.2.5/lib/noticed/delivery_methods/slack.rb:14:in `format'
/Users/dlite/.rvm/gems/ruby-2.6.4/gems/noticed-1.2.5/lib/noticed/delivery_methods/slack.rb:5:in `deliver'
/Users/dlite/.rvm/gems/ruby-2.6.4/gems/noticed-1.2.5/lib/noticed/delivery_methods/base.rb:20:in `block in perform'
/Users/dlite/.rvm/gems/ruby-2.6.4/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'

Perhaps the params method / variable referenced here does not exist in a parent class? Or my trivial example is missing something obvious?

Unable to access root_url helper in Noticed classes

I'm writing up an additional simple URL helper for Twilio messages to print the full url link so users can visit the site easily from their phones. I figure combing root_url and url_for should do the trick, but it doesn't seem I'm able to access the root_url helper in the Noticed classes.

I know you state in the readme URL helpers are here by default. I've tried including Rails.application.routes.url_helpers to check as well and prying in to the class and calling Rails.application.routes.url_helpers.root_url directly.

Error - ArgumentError: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true

In my test, dev, and prod environments I have specified this (see dev example below), so I'm not sure why it can't access it here. Perhaps I'm missing something obvious - does it not use the below..?

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

NameError - undefined local variable or method `recipient' for multiple deliveries

I am trying to send notifications to all users who are following the user who just posted a Guide. I am using the gem acts_as_favoritor to track which users are following whom. I am using an updated copy of JumpStart Pro and using Sidekiq to handle background jobs.

Here is the notification model new_guide.rb:

class NewGuide < ApplicationNotification
  # Database delivery is already added in ApplicationNotification
  deliver_by :action_cable, format: :to_websocket, channel: "NotificationChannel"

  # Add required params
  param :guide

  # Define helper methods to make rendering easier.
  # `message` and `url` are used for rendering in the navbar

  def message
    t(".new_guide", user: params[:guide].user.username, guide: params[:guide].title)
  end

  def url
    # You can use any URL helpers here such as:
    # post_path(params[:post])
    guide_path(params[:guide])
  end

  # Include account_id to make sure notification only triggers if user is signed in to that account
  def to_websocket
    {
      account_id: record.account_id,
      html: ApplicationController.render(partial: "notifications/notification", locals: {notification: record})
    }
  end
end

This is the portion of code in my guides_controller.rb:

if @guide.save
      following_personal_accounts = @guide.user.favoritors(scope: :user_follow).map(&:personal_account)
      notification = NewGuide.with(guide: @guide)
      notification.deliver_later(@guide.user.favoritors(scope: :user_follow))
      redirect_to @guide, notice: "Guide was successfully created."
 else
      render :new
end

Here is the error in the console:

Favorite Load (0.2ms)  SELECT "favorites".* FROM "favorites" WHERE "favorites"."favoritable_id" = $1 AND "favorites"."favoritable_type" = $2 AND "favorites"."blocked" = $3 AND "favorites"."scope" = $4  [["favoritable_id", 5], ["favoritable_type", "User"], ["blocked", false], ["scope", "user_follow"]]
22:29:43 web.1     |   ↳ app/controllers/guides_controller.rb:50:in `create'
22:29:43 web.1     |   User Load (0.3ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
22:29:43 web.1     |   ↳ app/controllers/guides_controller.rb:50:in `create'
22:29:43 web.1     |   Account Load (0.2ms)  SELECT "accounts".* FROM "accounts" WHERE "accounts"."owner_id" = $1 AND "accounts"."personal" = $2 LIMIT $3  [["owner_id", 7], ["personal", true], ["LIMIT", 1]]
22:29:43 web.1     |   ↳ app/controllers/guides_controller.rb:50:in `map'
22:29:43 web.1     |   CACHE Favorite Load (0.0ms)  SELECT "favorites".* FROM "favorites" WHERE "favorites"."favoritable_id" = $1 AND "favorites"."favoritable_type" = $2 AND "favorites"."blocked" = $3 AND "favorites"."scope" = $4  [["favoritable_id", 5], ["favoritable_type", "User"], ["blocked", false], ["scope", "user_follow"]]
22:29:43 web.1     |   ↳ app/controllers/guides_controller.rb:52:in `create'
22:29:43 web.1     |   CACHE User Load (0.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = $1  [["id", 7]]
22:29:43 web.1     |   ↳ app/controllers/guides_controller.rb:52:in `create'
22:29:43 web.1     | [ActiveJob] [Noticed::DeliveryMethods::Database] [1a803188-b873-4cba-8208-ae49eded519a] Performing Noticed::DeliveryMethods::Database (Job ID: 1a803188-b873-4cba-8208-ae49eded519a) from Sidekiq(default) enqueued at  with arguments: {:notification_class=>"NewGuide", :options=>{:format=>:to_database}, :params=>{:guide=>#<GlobalID:0x00007ffcdd456988 @uri=#<URI::GID gid://caliber-revised/Guide/100>>}, :recipient=>#<GlobalID:0x00007ffcdd455fb0 @uri=#<URI::GID gid://caliber-revised/User/7>>, :record=>nil}
22:29:43 web.1     | [ActiveJob] [Noticed::DeliveryMethods::Database] [1a803188-b873-4cba-8208-ae49eded519a] Error performing Noticed::DeliveryMethods::Database (Job ID: 1a803188-b873-4cba-8208-ae49eded519a) from Sidekiq(default) in 13.0ms: NameError (undefined local variable or method `recipient' for #<NewGuide:0x00007ffcdeba5d50>):
22:29:43 web.1     | /Users/admin/Dropbox/Caliber/Revised/caliber_revised/app/notifications/application_notification.rb:10:in `to_database'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/delivery_methods/database.rb:17:in `attributes'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/delivery_methods/database.rb:6:in `deliver'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/delivery_methods/base.rb:18:in `block in perform'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/delivery_methods/base.rb:17:in `perform'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/execution.rb:40:in `block in perform_now'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:112:in `block in run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/i18n-1.8.3/lib/i18n.rb:308:in `with_locale'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/translation.rb:9:in `block (2 levels) in <module:Translation>'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `instance_exec'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/core_ext/time/zones.rb:66:in `use_zone'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/timezones.rb:9:in `block (2 levels) in <module:Timezones>'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `instance_exec'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/logging.rb:25:in `block (4 levels) in <module:Logging>'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `block in instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications/instrumenter.rb:24:in `instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/logging.rb:24:in `block (3 levels) in <module:Logging>'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/logging.rb:45:in `block in tag_logger'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:80:in `block in tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:28:in `tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:80:in `tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/logging.rb:45:in `tag_logger'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/logging.rb:21:in `block (2 levels) in <module:Logging>'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `instance_exec'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:121:in `block in run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:139:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/execution.rb:39:in `perform_now'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activejob-6.0.3.2/lib/active_job/execution.rb:18:in `perform_now'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:96:in `block in run_delivery_method'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:94:in `run_delivery_method'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:73:in `run_delivery'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:55:in `block (2 levels) in deliver_later'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:54:in `each'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:54:in `block in deliver_later'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/noticed-1.2.2/lib/noticed/base.rb:53:in `deliver_later'
22:29:43 web.1     | /Users/admin/Dropbox/Caliber/Revised/caliber_revised/app/controllers/guides_controller.rb:52:in `create'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/abstract_controller/base.rb:195:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/rendering.rb:30:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/abstract_controller/callbacks.rb:42:in `block in process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:135:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/abstract_controller/callbacks.rb:41:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/rescue.rb:22:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/instrumentation.rb:33:in `block in process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `block in instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications/instrumenter.rb:24:in `instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/notifications.rb:180:in `instrument'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/instrumentation.rb:32:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal/params_wrapper.rb:245:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/searchkick-4.4.1/lib/searchkick/logging.rb:212:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activerecord-6.0.3.2/lib/active_record/railties/controller_runtime.rb:27:in `process_action'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/abstract_controller/base.rb:136:in `process'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionview-6.0.3.2/lib/action_view/rendering.rb:39:in `process'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal.rb:190:in `dispatch'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_controller/metal.rb:254:in `dispatch'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:50:in `dispatch'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:33:in `serve'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:49:in `block in serve'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:32:in `each'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/journey/router.rb:32:in `serve'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/routing/route_set.rb:834:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/warden-1.2.8/lib/warden/manager.rb:36:in `block in call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/warden-1.2.8/lib/warden/manager.rb:34:in `catch'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/warden-1.2.8/lib/warden/manager.rb:34:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/tempfile_reaper.rb:15:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/etag.rb:27:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/conditional_get.rb:40:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/head.rb:12:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/http/content_security_policy.rb:18:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:266:in `context'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/session/abstract/id.rb:260:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/cookies.rb:648:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activerecord-6.0.3.2/lib/active_record/migration.rb:567:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/callbacks.rb:27:in `block in call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/callbacks.rb:101:in `run_callbacks'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/callbacks.rb:26:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/executor.rb:14:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/actionable_exceptions.rb:17:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/better_errors-2.7.1/lib/better_errors/middleware.rb:84:in `protected_app_call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/better_errors-2.7.1/lib/better_errors/middleware.rb:79:in `better_errors_call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/better_errors-2.7.1/lib/better_errors/middleware.rb:57:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/debug_exceptions.rb:32:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-livereload-0.3.17/lib/rack/livereload.rb:23:in `_call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-livereload-0.3.17/lib/rack/livereload.rb:14:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/web-console-4.0.4/lib/web_console/middleware.rb:132:in `call_app'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/web-console-4.0.4/lib/web_console/middleware.rb:28:in `block in call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/web-console-4.0.4/lib/web_console/middleware.rb:17:in `catch'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/web-console-4.0.4/lib/web_console/middleware.rb:17:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/show_exceptions.rb:33:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/turbolinks_render-0.9.20/lib/turbolinks_render/middleware.rb:81:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/railties-6.0.3.2/lib/rails/rack/logger.rb:37:in `call_app'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/railties-6.0.3.2/lib/rails/rack/logger.rb:26:in `block in call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:80:in `block in tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:28:in `tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/tagged_logging.rb:80:in `tagged'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/railties-6.0.3.2/lib/rails/rack/logger.rb:26:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/sprockets-rails-3.2.1/lib/sprockets/rails/quiet_assets.rb:13:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/remote_ip.rb:81:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/request_store-1.5.0/lib/request_store/middleware.rb:19:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/request_id.rb:27:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/method_override.rb:24:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/runtime.rb:22:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/activesupport-6.0.3.2/lib/active_support/cache/strategy/local_cache_middleware.rb:29:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/executor.rb:14:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/static.rb:126:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-2.2.3/lib/rack/sendfile.rb:110:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/actionpack-6.0.3.2/lib/action_dispatch/middleware/host_authorization.rb:76:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/honeybadger-4.7.0/lib/honeybadger/rack/error_notifier.rb:33:in `block in call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/honeybadger-4.7.0/lib/honeybadger/agent.rb:401:in `with_rack_env'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/honeybadger-4.7.0/lib/honeybadger/rack/error_notifier.rb:30:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/honeybadger-4.7.0/lib/honeybadger/rack/user_feedback.rb:31:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/honeybadger-4.7.0/lib/honeybadger/rack/user_informer.rb:21:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/webpacker-5.1.1/lib/webpacker/dev_server_proxy.rb:25:in `perform_request'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/rack-proxy-0.6.5/lib/rack/proxy.rb:57:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/railties-6.0.3.2/lib/rails/engine.rb:527:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/puma-4.3.5/lib/puma/configuration.rb:228:in `call'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/puma-4.3.5/lib/puma/server.rb:713:in `handle_request'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/puma-4.3.5/lib/puma/server.rb:472:in `process_client'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/puma-4.3.5/lib/puma/server.rb:328:in `block in run'
22:29:43 web.1     | /Users/admin/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/puma-4.3.5/lib/puma/thread_pool.rb:134:in `block in spawn_thread'
22:29:43 web.1     | Completed 500 Internal Server Error in 438ms (Searchkick: 11.4ms | ActiveRecord: 32.4ms | Allocations: 154962)
22:29:43 web.1     |
22:29:43 web.1     |
22:29:43 web.1     |
22:29:43 web.1     | NameError - undefined local variable or method `recipient' for #<NewGuide:0x00007ffcdeba5d50>:
22:29:43 web.1     |   app/notifications/application_notification.rb:10:in `to_database'
22:29:43 web.1     |   app/controllers/guides_controller.rb:52:in `create'
22:29:43 web.1     |

I feel like I'm missing something obvious here, but I just can't seem to figure it out. Even if I replace notification.deliver_later(@guide.user.favoritors(scope: :user_follow)) with something like notification.deliver_later(User.all) I still get the same error.

Thanks in advance!

Twilio issues - need to use form data instead of json?

I could not for the life of me get Twilio notifications to send. I finally cloned noticed and poked around a bit. I found that Twilio was returning {"code": 21602, "message": "Message body is required."}

Changing the HTTP post call to use form instead of json fixed it. I have a pull request ready, but I'm wondering if there is something else going on here? I'd have to imagine this was tested at some point... is there something else I may have been doing wrong?

NameError? Uninitialized constant ApplicationJob

Hi there,

I have run the setup for Noticed. When I try to deliver an notification, a NameError occurs.
To replicate the bug;


1. bundle add "noticed"
2. rails generate noticed:model
3. rails generated noticed: notification CommentNotification & uncomment deliver_to database
4. Send notification to a user.  notification = CommentNotification.with(comment: @comment) 
5. Delivery notification immediate: notification.deliver(@comment.post.author)
6. An NameError occurs.

I am running Rails 6.

Generate notification classes inside `models` folder

When following the setup instructions, when we ran rails generate noticed:notification CommentNotification, the generated class is put on the app/notifications/comment_notification.rb file.

That way, rails doesn't find the created class.

Maybe is better to create new classes in app/models/notifications/comment_notification.rb as the default location. If so, the generator should also add the correct namespace to the class name. Or we could add the app/notifications on the loaded paths array, but I think that is more intrusive.

Improve support for associating notifications

Copied from #87

@sjieg said:

Add a parameter to database to set the notifiable target:
deliver_by :database, notifiable: :post
Which will check params for a :post value and set it.

@excid3 didn't agree with the naming convention, was looking for some other names. @sjieg came back with some idea's. But also pointed out that doing this would add complexity and lock users in on using a specific column name for the polymorphic relation.

@rafaelpivato came back with the following:

This idea sounds great. I saw some examples on README explaining how to query notifications based on "post" or a notifiable. To me it sounds as important having that as having a recipient. To adhere to a simpler terminology akin to "recipient", the term "subject" could be used instead of "notifiable".

Anyway, I truly believe we need that other polymorphic as part of the ActiveRecord model.

Also, I don't think that should be an option to database but a broader approach to the whole notification class, the same way recipient is. Maybe one of the following calls would do the job.

PostCreatedNotification.about(post).deliver(user)
PostCreatedNotification.with(user_agent: request.headers['User-Agent']).about(post).deliver(user)
PostCreatedNotification.deliver(user, subject: post)

# Not advisable as the subject would not be an actual param
PostCreatedNotification.with(subject: post).deliver(user)

Querying notifications for associations

First, thanks for the great gem!

This is not a bug report, but a question that I was hoping you could help with.

I have a Document model that has_many comments, each of which can have an associated notification for a particular user as the recipient.

I want to be able to mark all of a document's comment notifications as read when a user opens a document but I'm finding it difficult to query the notification's JSONB params based on the association data.

An alternative method would be to attach comment notifications to the parent Document, rather than creating notifications per comments, but I can't help but feel that there's a simple method to query an associations' notifications that I just can't wrap my head around.

Thanks again.

capture audit log for each notification

Would be nice to capture:

  1. which notification(s) were sent and when.
  2. which notification was not sent (which condition triggered the decision not to send).

Unable to get Twilio working

Me again. I have no idea what I'm doing wrong, but I'm unable to get any Twilio notifications to send. Here's my code:

class EngineerTimesheetReminderNotification < BaseNotification
  deliver_by :database
  deliver_by :email,
             mailer: "EngineerMailer",
             method: "timesheet_reminder",
             if: :email_notifications?
  deliver_by :twilio,
             credentials: :get_twilio_credentials,
             if: :sms_notifications?

  param :engineer
end
class BaseNotification < Noticed::Base
  def email_notifications?
    recipient.notification == 'email'
  end

  def sms_notifications?
    recipient.notification == 'sms'
  end

  def get_twilio_credentials
    {
      account_sid: ENV.fetch('TWILIO_ACCOUNT_SID'),
      auth_token: ENV.fetch('TWILIO_AUTH_TOKEN'),
      phone_number: ENV.fetch('TWILIO_PHONE_NUMBER')
    }
  end
end
en:
  notifications:
    engineer_timesheet_reminder_notification:
      message: "Please submit your timesheet before 8am Monday morning so we can process your payment."

I've also implemented a .phone_number method on the Account object, and have set notification to 'sms'

I'm currently testing on the rails console using:

foxy = Account.find_by_email '[email protected]'
EngineerTimesheetReminderNotification.with(engineer: foxy).deliver(foxy)

and get the following output:

Performing Noticed::DeliveryMethods::Database (Job ID: 2016e766-60e3-4d84-ad1c-e9b229164c20) from DelayedJob(default) enqueued at  with arguments: {:notification_class=>"EngineerTimesheetReminderNotification", :options=>{}, :params=>{:engineer=>#<GlobalID:0x000055993e488410 @uri=#<URI::GID gid://system10/Account/6>>}, :recipient=>#<GlobalID:0x000055993e49bd58 @uri=#<URI::GID gid://system10/Account/6>>, :record=>nil}
Performed Noticed::DeliveryMethods::Database (Job ID: 2016e766-60e3-4d84-ad1c-e9b229164c20) from DelayedJob(default) in 30.66ms
Performing Noticed::DeliveryMethods::Twilio (Job ID: d6d97985-c504-42be-b235-e27a560c3301) from DelayedJob(default) enqueued at  with arguments: {:notification_class=>"EngineerTimesheetReminderNotification", :options=>{:credentials=>:get_twilio_credentials, :if=>:sms_notifications?}, :params=>{:engineer=>#<GlobalID:0x000055993e65a248 @uri=#<URI::GID gid://system10/Account/6>>}, :recipient=>#<GlobalID:0x000055993e659be0 @uri=#<URI::GID gid://system10/Account/6>>, :record=>#<GlobalID:0x000055993e659528 @uri=#<URI::GID gid://system10/Notification/20>>}
Performed Noticed::DeliveryMethods::Twilio (Job ID: d6d97985-c504-42be-b235-e27a560c3301) from DelayedJob(default) in 598.72ms
=> [#<Account:0x000055993e1b5f58
  id: 6,
  <snip>
]

I might have a fighting chance of seeing what I'm doing wrong if the output from the API call was logged somewhere, but as it is right now, it seems to be fire and forget.

Documentation improvement: user params.except(:post) instead of params.delete(:post)

In the documentation using a notifiable relation the following is suggested to use:

def format_for_database
    {
      notifiable: params.delete(:post),
      type: self.class.name,
      params: params
    }
  end

In my case, when sending a notification to more than 1 user, with the second users params[:post] did not exist anymore, because that parameter was deleted on the previous iteration.

This caused quite a strange bug, as I was creating the Notification from a delayed job, and the error caused the job to repeat. So this caused the first user to receive multiple messages, while the second user didn't receive anything.

Anyway, I'd like to recommend the following documentation to prevent these kind of problems:

  def format_for_database
    {
      notifiable: params[:post],
      type: self.class.name,
      params: params.except(:post)
    }
  end

If you like the following idea, I can make a new ticket for it:

Add a parameter to database to set the notifiable target:

deliver_by :database, notifiable: :post

Which will check params for a :post value and set it.

WDYT?

Notification generator does not fully support namespaces

I was tinkering around with the gem this morning and am really liking it so far. One thing I did notice is when you pass in a namespaced notification name the namespace is added to the class itself, but the corresponding folder under the notifications directory is not created.

Example

rails generate noticed:notification Inspection::CompletedNotification

Class is generated as:

class Inspection::CompletedNotification < Noticed::Base
  ...
end

Path is: app/notifications/completed_notification.rb

If this is something you'd like to support I'd be happy to take a crack at pull request for it.

Custom delivery methods

Currently, we have the ability to specify delivery methods by name, but it would be nice to be able to specify them by providing the class.

For example:

  deliver_by :database, format: :attributes_for_database
  deliver_by :websocket
  deliver_by :email, mailer: UserMailer
  
  deliver_by DiscordNotification

We could then define a DiscordNotification class and it would be instantiated automatically for the delivery.

This probably is close to working. The deliver_by method on Noticed::Base and the perform method are the two main things we'll need to explore.

Naming of the notification param in the 'email' delivery method

I hope this doesn't come off as nit-picky, but I wanted to bring up/suggest a tweak to the naming of one of the mailer params in the email delivery method.

Since in my case I'm utilizing a polymorphic relationship to link up to the subject of my notifications, I was trying to find the best way to refer back to the notification record itself while building out my mailer action. Turns out there's a way to do this, and it's by referencing params[:record].

In my head it seems a bit confusing to build out my mailer with a param called :record as opposed to :notification.

As it currently stands this is what I need to do:

class CommentMailer < ApplicationMailer
  before_action do
    @recipient, @notification = params[:recipient], params[:record]
    @comment = @notification.notifiable
  ...
end

My proposal would be to change the params name to :notification so that in my mailers I could do this:

class CommentMailer < ApplicationMailer
  before_action do
    @recipient, @notification = params[:recipient], params[:notification]
    @comment = @notification.notifiable
  ...
end

As I mentioned above I know this is a bit pedantic, but sometimes I feel that's half of what being a programmer is all about :)

I'm happy to submit a PR for this if you agree. From the looks of it I'd just need to make a change on this line:

Handle deserialization error for deliver_by email when record destroyed

Probably would be nice to have an explanation in the readme like this:

class NotificationMailer < ApplicationMailer
  rescue_from(ActiveJob::DeserializationError) do |e|
    Rails.logger.info("Skipping notification due to #{e.message}")
  end

  def some_notification
    # ... params[:some_record] that can be destroyed, will raise ActiveJob::DeserializationError, so we catch it.
  end

Bubble up more email errors

Hi Chris,

I just noticed, that I hadn't set my email up correctly, but even with debug: true I wasn't getting any indication I'd done anything wrong.

This is my development.log (Sanitised and shortened for brevity)

[ActiveJob] [Noticed::DeliveryMethods::Email] [<some id>]
Performing Noticed::DeliveryMethods::Email 
(Job ID: <job id>) from Sidekiq(default) enqueued at 2020-08-13T21:50:26Z with arguments: 
{:notification_class=>"ExampleNotification", :options=>{:mailer=>"UserMailer", 
  :method=>"example_notification", :debug=>true},
  :params=>{:recipient=>#<GlobalID:<etc, etc>}
[ActiveJob] [Noticed::DeliveryMethods::Email] [<some id>]
UserMailer#example_notification: processed outbound mail in 3.9ms

I noticed however the job was sitting in Sidekiq. Having watched your debugging screen cast the other day, I put in a byebug above line 5 https://github.com/excid3/noticed/blob/master/lib/noticed/delivery_methods/email.rb#L5

And result!

(byebug) mailer.with(format).send(method.to_sym).deliver
*** ActionView::MissingTemplate
Exception: Missing template user_mailer/example_notification, application_mailer/example_notification
with {:locale=>[:en], :formats=><etc, etc>}. Searched in:
  * "app/views"
  * <other gem views>
  * "gems/actiontext-6.0.3.2/app/views"
  * "actionmailbox-6.0.3.2/app/views"

Now maybe I could have changed a setting on my development environment that would have shown me this I'm not sure.
Anyway, just posting it here for future issue spotters.

It's a bit late in the night here for me to try and create a PR, but would be happy to help if showing this error in Noticed would be useful.

Big thank you for this library 💕

Update: I realised we're using MJML for our email templates, and I had the wrong ending for my file, html.erb and not .mjml so once I fixed that, I was getting more debug messages in the logs without byebug.

However one more issue, and this again be related to mjml is that I am unable to send emails with deliver_later, but deliver_now works fine. 🤷🏼‍♀️ Anyway, it's definitely too late at night now. Happy to help out where I can anyway.

Idea: debouncing notifications

Hey! first of all - thanks for this awesome lib. Really liked that it does things "the rails way" and lets you focus on the notifications themselves.

I wonder if that's something that can be handled in the library itself — but one might want to debounce notifications or at least send some in batches. Imagine a busy thread in FB or Twitter — you don't want to get a message for every message there.

A nice approach would be to have a notification that is stored to DB and a cron job that runs every 5m/10m/whatever to get all the necessary notifications and group them into one big email instead of many small ones.

I wonder if that's something you have considered to bake into the library itself. It's not hard to implement that in userland (or at least I think so, haven't tried yet) but I wonder how it would look if it was baked in

I guess it's just a discussion, because there is no real suggestion here.

Thanks again for a wonderfully crafted library.

Generator for ApplicationNotification base class

Create a generator noticed:application_notification to create a base class for notifications of a Rails application.

class ApplicationNotification < Noticed::Base
  def database_format
    {
      'type': self.class.name,
      'params': params
    }
  end

  def action_format
    params
  end
end

It would be good if noticed:notification could be modified to inherit from this ApplicationNotification whenever it exists. On the other hand, a warning could also be emitted if you have notifications already created in order to update their super class.

Notification model attribute type is generating testing errors for other classes

Upon running a test for a different class rails test test/controllers/roleusers_controller_test.rb

The error:

RoleusersControllerTest#test_should_get_edit:
DRb::DRbRemoteError: PG::NotNullViolation: ERROR:  null value in column "type" violates not-null constraint
DETAIL:  Failing row contains (980190962, Recipient, 980190962, null, null, 2020-11-27 21:19:12, 2020-11-28 10:27:11.443691, 2020-11-28 10:27:11.443691)

Checking the application schema, there is only one instance of a column with the name type and it is with the column of the notifications table which thus had passed to postgresql upon its migration generation: t.string "type", null: false

I've traced the mistake to the fixture:

one:
  recipient: one
  recipient_type: Recipient
  type: 
  params: 
  read_at: 2020-11-27 21:19:12

By populating type and params with dummy data MyString and 1, the test passes.
The gem read_me does not indicate what is expected for the type attributes, which might be helpful is the model were extended with other attributes, then subjected to testing.

Usecases - what exactly should we use the gem for / scope of notifications

Hey,

I'm a pretty newbie to RoR and coding in general.

First what is a database notification? i get what is an email, sms notification but not a database notification ? it's when you writes it to the database so next time a certain page is viewed by a user and you'll fetch this data to show him ?

I understand this is the core usecase when there is an external event that User1 is not aware as he did not initiate it himself.

  • if in a forum, user 2 comments the post of User 1, then we could use your Notification gem to notify User 1 with User's 2 message (in this case User1 is not at the source of the change triggering the notification)

  • but what about if User1 performs an action such as post "5 messages in 10 days" and we consider this is a milestone, and let's say i want to send an email saying "congrats for doing 5 messages in 10 days", should we use your "notification" gem for this ? as there is no "external" force (User2), are we in the concept of Notifications ? maybe I am misunderstanding the scope of Notifications: is it a concept that allow to simply structure things more cleanly than just a callback in a PostMessagescontroller?

  • what about devise emails ? I guess emails sent in relation with authentication (ex: reset password, confirm email on sign up) are not to be used with noticed but rather already covered by devise gem

  • notifications about new feature for everybody or a user segment... ? in your great jumpstart Pro this is also covered by another part called Annoucements so I guess it's not the right usecase for Notifications.

  • I see you adjusted the gem for jumpstart rails pro for multitenant cases: would you say it's the right use case if member 4 of team X edits the common Team X profile and so you add a email notification to certain members (ex those with admin roles) of team X with sth like "hey member 4 just edited the Team Profile) ? Again I'd just have used some callbacks on the TeamProfile Controller but maybe using notifications is cleaner ?

I know these questions must sound super naive but I find it hard to understand the scope/limits of what you call "Notifications".

Thanks for any help,
Cheers

ActiveJob support out of the box

Notifications are usually sent via background jobs if we can add opt-in support which makes noticed gem to take care of queuing as well then it will be an end to end solution I believe.

Something like notify_async would be nice

E.g:

notification = CommentNotification.new(comment: @comment.to_gid)
notification.notify_async(@comment.post.author)

Record HTTP requests for testing

Whilst working on #59 I noticed that the Vonage, Slack, Twilio and MicrosoftTeams tests trigger a real HTTP request to different services, which always fail and raise a Noticed::ResponseUnsuccessful exception, making all of those tests a little bit more complicated to read and write.

It may be great if we could locally record successful requests to each service, and use them in the tests, so we could replicate real case scenarios. I've used WebMock in the past for this specific case and it works great.

wrong number of arguments (given 0, expected 1) on mailers

Can you show an example mailer that works with this, please? I'm unable to get it working correctly as I'm getting the error

wrong number of arguments (given 0, expected 1)
/app/app/mailers/engineer_mailer.rb:48:in `timesheet_reminder'

My mailer looks like this:

class EngineerMailer < ApplicationMailer
  def timesheet_reminder(recipient)
    timesheet = Timesheet.new(
      date: Date.current.beginning_of_week,
      account: recipient
    )

    @timesheet_title = timesheet.to_label
    @timesheet_url = engineers_timesheet_url(timesheet.slug)
    mail to: timesheet.account.email
  end
end

And I'm guessing the missing arguments relates to the recipient I want to pass in.

My notification looks like this:

class EngineerTimesheetReminderNotification < BaseNotification
  deliver_by :database
  deliver_by :email,
             mailer: "EngineerMailer",
             method: "timesheet_reminder"

  deliver_by :twilio,
             credentials: :get_twilio_credentials

  param :engineer

  def url
    timesheet = Timesheet.new(
      date: Date.current.beginning_of_week,
      account: engineer
    )

    engineers_timesheet_url(timesheet.slug)
  end
end

What am I missing? Thanks.

http dependency breaks on Apple M1

I kept getting the following error when trying to run my Rails app that uses Jumpstart Pro:

Could not open library '/Users/---/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/http-parser-1.2.2/ext/aarch64-darwin/libhttp-parser-ext.bundle': dlopen(/Users/---/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/http-parser-1.2.2/ext/aarch64-darwin/libhttp-parser-ext.bundle, 5): no suitable image found.  Did find: (LoadError)
	/Users/---/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/http-parser-1.2.2/ext/aarch64-darwin/libhttp-parser-ext.bundle: mach-o, but wrong architecture
	/Users/---/.rbenv/versions/3.0.0/lib/ruby/gems/3.0.0/gems/http-parser-1.2.2/ext/aarch64-darwin/libhttp-parser-ext.bundle: stat() failed with errno=25

I was able to narrow do the problematic gem to noticed -- turns out the http dependency is not yet M1 compatible.

Get response from delivery method in callback

I wonder if it's currently possible, or whether it would be a useful feature to be able to access the delivery response in an after callback.

class InvitationNotification < ApplicationNotification
  deliver_by :vonage

  after_vonage :record_vonage

  # ...

  private 

  def record_vonage
    recipient.deliveries.create(status: vonage_response)
  end
end

This is a contrived example (using vonage_response, which would be the status code from this line. This would provide the flexibility to actually record whether a delivery method was successful or not.

My specific use-case is being able to determine whether text messages where delivered or not as they're displayed back to a user, though I'm sure it would be equally beneficial for most delivery methods if you needed logging/tracking of the response.

Rails >= 6?

The noticed.gemspec says "rails", ">= 6.0.0"

But then, in the readme, you mention "For Rails 6.0 and earlier ... " Does this really just mean "for Rails 6.0"?

I'm just holding out the slim hope that I'll be able to add noticed to my old, gray-bearded Rails 4 project ... unlikely, I know.

Generators not appearing in "rails -T"

rails -T does not include the generators included in this gem. I'm using the gem as installed/configured in JSP.

Within JSP:

$ rails -T | grep not
rails annotate_models                              # Add schema information (as comments) to model and fixture files
rails annotate_routes                              # Adds the route map to routes.rb
rails db:prepare                                   # Runs setup if database does not exist, or runs migrations if it does
rails remove_annotation                            # Remove schema information from model and fixture files
~~~

Mark individual notifications as unread

Hello!

Love the gem! 🙌 🙂. Just had one instance method I found myself adding to the Notification model that may be a candidate for inclusion - to mark an individual notification as unread again if it can be saved for later.

def mark_as_unread!
  update(read_at: nil)
end

Is this something that would suit being added to model.rb, or perhaps this was something that was not done on purpose for a reason I am unaware of 😅. If it is something that could be added I am happy to submit the PR, just wanted to check for suitability/interest first! Thanks again for the awesome gem 🙌

Params not available in custom delivery method

Hi, I'm implementing a custom delivery method for push notification.
Defined like so

class DeliveryMethods::PushNotification < Noticed::DeliveryMethods::Base
  def deliver
    PushAdapter.send_message(
      registration_ids: recipient.devices.pluck(:id),
      title: notification.message,
      body: params[:body],
      data: params[:data]
    )
  end

  # You may override this method to validate options for the delivery method
  # Invalid options should raise a ValidationError
  #
  # def self.validate!(options)
  #   raise ValidationError, "required_option missing" unless options[:required_option]
  # end
end

In the custom delivery method of the read me it states that params should be available in the delivery method.
However, when running a notification that using this method, this error is returned

NameError: undefined local variable or method `params' for #<DeliveryMethods::PushNotification:0x00007f9cab2bb368>

And inspecting the the object returns this value

#<DeliveryMethods::PushNotification:0x00007f9cab2bb368
 @arguments=
  [{:notification_class=>"OfferNotification",
    :options=>{:class=>"DeliveryMethods::PushNotification"},
    :params=> {...},
    :recipient=>#<User id: 1 >,
    :record=>nil}],
 @enqueued_at="2020-09-02T00:58:08Z",
 @exception_executions={},
 @executions=1,
 @job_id="10ea2d3b-166b-4874-a599-f68c4af92ff8",
 @locale="en",
 @notification=
  #<OfferNotification:0x00007f9cab25ad60
   @params={...},
   @recipient=#<User id: 1 >,
   @record=nil>,
 @options={:class=>"DeliveryMethods::PushNotification"l},
 @priority=nil,
 @provider_job_id=nil,
 @queue_name="",
 @recipient=
  #<User id: 1 >,
 @record=nil,
 @serialized_arguments=nil,
 @timezone="UTC">

Here params are only available via the notification or through inspecting the arguments.

Is this desired behaviour and the read me should be updated, or should params actually be available here?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.