diff --git a/API.md b/API.md new file mode 100644 index 0000000..76c8a1e --- /dev/null +++ b/API.md @@ -0,0 +1,531 @@ +# ActionWebPush API Documentation + +## Table of Contents + +- [Configuration](#configuration) +- [Notification](#notification) +- [Batch Delivery](#batch-delivery) +- [Base Classes](#base-classes) +- [Rate Limiting](#rate-limiting) +- [Error Handling](#error-handling) +- [Instrumentation](#instrumentation) +- [ActiveRecord Models](#activerecord-models) +- [Background Jobs](#background-jobs) + +## Configuration + +### ActionWebPush.configure + +Configure the gem with VAPID keys and delivery settings. + +```ruby +ActionWebPush.configure do |config| + config.vapid_subject = "mailto:admin@example.com" + config.vapid_public_key = "your_vapid_public_key" + config.vapid_private_key = "your_vapid_private_key" + config.delivery_method = :web_push + config.pool_size = 10 + config.queue_size = 100 + config.connection_pool_size = 5 + config.batch_size = 100 +end +``` + +#### Configuration Options + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `vapid_subject` | String | nil | VAPID subject (mailto: or https:) | +| `vapid_public_key` | String | nil | VAPID public key | +| `vapid_private_key` | String | nil | VAPID private key | +| `delivery_method` | Symbol | :web_push | Delivery method (:web_push, :test) | +| `pool_size` | Integer | 10 | Thread pool size for async delivery | +| `queue_size` | Integer | 100 | Queue size for thread pool | +| `connection_pool_size` | Integer | 5 | HTTP connection pool size | +| `batch_size` | Integer | 100 | Default batch size for bulk operations | + +### ActionWebPush.config + +Access current configuration: + +```ruby +config = ActionWebPush.config +puts config.vapid_subject +``` + +## Notification + +### Creating Notifications + +```ruby +notification = ActionWebPush::Notification.new( + title: "Hello World", + body: "This is a push notification", + endpoint: "https://fcm.googleapis.com/fcm/send/...", + p256dh_key: "BNbN3OiAT...", + auth_key: "k2i6t8hBmF...", + data: { url: "/messages/123" }, + icon: "/icon.png", + badge: "/badge.png", + urgency: "high" +) +``` + +#### Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `title` | String | Yes | Notification title | +| `body` | String | Yes | Notification body | +| `endpoint` | String | Yes | Push service endpoint | +| `p256dh_key` | String | Yes | P256DH public key from subscription | +| `auth_key` | String | Yes | Auth secret from subscription | +| `data` | Hash | No | Custom data payload | +| `icon` | String | No | Notification icon URL | +| `badge` | String | No | Badge icon URL | +| `urgency` | String | No | Urgency level (very-low, low, normal, high) | + +### Delivery Methods + +#### Synchronous Delivery + +```ruby +# Deliver immediately +notification.deliver_now + +# Alias for deliver_now +notification.deliver +``` + +#### Asynchronous Delivery + +```ruby +# Deliver via background job +notification.deliver_later + +# With options +notification.deliver_later( + wait: 5.minutes, + wait_until: 1.hour.from_now, + queue: :push_notifications, + priority: 10 +) +``` + +### Notification Methods + +#### Instance Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `deliver(connection: nil)` | Boolean | Deliver notification synchronously | +| `deliver_now(connection: nil)` | Boolean | Alias for deliver | +| `deliver_later(options = {})` | Job | Deliver via background job | +| `to_params` | Hash | Convert to parameter hash | +| `to_json(*args)` | String | Convert to JSON | + +#### Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `title` | String | Notification title | +| `body` | String | Notification body | +| `data` | Hash | Custom data | +| `endpoint` | String | Push endpoint | +| `p256dh_key` | String | P256DH key | +| `auth_key` | String | Auth key | +| `options` | Hash | Additional options | + +## Batch Delivery + +### ActionWebPush::BatchDelivery + +Efficiently deliver multiple notifications. + +```ruby +notifications = [notification1, notification2, notification3] + +# Basic batch delivery +ActionWebPush::BatchDelivery.deliver(notifications) + +# With custom pool and batch size +batch = ActionWebPush::BatchDelivery.new( + notifications, + pool: custom_pool, + batch_size: 50 +) +batch.deliver_all +``` + +#### Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `new(notifications, pool: nil, batch_size: nil)` | Array, Pool, Integer | BatchDelivery | Create new batch | +| `deliver_all` | None | Void | Deliver all notifications | +| `self.deliver(notifications, **options)` | Array, Hash | Void | Class method for quick delivery | + +## Base Classes + +### ActionWebPush::Base + +Base class for notification senders with ActionMailer-like interface. + +```ruby +class NotificationSender < ActionWebPush::Base + default from_subscription: -> { current_subscription } + + def welcome(user) + web_push( + title: "Welcome!", + body: "Thanks for joining", + to: user.push_subscriptions + ) + end +end + +# Usage +NotificationSender.welcome(user).deliver_later +``` + +#### Class Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `default(params)` | Hash | Void | Set default parameters | +| `web_push(params)` | Hash | Notification | Create notification | + +#### Instance Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `web_push(params)` | Hash | Notification | Create notification | + +## Rate Limiting + +### ActionWebPush::RateLimiter + +Prevent abuse with configurable rate limits. + +```ruby +rate_limiter = ActionWebPush::RateLimiter.new( + limits: { + endpoint: { max_requests: 100, window: 3600 }, + user: { max_requests: 1000, window: 3600 } + } +) + +# Check rate limit +begin + rate_limiter.check_rate_limit!(:endpoint, endpoint_url, user_id) +rescue ActionWebPush::RateLimitExceeded => e + # Handle rate limit exceeded +end + +# Check without raising +if rate_limiter.within_rate_limit?(:endpoint, endpoint_url, user_id) + # Proceed with notification +end + +# Get rate limit info +info = rate_limiter.rate_limit_info(:endpoint, endpoint_url, user_id) +# => { limit: 100, remaining: 95, window: 3600, reset_at: Time } +``` + +#### Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `check_rate_limit!(type, id, user_id)` | Symbol, String, String | Boolean | Check rate limit, raise if exceeded | +| `within_rate_limit?(type, id, user_id)` | Symbol, String, String | Boolean | Check rate limit, return boolean | +| `rate_limit_info(type, id, user_id)` | Symbol, String, String | Hash | Get rate limit information | + +#### Rate Limit Types + +| Type | Default Limit | Description | +|------|---------------|-------------| +| `:endpoint` | 100/hour | Per endpoint URL | +| `:user` | 1000/hour | Per user | +| `:global` | 10000/hour | Global limit | +| `:subscription` | 50/hour | Per subscription | + +## Error Handling + +### Exception Hierarchy + +``` +ActionWebPush::Error +├── ActionWebPush::DeliveryError +├── ActionWebPush::ExpiredSubscriptionError +├── ActionWebPush::ConfigurationError +└── ActionWebPush::RateLimitExceeded +``` + +### ActionWebPush::ErrorHandler + +Centralized error handling with instrumentation. + +```ruby +begin + notification.deliver +rescue ActionWebPush::ExpiredSubscriptionError => e + # Automatically handles cleanup and instrumentation +rescue ActionWebPush::RateLimitExceeded => e + # Automatically handles rate limit logging and instrumentation +end +``` + +#### Handler Methods + +| Method | Parameters | Description | +|--------|------------|-------------| +| `handle_delivery_error(error, context)` | Error, Hash | Route error to appropriate handler | +| `handle_expired_subscription_error(error, context)` | Error, Hash | Handle expired subscriptions | +| `handle_rate_limit_error(error, context)` | Error, Hash | Handle rate limit exceeded | +| `handle_delivery_failure(error, context)` | Error, Hash | Handle delivery failures | +| `handle_configuration_error(error, context)` | Error, Hash | Handle configuration errors | +| `handle_unexpected_error(error, context)` | Error, Hash | Handle unexpected errors | + +## Instrumentation + +### ActionWebPush::Instrumentation + +Integrated with ActiveSupport::Notifications for monitoring. + +```ruby +# Subscribe to all ActionWebPush events +ActiveSupport::Notifications.subscribe(/^action_web_push\./) do |name, start, finish, id, payload| + duration = finish - start + Rails.logger.info "#{name}: #{duration}ms #{payload.inspect}" +end + +# Subscribe to specific events +ActiveSupport::Notifications.subscribe("action_web_push.notification_delivery") do |name, start, finish, id, payload| + # payload includes: endpoint, title, urgency, success, response_code +end +``` + +#### Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `notification_delivery` | endpoint, title, urgency, success, response_code | Notification delivery attempt | +| `subscription_expired` | endpoint, error, subscription_id | Subscription expired | +| `rate_limit_exceeded` | resource_type, resource_id, current_count | Rate limit hit | +| `pool_overflow` | overflow_count, total_queued, overflow_rate | Pool queue overflow | +| `notification_delivery_failed` | endpoint, title, error, error_class | Delivery failed | +| `configuration_error` | error, context | Configuration error | +| `unexpected_error` | error, error_class, backtrace, context | Unexpected error | + +## ActiveRecord Models + +### ActionWebPush::Subscription + +Manage push subscriptions in your database. + +```ruby +# Find subscriptions +subscriptions = ActionWebPush::Subscription.active +user_subscriptions = ActionWebPush::Subscription.for_user(user) + +# Create subscription +subscription = ActionWebPush::Subscription.create!( + endpoint: "https://fcm.googleapis.com/fcm/send/...", + p256dh_key: "BNbN3OiAT...", + auth_key: "k2i6t8hBmF...", + user: current_user +) + +# Build notification +notification = subscription.build_notification( + title: "Hello", + body: "World", + data: { url: "/path" } +) +``` + +#### Scopes + +| Scope | Parameters | Description | +|-------|------------|-------------| +| `active` | None | Non-expired subscriptions | +| `expired` | None | Expired subscriptions | +| `for_user(user)` | User | Subscriptions for specific user | +| `by_endpoint(endpoint)` | String | Find by endpoint | +| `by_user_agent(agent)` | String | Find by user agent pattern | + +#### Instance Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `build_notification(params)` | Hash | Notification | Build notification for subscription | +| `expired?` | None | Boolean | Check if subscription is expired | +| `mark_as_expired!` | None | Boolean | Mark subscription as expired | + +## Background Jobs + +### ActionWebPush::DeliveryJob + +ActiveJob for asynchronous delivery. + +```ruby +# Enqueue job directly +ActionWebPush::DeliveryJob.perform_later(notification_params) + +# Via notification (recommended) +notification.deliver_later +``` + +#### Job Methods + +| Method | Parameters | Description | +|--------|------------|-------------| +| `perform(notification_params)` | Hash | Deliver notification from parameters | + +## Thread Pool Management + +### ActionWebPush::Pool + +Manage concurrent deliveries with thread pooling. + +```ruby +# Create custom pool +pool = ActionWebPush::Pool.new( + invalid_subscription_handler: ->(subscription_id) { + ActionWebPush::Subscription.find(subscription_id)&.destroy + } +) + +# Queue notifications +pool.queue(notification) +pool.queue(notifications_array) + +# Get metrics +metrics = pool.metrics +# => { total_queued: 1000, overflow_count: 5, overflow_rate: 0.5, ... } + +# Shutdown gracefully +pool.shutdown +``` + +#### Methods + +| Method | Parameters | Returns | Description | +|--------|------------|---------|-------------| +| `queue(notifications, subscriptions)` | Mixed, Array | Void | Queue for delivery | +| `metrics` | None | Hash | Get pool metrics | +| `shutdown` | None | Void | Graceful shutdown | + +## Examples + +### Basic Usage + +```ruby +# Configure +ActionWebPush.configure do |config| + config.vapid_subject = "mailto:admin@example.com" + config.vapid_public_key = ENV['VAPID_PUBLIC_KEY'] + config.vapid_private_key = ENV['VAPID_PRIVATE_KEY'] +end + +# Send notification +notification = ActionWebPush::Notification.new( + title: "New Message", + body: "You have a new message", + endpoint: subscription.endpoint, + p256dh_key: subscription.p256dh_key, + auth_key: subscription.auth_key, + data: { message_id: 123 } +) + +notification.deliver_later +``` + +### ActionMailer-like Senders + +```ruby +class UserNotifications < ActionWebPush::Base + def welcome(user) + web_push( + title: "Welcome!", + body: "Thanks for joining our app", + to: user.push_subscriptions, + data: { type: 'welcome' } + ) + end + + def message_received(user, message) + web_push( + title: "New Message", + body: message.preview, + to: user.push_subscriptions, + data: { + type: 'message', + message_id: message.id, + url: message_url(message) + } + ) + end +end + +# Usage +UserNotifications.welcome(user).deliver_later +UserNotifications.message_received(user, message).deliver_now +``` + +### Batch Operations + +```ruby +# Send to multiple users +users = User.where(notifications_enabled: true) +notifications = users.map do |user| + user.push_subscriptions.map do |subscription| + subscription.build_notification( + title: "System Maintenance", + body: "Scheduled maintenance tonight", + data: { type: 'announcement' } + ) + end +end.flatten + +ActionWebPush::BatchDelivery.deliver(notifications) +``` + +### Error Handling + +```ruby +begin + notification.deliver +rescue ActionWebPush::ExpiredSubscriptionError + # Remove expired subscription + subscription.destroy +rescue ActionWebPush::RateLimitExceeded => e + # Retry later or queue for later delivery + notification.deliver_later(wait: 1.hour) +rescue ActionWebPush::DeliveryError => e + # Log and handle delivery failure + Rails.logger.error "Push delivery failed: #{e.message}" +end +``` + +### Monitoring + +```ruby +# Subscribe to all push notifications events +ActiveSupport::Notifications.subscribe("action_web_push.notification_delivery") do |name, start, finish, id, payload| + duration = (finish - start) * 1000 + + if payload[:success] + Rails.logger.info "Push delivered in #{duration.round(2)}ms to #{payload[:endpoint]}" + else + Rails.logger.warn "Push failed to #{payload[:endpoint]}: #{payload[:response_code]}" + end +end + +# Custom metrics collection +ActiveSupport::Notifications.subscribe("action_web_push.rate_limit_exceeded") do |name, start, finish, id, payload| + Metrics.increment("push.rate_limit_exceeded", + tags: { resource_type: payload[:resource_type] } + ) +end +``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3cf265e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,56 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial preparation for public release +- Comprehensive test suite improvements +- Documentation enhancements + +### Fixed +- Thread safety improvements +- Rate limiting optimizations + +## [0.1.0] - 2025-09-22 + +### Added +- Initial release of ActionWebPush gem +- ActionMailer-like interface for Web Push notifications +- Thread pool management with concurrent-ruby +- Rate limiting with Redis and memory stores +- Multiple delivery methods (WebPush, Test) +- Rails engine integration with generators and migrations +- ActiveRecord model for subscription management +- VAPID key management and configuration +- Background job integration with ActiveJob +- Comprehensive error handling and retry logic +- Sentry integration for error reporting +- Analytics and metrics collection +- Status broadcasting via ActionCable +- Tenant configuration support +- Campfire migration support + +### Security +- VAPID key validation and management +- Rate limiting to prevent abuse +- Secure subscription handling +- Input validation and sanitization + +### Performance +- Thread pool optimization for concurrent delivery +- Connection pooling with Net::HTTP::Persistent +- Efficient batch delivery mechanisms +- Memory and Redis-based rate limiting stores + +### Documentation +- Basic README with usage examples +- Generator-based installation process +- Configuration examples and best practices + +[Unreleased]: https://github.com/keshav-k3/actionwebpush/compare/v0.1.0...HEAD +[0.1.0]: https://github.com/keshav-k3/actionwebpush/releases/tag/v0.1.0 \ No newline at end of file diff --git a/README.md b/README.md index 958735e..3cb0265 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,583 @@ # ActionWebPush +[![Gem Version](https://badge.fury.io/rb/actionwebpush.svg)](https://badge.fury.io/rb/actionwebpush) +[![Build Status](https://github.com/keshav-k3/actionwebpush/workflows/CI/badge.svg)](https://github.com/keshav-k3/actionwebpush/actions) + Rails integration for Web Push notifications with ActionMailer-like interface. -This gem has been extracted from the Campfire project and provides a Rails-integrated solution for Web Push notifications. +ActionWebPush provides a comprehensive solution for sending Web Push notifications in Rails applications. It offers an ActionMailer-inspired API, background job integration, rate limiting, and sophisticated error handling. + +Extracted from the [Campfire](https://github.com/keshav-k3/once-campfire) project and designed for production use. + +> **🎉 Version 0.1.0** - Initial stable release with full feature set including ActionMailer-like interface, background jobs, rate limiting, and comprehensive error handling. + +## Features + +- 🚀 **ActionMailer-like Interface** - Familiar Rails patterns for sending notifications +- 🔧 **Easy Configuration** - Simple setup with VAPID keys +- 🎯 **Background Jobs** - ActiveJob integration for async delivery +- 📊 **Rate Limiting** - Built-in protection against abuse +- 🔄 **Thread Pool Management** - Efficient concurrent delivery +- 📈 **Instrumentation** - ActiveSupport::Notifications integration +- 🛡️ **Error Handling** - Comprehensive error management with cleanup +- 🗄️ **ActiveRecord Integration** - Models and migrations included +- 🧪 **Test Helpers** - Testing utilities for development +- 📝 **Detailed Logging** - Structured logging for debugging + +## Table of Contents -### WIP ^-^ +- [Installation](#installation) +- [Configuration](#configuration) +- [Quick Start](#quick-start) +- [Usage](#usage) + - [Basic Notifications](#basic-notifications) + - [ActionMailer-like Senders](#actionmailer-like-senders) + - [Background Delivery](#background-delivery) + - [Batch Operations](#batch-operations) +- [Subscription Management](#subscription-management) +- [Rate Limiting](#rate-limiting) +- [Error Handling](#error-handling) +- [Monitoring](#monitoring) +- [Testing](#testing) +- [Configuration Reference](#configuration-reference) +- [API Documentation](#api-documentation) +- [Contributing](#contributing) +- [Requirements](#requirements) +- [License](#license) + +## Installation + +Add this line to your application's Gemfile: ```ruby gem 'actionwebpush' ``` +And then execute: + ```bash bundle install +``` + +Run the installation generator: + +```bash rails generate action_web_push:install +``` + +This will: +- Create configuration file `config/initializers/action_web_push.rb` +- Generate database migrations for subscription management +- Create VAPID keys if not present + +Run the migrations: + +```bash rails db:migrate ``` +## Configuration + +Configure ActionWebPush in `config/initializers/action_web_push.rb`: + ```ruby -# Configure in config/initializers/action_web_push.rb ActionWebPush.configure do |config| + # Required: VAPID keys for push service authentication config.vapid_public_key = ENV['VAPID_PUBLIC_KEY'] config.vapid_private_key = ENV['VAPID_PRIVATE_KEY'] - config.vapid_subject = 'mailto:support@example.com' + config.vapid_subject = 'mailto:support@yourapp.com' + + # Optional: Performance tuning + config.pool_size = 10 # Thread pool size + config.queue_size = 100 # Queue size + config.connection_pool_size = 5 # HTTP connection pool + config.batch_size = 100 # Default batch size + + # Optional: Delivery method (default: :web_push) + config.delivery_method = :web_push end +``` -# In your User model +### Generate VAPID Keys + +If you don't have VAPID keys, generate them: + +```bash +rails generate action_web_push:vapid_keys +``` + +This creates a `.env` file with your keys. Add them to your production environment: + +```bash +# .env +VAPID_PUBLIC_KEY=your_generated_public_key +VAPID_PRIVATE_KEY=your_generated_private_key +``` + +## Quick Start + +### 1. Set up User Associations + +```ruby class User < ApplicationRecord - has_many :push_subscriptions, class_name: "ActionWebPush::Subscription" + has_many :push_subscriptions, + class_name: "ActionWebPush::Subscription", + foreign_key: :user_id, + dependent: :destroy end +``` + +### 2. Create Notification Senders + +```ruby +class UserNotifications < ActionWebPush::Base + def welcome(user) + web_push( + title: "Welcome to MyApp!", + body: "Thanks for joining us", + to: user.push_subscriptions, + data: { type: 'welcome', url: '/dashboard' } + ) + end -# Create a notification class -class NotificationPusher < ActionWebPush::Base def new_message(user, message) - push( - user.push_subscriptions, + web_push( title: "New Message", - body: message.content, - data: { url: message_path(message) } + body: message.preview, + to: user.push_subscriptions, + data: { + type: 'message', + message_id: message.id, + url: message_path(message) + } ) end end +``` + +### 3. Send Notifications + +```ruby +# Deliver immediately +UserNotifications.welcome(user).deliver_now + +# Deliver via background job (recommended) +UserNotifications.new_message(user, message).deliver_later + +# Batch delivery to multiple users +users = User.active.includes(:push_subscriptions) +users.each do |user| + UserNotifications.welcome(user).deliver_later +end +``` + +## Usage + +### Basic Notifications + +Create and send notifications directly: + +```ruby +notification = ActionWebPush::Notification.new( + title: "System Alert", + body: "Maintenance scheduled for tonight", + endpoint: subscription.endpoint, + p256dh_key: subscription.p256dh_key, + auth_key: subscription.auth_key, + data: { type: 'maintenance', scheduled_at: '2024-01-01T02:00:00Z' }, + icon: '/system-icon.png', + badge: '/badge.png', + urgency: 'high' +) + +# Synchronous delivery +notification.deliver_now + +# Asynchronous delivery +notification.deliver_later +``` + +### ActionMailer-like Senders + +Create notification classes that inherit from `ActionWebPush::Base`: + +```ruby +class SystemNotifications < ActionWebPush::Base + default data: { app: 'MyApp' } + + def maintenance_notice(users, maintenance) + web_push( + title: "Scheduled Maintenance", + body: "Service will be unavailable #{maintenance.start_time}", + to: users.flat_map(&:push_subscriptions), + data: { + type: 'maintenance', + start_time: maintenance.start_time.iso8601, + duration: maintenance.duration_minutes, + url: maintenance_path(maintenance) + }, + urgency: 'high' + ) + end + + def feature_announcement(user, feature) + web_push( + title: "New Feature Available!", + body: feature.description, + to: user.push_subscriptions, + data: { + type: 'feature', + feature_id: feature.id, + url: feature_path(feature) + }, + icon: feature.icon_url + ) + end +end +``` + +### Background Delivery + +Leverage ActiveJob for asynchronous delivery: + +```ruby +# Basic background delivery +notification.deliver_later + +# With scheduling +notification.deliver_later(wait: 1.hour) +notification.deliver_later(wait_until: Date.tomorrow.noon) + +# Custom queue and priority +notification.deliver_later( + queue: :critical_notifications, + priority: 10 +) + +# ActionMailer-like senders +UserNotifications.welcome(user).deliver_later(wait: 5.minutes) +``` + +### Batch Operations + +Efficiently send to multiple recipients: + +```ruby +# Using BatchDelivery for performance +notifications = users.map do |user| + user.push_subscriptions.map do |subscription| + subscription.build_notification( + title: "Weekly Update", + body: "Check out this week's highlights", + data: { type: 'weekly_update' } + ) + end +end.flatten + +ActionWebPush::BatchDelivery.deliver(notifications) + +# With custom batch size +ActionWebPush::BatchDelivery.new(notifications, batch_size: 50).deliver_all + +# Using ActionMailer-like pattern for batches +users.find_each do |user| + WeeklyNotifications.digest(user).deliver_later +end +``` + +## Subscription Management + +ActionWebPush includes ActiveRecord models for managing subscriptions: + +```ruby +# Create subscription from frontend +subscription = ActionWebPush::Subscription.create!( + endpoint: params[:endpoint], + p256dh_key: params[:keys][:p256dh], + auth_key: params[:keys][:auth], + user: current_user, + user_agent: request.user_agent +) + +# Find subscriptions +user_subscriptions = ActionWebPush::Subscription.for_user(current_user) +active_subscriptions = ActionWebPush::Subscription.active +mobile_subscriptions = ActionWebPush::Subscription.by_user_agent('Mobile') + +# Build notifications from subscriptions +notification = subscription.build_notification( + title: "Hello", + body: "World", + data: { url: '/dashboard' } +) + +# Cleanup expired subscriptions +ActionWebPush::Subscription.expired.destroy_all +``` + +### Frontend Integration + +Example JavaScript for subscription management: + +```javascript +// Register service worker and get subscription +navigator.serviceWorker.register('/sw.js').then(registration => { + return registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: '<%= ActionWebPush.config.vapid_public_key %>' + }); +}).then(subscription => { + // Send subscription to your Rails app + fetch('/push_subscriptions', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content + }, + body: JSON.stringify({ + subscription: { + endpoint: subscription.endpoint, + keys: { + p256dh: arrayBufferToBase64(subscription.getKey('p256dh')), + auth: arrayBufferToBase64(subscription.getKey('auth')) + } + } + }) + }); +}); +``` + +## Rate Limiting + +Protect your application from abuse with built-in rate limiting: + +```ruby +# Configure rate limits +rate_limiter = ActionWebPush::RateLimiter.new( + limits: { + endpoint: { max_requests: 100, window: 3600 }, # 100/hour per endpoint + user: { max_requests: 1000, window: 3600 }, # 1000/hour per user + subscription: { max_requests: 50, window: 3600 } # 50/hour per subscription + } +) + +# Check before sending +begin + rate_limiter.check_rate_limit!(:user, current_user.id) + notification.deliver_now +rescue ActionWebPush::RateLimitExceeded => e + render json: { error: "Rate limit exceeded" }, status: 429 +end + +# Get rate limit information +info = rate_limiter.rate_limit_info(:user, current_user.id) +# => { limit: 1000, remaining: 950, window: 3600, reset_at: Time } +``` + +## Error Handling + +ActionWebPush provides comprehensive error handling: + +```ruby +begin + notification.deliver_now +rescue ActionWebPush::ExpiredSubscriptionError => e + # Subscription is no longer valid - cleanup + subscription.destroy +rescue ActionWebPush::RateLimitExceeded => e + # Rate limit hit - retry later + notification.deliver_later(wait: 1.hour) +rescue ActionWebPush::DeliveryError => e + # Delivery failed - log and handle + Rails.logger.error "Push notification failed: #{e.message}" +rescue ActionWebPush::ConfigurationError => e + # Configuration issue - check VAPID keys + Rails.logger.error "Push configuration error: #{e.message}" +end +``` + +All errors include detailed context and are automatically instrumented for monitoring. + +## Monitoring + +ActionWebPush integrates with ActiveSupport::Notifications for comprehensive monitoring: + +```ruby +# Subscribe to all ActionWebPush events +ActiveSupport::Notifications.subscribe(/^action_web_push\./) do |name, start, finish, id, payload| + duration = (finish - start) * 1000 + Rails.logger.info "#{name}: #{duration.round(2)}ms #{payload.inspect}" +end + +# Monitor specific events +ActiveSupport::Notifications.subscribe("action_web_push.notification_delivery") do |name, start, finish, id, payload| + if payload[:success] + Metrics.increment('push.delivery.success') + else + Metrics.increment('push.delivery.failure', tags: { code: payload[:response_code] }) + end +end + +# Track rate limiting +ActiveSupport::Notifications.subscribe("action_web_push.rate_limit_exceeded") do |name, start, finish, id, payload| + Metrics.increment('push.rate_limit_exceeded', + tags: { resource_type: payload[:resource_type] }) +end + +# Pool overflow monitoring +ActiveSupport::Notifications.subscribe("action_web_push.pool_overflow") do |name, start, finish, id, payload| + Metrics.gauge('push.pool.overflow_rate', payload[:overflow_rate]) +end +``` + +### Available Events + +- `action_web_push.notification_delivery` - Individual notification delivery +- `action_web_push.subscription_expired` - Subscription marked as expired +- `action_web_push.rate_limit_exceeded` - Rate limit threshold hit +- `action_web_push.pool_overflow` - Thread pool queue overflow +- `action_web_push.notification_delivery_failed` - Delivery failure +- `action_web_push.configuration_error` - Configuration validation error +- `action_web_push.unexpected_error` - Unexpected error occurred + +## Testing + +ActionWebPush includes comprehensive test helpers: + +```ruby +# Test mode (add to test environment) +ActionWebPush.configure do |config| + config.delivery_method = :test +end + +# In your tests +require 'action_web_push/test_helper' + +class NotificationTest < ActiveSupport::TestCase + include ActionWebPush::TestHelper + + test "sends welcome notification" do + user = users(:alice) + + assert_enqueued_push_deliveries 1 do + UserNotifications.welcome(user).deliver_later + end + + assert_push_delivered_to user.push_subscriptions.first do |notification| + assert_equal "Welcome!", notification[:title] + assert_equal "welcome", notification[:data][:type] + end + end + + test "handles expired subscriptions" do + expired_subscription = push_subscriptions(:expired) + + assert_raises ActionWebPush::ExpiredSubscriptionError do + notification = expired_subscription.build_notification( + title: "Test", body: "Test" + ) + notification.deliver_now + end + end +end +``` + +### Test Helpers + +- `assert_push_delivered_to(subscription, &block)` - Assert notification delivered +- `assert_enqueued_push_deliveries(count, &block)` - Assert jobs enqueued +- `assert_no_push_deliveries(&block)` - Assert no deliveries +- `clear_push_deliveries` - Clear test delivery queue + +## Configuration Reference + +Complete configuration options: + +```ruby +ActionWebPush.configure do |config| + # VAPID Configuration (Required) + config.vapid_subject = "mailto:admin@example.com" # or "https://example.com" + config.vapid_public_key = "your_public_key" + config.vapid_private_key = "your_private_key" + + # Delivery Method + config.delivery_method = :web_push # :web_push, :test, or custom + + # Thread Pool Configuration + config.pool_size = 10 # Max concurrent deliveries + config.queue_size = 100 # Queue size before overflow + config.connection_pool_size = 5 # HTTP connection pool size + + # Batch Processing + config.batch_size = 100 # Default batch size + + # Rate Limiting (optional - uses sensible defaults) + config.rate_limits = { + endpoint: { max_requests: 100, window: 3600 }, + user: { max_requests: 1000, window: 3600 }, + global: { max_requests: 10000, window: 3600 }, + subscription: { max_requests: 50, window: 3600 } + } + + # Logging + config.logger = Rails.logger # Custom logger +end +``` + +## API Documentation + +For detailed API documentation, see [API.md](API.md). + +## Requirements + +- Ruby 2.6+ +- Rails 6.0+ +- Redis (optional, for rate limiting) + +## Dependencies + +- `web-push` - Web Push protocol implementation +- `concurrent-ruby` - Thread pool management +- `activejob` - Background job processing +- `activerecord` - Database integration + +## Contributing + +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details. + +1. Fork the repository +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Write tests for your changes +4. Ensure all tests pass (`bundle exec rake test`) +5. Commit your changes (`git commit -am 'Add some feature'`) +6. Push to the branch (`git push origin my-new-feature`) +7. Create a Pull Request + +## Changelog + +See [CHANGELOG.md](CHANGELOG.md) for a list of changes. + +## License + +The gem is available as open source under the terms of the [MIT License](LICENSE.txt). + +## Security + +For security issues, please email keshavkk.musafir@gmail.com instead of using the issue tracker. + +## Roadmap + +As this is the initial 0.1.0 release, we're excited to hear from the community! Planned future enhancements include: + +- Additional delivery method adapters +- Enhanced monitoring and metrics +- Performance optimizations +- Extended configuration options + +## Support + +- **GitHub Issues**: [Report bugs and feature requests](https://github.com/keshav-k3/actionwebpush/issues) +- **Documentation**: [API.md](API.md) +- **Examples**: See examples in this README and [API.md](API.md) -# Send notifications -NotificationPusher.new_message(user, message).deliver_now -``` \ No newline at end of file +We welcome feedback, bug reports, and contributions from the Rails community! \ No newline at end of file