GithubHelp home page GithubHelp logo

laravel-notification-rate-limit's Introduction

Laravel Notification Rate Limit

Latest Version on Packagist Total Downloads Quality Score StyleCI

Licence Buy us a tree Treeware (Trees)

Rate Limiting Notifications in Laravel using Laravel's native rate limiter to avoid flooding users with duplicate notifications.

Version Compatability

Laravel PHP Laravel-Notification-Rate-Limit Date
7.x/8.x 7.1/8.0 1.1.0 2021-05-20
9.x 8.0 2.1.0 2023-08-26
10.x 8.0/8.1 2.1.0 2023-08-26
10.x 8.2/8.3 2.2.0 2024-03-18
10.x 8.2/8.3 3.0.0 2024-05-25
11.x 8.2/8.3 2.2.0 2024-03-18
11.x 8.2/8.3 3.0.0 2024-05-25

Installation

You can install the package via composer:

composer require jamesmills/laravel-notification-rate-limit

Update your Notifications

Implement the ShouldRateLimit interface and add the RateLimitedNotification trait to the Notifications you would like to rate limit.

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Jamesmills\LaravelNotificationRateLimit\RateLimitedNotification;
use Jamesmills\LaravelNotificationRateLimit\ShouldRateLimit;

class NotifyUserOfOrderUpdateNotification extends Notification implements ShouldRateLimit
{
    use Queueable;
    use RateLimitedNotification;

...

Publish Config

Everything in this package has opinionated global defaults. However, you can override everything in the config, and many options may also be further customized on a per-notification basis (see below).

Publish it using the command below.

php artisan vendor:publish --provider="Jamesmills\LaravelNotificationRateLimit\LaravelNotificationRateLimitServiceProvider"

Upgrading from 2.x

If you are upgrading from version 2, be aware that the NotificationRateLimitReached event signature has changed, and now includes more information about the notification being skipped. If you have implemented your own version of this event class, you will need to update the constructor signature to accept these additional parameters. No other changes should be required as a part of the upgrade process.

Important considerations

Queued and delayed notifications

Rate limiting is checked only when notifications are actually being delivered.

If a notification is sent to a queue, or a notification is dispatched with a delay (e.g. $user->notify($notification->delay(...))), then any rate limiting will be considered only when the notification is actually about to be dispatched to the user.

Identifier conflicts when using multiple types of Notifiable models

If you have multiple models that use the Notifiable trait (e.g. multiple types of User models), you should add the class name of the Notifiable instance to the cache key (see Customizing the Notifiasble identifier below).

Options

Events

By default, the NotificationRateLimitReached event will be fired when a Notification is skipped. You can customise this using the event option in the config.

Overriding the time the notification is rate limited for

By default, a rate-limited Notification will be rate-limited for 60 seconds.

Update globally with the rate_limit_seconds config setting.

Update for an individual basis by adding the below to the Notification:

// Change rate limit to 1 hour
protected $rateLimitForSeconds = 3600;

Logging skipped notifications

By default, this package will log all skipped notifications.

Update globally with the log_skipped_notifications config setting.

Update for an individual basis by adding the below to the Notification:

// Do not log skipped notifications
protected $logSkippedNotifications = false;

Skipping unique notifications

By default, the Rate Limiter uses a cache key made up of some opinionated defaults. One of these default keys is serialize($notification). You may wish to turn this off.

Update globally with the should_rate_limit_unique_notifications config setting.

Update for an individual basis by adding the below to the Notification:

protected $shouldRateLimitUniqueNotifications = false;

Customising the cache key

You may want to customise the parts used in the cache key. You can do this by adding code such as the below to your Notification:

public function rateLimitCustomCacheKeyParts()
{
    return [
        $this->account_id
    ];
}

Customizing the Notifiable identifier

By default, we use the primary key or $id field on the Notifiable instance to identify the recipient of a notification.

If for some reason you do not want to use $id, you can add a rateLimitNotifiableKey() method to your Notifiable model and return a string containing the key to use.

For example, if multiple users could belong to a group and you only want one person (any person) in the group to receive the notification, you might return the group ID instead of the user ID:

class User extends Authenticatable
{
    use Notifiable;

    protected $fillable = ['id', 'name', 'email', 'groupId'];
    
    public function rateLimitNotifiableKey(): string
    {
        return $this->group_id;
    }
}

Similarly, if you have multiple models in your application that are Notifiable, using only the id could result in collisions (where, for example, Agent #41 receives a notification that then precludes Customer #41 from receiving a similar notification). In this case, you may want to return an identifier that also includes the class name in the key for each model:

class Customer extends Authenticatable
{
    use Notifiable;

    protected $fillable = ['id', 'name', 'email'];
    
    public function rateLimitNotifiableKey(): string
    {
        return get_class($this) . '#' . $this->id;
    }
}

class Agent extends Authenticatable
{
    use Notifiable;

    protected $fillable = ['id', 'name', 'email'];
    
    public function rateLimitNotifiableKey(): string
    {
        return get_class($this) . '#' . $this->id;
    }
}

Advanced Usage

Discarding a notification for application-defined reasons

There may be circumstances where you wish to implement custom application logic for determining that a notification should be discarded even if the rate limiter itself would not prevent it from being sent (e.g. keeping track of, and setting an upper limit of, the number of times a given user can receive a specific notification in total).

To do so, add a rateLimitCheckDiscard function to your notification, and return a non-NULL string to indicate the reason that a notification is being discarded. Example:

public function rateLimitCheckDiscard(string $key): ?string
{
    $max_send_key = $key . '_send_count';
    
    $count = Cache::get($max_send_key, 0);
    if ($count >= 3) {
        return 'Max send limit reached';
    }
    
    Cache::put($max_send_key, $count + 1);
    return null;
}

Notes:

  • The string 'Rate limit reached' (defined at NotificationRateLimitReached::REASON_LIMITER) is reserved to indicate that the rate limiter is preventing the notification from being dispatched.
  • If the rate limiter itself is preventing a notification from being dispatched, then the custom rateLimitCheckDiscard will not be called at all.
  • If rateLimitCheckDiscard returns a non-NULL string, then:
    • the notification will not be dispatched and it will be discarded; and
    • the attempt will not be counted as a 'hit' against the rate limiter itself.
  • The 'reason' returned from rateLimitCheckDiscad will be included in the log entry (if configured) and forwarded along to the NotificationRateLimitReached event as well.

Deferring (rather than discarding) a notification

If you wish to defer/delay the delivery of a notification rather than completely discarding it (see issue #33), an example of one way that this could be implemented is available at https://github.com/tibbsa/lnrl_deferral_example/.

Testing

composer test

Changelog

Please see CHANGELOG for more information what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security related issues, please email [email protected] and [email protected] instead of using the issue tracker.

Credits

License (Treeware)

This package is 100% free and open-source, under the MIT License (MIT). Use it however you want.

This package is Treeware. If you use it in production, then we ask that you buy the world a tree to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

Inspiration

Inspiration for this package was taken from the article Rate Limiting Notifications in Laravel by Scott Wakefield (now available only via the Internet Archive's Wayback Machine).

laravel-notification-rate-limit's People

Contributors

edgrosvenor avatar jamesmills avatar mhkb avatar myckhel avatar samtlewis avatar stylecibot avatar taghreedaa avatar tibbsa 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

Watchers

 avatar  avatar  avatar  avatar

laravel-notification-rate-limit's Issues

Conflict from cache keys relying only on Notifiable::$id

Cache keys are built using the Notifiable 'id' value, but this is potentially ambiguous in an application that has multiple models having the Notifiable trait. Two different types of Notifiable models may share the same $id value.

While an individual developer could add this to the cache key through the use of custom cache name parts, the potential for this clash may not be recognized.

We should anticipate that problem and provide a simple config option to add this to cache keys.

Additional contributors?

@jamesmills - There was some discussion in a pull request thread about the need for some maintainer assistance on this project. (Tagging @3m1n3nc3 @Riley19280 @samtlewis on this.)

I see there are various forks but nobody seems to have actually "taken over". I happen to need this across multiple projects and would be willing to step up and take on a coordinating role here if that will help move some of the issues forward. Whether you want to "transfer" it or simply add me as an additional maintainer is up to you, but I am indifferent. You can stay in lurk mode.

MVP

Background

I have an application which accepts a Conversion via a webhook URL. It's basically an API endpoint that accepts a ping when something happens.

When a Conversion is sent to the API via a webhook there is some code which checks to see if some of the values sent are valid. You can send a ConversionClickId and if this is invalid that it will fire an event. There are also notifications which are fired to notify the user who sent the webhook that it contains an invalid ClickId.

If the implementation was set up wrongly then the webhook could fire a load of times with an invalid ClickId which would cause loads of notifications to be sent to the user. I would like to throttle these or "rate limit" them somehow.

Background research

I have not found any packages out there and I am sure that this is something which a number of people need to do in their applications. I found this article with some good ideas https://scottwakefield.co.uk/journal/rate-limiting-notifications-in-laravel/

Initial Q&A

Q: The hard part is to decide what makes a notification unique. Do all data have to be equal? Or only specific keys?

A: It's purely the fact that the Notification has been fired. It does not matter about any of the data being passed. I suggest we just cache the UserId and the Notification class name as a key to check against.

Q: And will it check during queuing or when it's really send?

A: Probably somehow hooked into the Notification request lifecycle when it's about to be sent? I'm not sure. Not sure it matters.

Q: Another question that comes to mind is how it handles multi channel notifications? Will the SMS be suppressed if email was first?

A: In the initial release (could lad other options on later) It would stop the entire Notification from processing any of the channels.

Suggested solution and opinionated assumptions

  • I like the idea of writing a Trait that can be added to a Notification class.
  • I like the idea of using the cache and using a hash of the User and Notification class name.
  • I like the idea of using the Illuminate\Cache\RateLimiter
  • I like the idea of being able to override on the Notification class the throttleKey and cachTimeout if the user of the package wants to. A reasonable default would be set, maybe in the congif.
  • It would stop all channel notifications being sent for that notification event

Add Max Attempt Limit to Notification Rate Limiting

Overview

I propose introducing a feature that allows developers to specify a maximum number of attempts for which a notification can be retried before permanently ceasing any further attempts. This feature aims to prevent endless notification retries and enables better control over the notification flow, especially in automated systems such as CRON jobs.

Problem

Currently, while the Laravel Notification Rate Limit package effectively prevents flooding users with notifications by enforcing a delay between attempts, it does not limit the total number of retries for a notification. In scenarios where notifications are tied to regular checks (e.g., a CRON job checking for low user credits), a notification can be repeatedly sent after the specified delay without a cap on the total attempts. This can potentially lead to situations where notifications become redundant or annoying.

Example Use Case

Consider a notification system that alerts users about their low credits. A CRON job periodically checks if a user's credits fall below a certain threshold and triggers a notification. The rate limiter is set to delay subsequent notifications for 2 days. However, without a cap on attempts, the user might receive the same notification indefinitely every 2 days if their credit status doesn't change. Ideally, the notification should only be retried a limited number of times, say 3 times, before stopping.

Proposed Solution

Integrate a setting within the RateLimitedNotification trait that allows specifying a maximum number of delivery attempts. Once a notification reaches this limit, it would not be eligible for further retries. This could be implemented using a counter that increments with each attempt, stored either in the cache or directly managed through Laravel’s existing job mechanism for queued notifications.

Benefits

  • Reduces Redundancy: Ensures notifications do not become repetitive and annoying to users.
  • Enhances Control: Gives developers explicit control over the notification lifecycle, allowing them to manage user experience more effectively.
  • Promotes Efficiency: Prevents system resources from being wasted on unnecessary notification attempts.

Undefined property: Illuminate\Notifications\AnonymousNotifiable::$id

Versions

Laravel: 8.78.1
jamesmills/laravel-notification-rate-limit: 1.1.0

Steps to reproduce

  • install the package and publish the configuration file (default values)
  • update MyNotification (fancy name):
class MyNotification extends Notification implements ShouldQueue, ShouldRateLimit
{
    use Queueable;
    use RateLimitedNotification;
...

In a service class, I'm sending an on-demand notification MyNotification:

Notification::route('discord', 'my-channel-id')->notify(app(MyNotification::class));

I got this error:

   ErrorException

  Undefined property: Illuminate\Notifications\AnonymousNotifiable::$id

  at vendor/jamesmills/laravel-notification-rate-limit/src/RateLimitedNotification.php:29
     25$parts = array_merge(
     26▕             [
     27▕                 config('laravel-notification-rate-limit.key_prefix'),
     28▕                 class_basename($notification),
  ➜  29$notifiables->id,
     30▕             ],
     31$this->rateLimitCustomCacheKeyParts(),
     32$this->rateLimitUniqueueNotifications($notification)
     33▕         );

I tried to remove any dependency in the notification constructor, but nothing changed.

Without using ShouldRateLimit and RateLimitedNotification, notifications are sent (but Discord is sending a 429 response, which is the reason why I'm using this package).

Thank you

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.