sincerely 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +38 -0
- data/CHANGELOG.md +12 -0
- data/README.md +202 -0
- data/Rakefile +12 -0
- data/app/controllers/sincerely/application_controller.rb +81 -0
- data/app/controllers/sincerely/dashboard_controller.rb +112 -0
- data/app/controllers/sincerely/delivery_events_controller.rb +31 -0
- data/app/controllers/sincerely/engagement_events_controller.rb +32 -0
- data/app/controllers/sincerely/notifications_controller.rb +69 -0
- data/app/controllers/sincerely/send_controller.rb +105 -0
- data/app/controllers/sincerely/templates_controller.rb +61 -0
- data/app/helpers/sincerely/application_helper.rb +39 -0
- data/app/views/layouts/sincerely/application.html.erb +593 -0
- data/app/views/sincerely/dashboard/index.html.erb +382 -0
- data/app/views/sincerely/delivery_events/index.html.erb +97 -0
- data/app/views/sincerely/engagement_events/index.html.erb +97 -0
- data/app/views/sincerely/notifications/index.html.erb +91 -0
- data/app/views/sincerely/notifications/show.html.erb +98 -0
- data/app/views/sincerely/send/new.html.erb +592 -0
- data/app/views/sincerely/shared/_pagination.html.erb +19 -0
- data/app/views/sincerely/templates/_form.html.erb +226 -0
- data/app/views/sincerely/templates/edit.html.erb +11 -0
- data/app/views/sincerely/templates/index.html.erb +59 -0
- data/app/views/sincerely/templates/new.html.erb +11 -0
- data/app/views/sincerely/templates/preview.html.erb +48 -0
- data/app/views/sincerely/templates/show.html.erb +69 -0
- data/config/routes.rb +21 -0
- data/lib/config/application_config.rb +18 -0
- data/lib/config/sincerely_config.rb +31 -0
- data/lib/generators/sincerely/aws_ses_webhook_controller_generator.rb +31 -0
- data/lib/generators/sincerely/events_generator.rb +45 -0
- data/lib/generators/sincerely/install_generator.rb +18 -0
- data/lib/generators/sincerely/migration_generator.rb +65 -0
- data/lib/generators/templates/aws_ses_webhook_controller.rb.erb +3 -0
- data/lib/generators/templates/events/delivery_event_model.rb.erb +5 -0
- data/lib/generators/templates/events/delivery_events_create.rb.erb +20 -0
- data/lib/generators/templates/events/engagement_event_model.rb.erb +5 -0
- data/lib/generators/templates/events/engagement_events_create.rb.erb +21 -0
- data/lib/generators/templates/notification_model.rb.erb +3 -0
- data/lib/generators/templates/notifications_create.rb.erb +21 -0
- data/lib/generators/templates/notifications_update.rb.erb +16 -0
- data/lib/generators/templates/sincerely.yml +21 -0
- data/lib/generators/templates/templates_create.rb.erb +15 -0
- data/lib/sincerely/delivery_systems/email_aws_ses.rb +69 -0
- data/lib/sincerely/engine.rb +7 -0
- data/lib/sincerely/mixins/notification_model.rb +94 -0
- data/lib/sincerely/mixins/webhooks/aws_ses_events.rb +74 -0
- data/lib/sincerely/renderers/liquid.rb +14 -0
- data/lib/sincerely/services/events/aws_ses_bounce_event.rb +26 -0
- data/lib/sincerely/services/events/aws_ses_click_event.rb +25 -0
- data/lib/sincerely/services/events/aws_ses_complaint_event.rb +25 -0
- data/lib/sincerely/services/events/aws_ses_delivery_event.rb +17 -0
- data/lib/sincerely/services/events/aws_ses_event.rb +56 -0
- data/lib/sincerely/services/events/aws_ses_open_event.rb +25 -0
- data/lib/sincerely/services/process_delivery_event.rb +72 -0
- data/lib/sincerely/templates/email_liquid_template.rb +13 -0
- data/lib/sincerely/templates/notification_template.rb +22 -0
- data/lib/sincerely/version.rb +5 -0
- data/lib/sincerely.rb +20 -0
- data/sincerely.gemspec +37 -0
- metadata +187 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3126012f05e46f992478bb05e997605aef323820d2fd72c95dfe3fa49105c33c
|
|
4
|
+
data.tar.gz: 5c5c1b66fc5e956506a634b8c4da25869019f741bb66e8517e28a08eef32c209
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0b093a942bbb2d4ae549e049e47595ca42e645211b1b6d1a92be1e035cac3aa3c690c1f829505bba4e0ae61fbbfa950e08f25a9bc1c4507be2a3e2f5a2555225
|
|
7
|
+
data.tar.gz: 4f75a74cd95ec7025a04c6d549591129f6736c9303f07f866fe4213f74ba72a2fe677fc699c668efc405271e09b5b0437679bcb61a19439450400f45323fd58c
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- rubocop-rspec
|
|
3
|
+
- rubocop-rails
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
NewCops: enable
|
|
7
|
+
TargetRubyVersion: 3.1
|
|
8
|
+
|
|
9
|
+
Style/Documentation:
|
|
10
|
+
Enabled: false
|
|
11
|
+
Layout/LineLength:
|
|
12
|
+
Max: 120
|
|
13
|
+
RSpec/NestedGroups:
|
|
14
|
+
Enabled: false
|
|
15
|
+
|
|
16
|
+
RSpec/MultipleExpectations:
|
|
17
|
+
Enabled: false
|
|
18
|
+
|
|
19
|
+
# Controller methods can be complex; these metrics produce false positives
|
|
20
|
+
Metrics/AbcSize:
|
|
21
|
+
Enabled: false
|
|
22
|
+
|
|
23
|
+
Metrics/MethodLength:
|
|
24
|
+
Enabled: false
|
|
25
|
+
|
|
26
|
+
Metrics/CyclomaticComplexity:
|
|
27
|
+
Enabled: false
|
|
28
|
+
|
|
29
|
+
Metrics/PerceivedComplexity:
|
|
30
|
+
Enabled: false
|
|
31
|
+
|
|
32
|
+
# Engines don't typically use i18n for simple flash messages
|
|
33
|
+
Rails/I18nLocaleTexts:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
# Keep :unprocessable_entity for Rails < 7.1 compatibility
|
|
37
|
+
Rails/ResponseParsedBody:
|
|
38
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2024-07-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Initial release
|
|
9
|
+
- Amazon SES email sending
|
|
10
|
+
- Amazon SES email sending events
|
|
11
|
+
- liquid email templates
|
|
12
|
+
- notification statuses
|
data/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# Sincerely
|
|
2
|
+
## Introduction
|
|
3
|
+
Sincerely is a versatile Ruby gem designed for creating, delivering and monitoring email, sms or push notifications.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Installation](#installation)
|
|
7
|
+
2. [Getting Started](#getting-started)
|
|
8
|
+
3. [Configuration](#configuration)
|
|
9
|
+
4. [Usage](#usage)
|
|
10
|
+
5. [Notification model](#notification-model)
|
|
11
|
+
6. [Notification states](#notification-states)
|
|
12
|
+
7. [Callbacks](#callbacks)
|
|
13
|
+
8. [How to Run Tests](#how-to-run-tests)
|
|
14
|
+
9. [Guide for Contributing](#guide-for-contributing)
|
|
15
|
+
10. [How to Contact Us](#how-to-contact-us)
|
|
16
|
+
11. [License](#license)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
Sincerely 1.0 works with Rails 6.0 onwards. Run:
|
|
20
|
+
```bash
|
|
21
|
+
bundle add sincerely
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Getting Started
|
|
25
|
+
|
|
26
|
+
1. First, you need to run the install generator, which will create the `config/sincerely.yml` initializer file for you:
|
|
27
|
+
```bash
|
|
28
|
+
rails g sincerely:install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
2. You need to generate and run a migration to create the `notifications` and `notification_templates` tables and the `Notification` and the `NotificationTemplate` model:
|
|
32
|
+
```bash
|
|
33
|
+
rails g sincerely:migration Notification
|
|
34
|
+
|
|
35
|
+
rails db:migrate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. If you want to enable event callbacks you need to run the `events` task to create the `sincerely_delivery_events` and `sincerely_engagements_events` tables and the `Sincerely::DeliveryEvent` and `Sincerely::EngagementEvent` models:
|
|
39
|
+
```bash
|
|
40
|
+
rails g sincerely:events
|
|
41
|
+
|
|
42
|
+
rails db:migrate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
1. You need to set the notification model generated in the previous step by setting the `notification_model_name` option in the `config/sincerely.yml` file:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
# config/sincerely.yml
|
|
50
|
+
defaults: &defaults
|
|
51
|
+
notification_model_name: Notification
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
2. You need to set the delivery system for each delivery methods (email/sms/push) by setting the `delivery_methods` option in the `config/sincerely.yml` file. Please note that right now the gem supports only AWS SES email notification (`Sincerely::DeliverySystems::EmailAwsSes` module).
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
# config/sincerely.yml
|
|
58
|
+
defaults: &defaults
|
|
59
|
+
delivery_methods:
|
|
60
|
+
email:
|
|
61
|
+
delivery_system: Sincerely::DeliverySystems::EmailAwsSes
|
|
62
|
+
options:
|
|
63
|
+
region: region
|
|
64
|
+
access_key_id: your_access_key_id
|
|
65
|
+
secret_access_key: your_secret_access_key
|
|
66
|
+
configuration_set_name: config_set
|
|
67
|
+
sms:
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
* `region`: the AWS region to connect to
|
|
71
|
+
* `access_key_id`: AWS access key id
|
|
72
|
+
* `secret_access_key`: AWS secret access key
|
|
73
|
+
* `configuration_set_name`: the name of the configuration set to use when sending the email, you don't need to specify the configuration set option if you don't want to handle SES email sending events
|
|
74
|
+
|
|
75
|
+
Sincerely uses the `aws-sdk-sesv2` gem to send emails. See [sesv2](https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SESV2.html) for details.
|
|
76
|
+
|
|
77
|
+
Furthermore you need to configure Amazon SES to be able to send emails, see [SES](https://docs.aws.amazon.com/ses/latest/dg/Welcome.html) for details.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
1. Once the email delivery method is configured you need to create an email template which is used to generate the email content:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Sincerely::Templates::EmailLiquidTemplate.create(
|
|
85
|
+
name: 'test',
|
|
86
|
+
subject: 'template for sending messages',
|
|
87
|
+
sender: 'john.doe@gmail.com'
|
|
88
|
+
html_content: 'hi {{name}}',
|
|
89
|
+
text_content: 'hi {{name}}'
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
* `name`: unique id of the template
|
|
94
|
+
* `subject`: subject of the generated email (can be overwritten in the notification)
|
|
95
|
+
* `sender`: email address of the sender
|
|
96
|
+
* `html_content`: html content of the generated email, you can use the liquid syntax to use variables in the content, see [liquid](https://github.com/Shopify/liquid) for details
|
|
97
|
+
* `text_content`: text content of the generated email, you can use the liquid syntax to use variables in the content, see [liquid](https://github.com/Shopify/liquid) for details
|
|
98
|
+
|
|
99
|
+
2. You need to create the notification:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
Notification.create(
|
|
103
|
+
recipient: 'jane.doe@gmail.com',
|
|
104
|
+
notification_type: 'email',
|
|
105
|
+
template: Sincerely::Templates::EmailLiquidTemplate.first,
|
|
106
|
+
delivery_options: {
|
|
107
|
+
template_data: {
|
|
108
|
+
name: 'John Doe'
|
|
109
|
+
},
|
|
110
|
+
subject: 'subject'
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
* `recipient`: recipient of the notification
|
|
115
|
+
* `notification_type`: email/sms/push (please note that right now the gem supports only AWS SES email notifications)
|
|
116
|
+
* `template`: template to generate the notification content
|
|
117
|
+
* `delivery_options`: options for the associated template (`EmailLiquidTemplate` supports (i) the `template_data` option which contains the parameter hash for the liquid template defined in the html_content field of the associated template, (ii) the `subject` option which overwrites the subject defined in the associated template)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
3. You need to send the notification:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
notification.deliver
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Notification model
|
|
127
|
+
* `recipient`: recipient (email or phone number) of the notification
|
|
128
|
+
* `notification_type`: email | sms | push
|
|
129
|
+
* `delivery_options`: options for the associated template
|
|
130
|
+
* `delivery_system`: associated delivery system
|
|
131
|
+
* `delivery_state`: state of the notification
|
|
132
|
+
* `template_id`: associated template
|
|
133
|
+
* `sent_at`: timestamp of the delivery
|
|
134
|
+
* `message_id`: message id generated by the delivery system
|
|
135
|
+
* `error_message`: error message if the notification is rejected by the delivery system
|
|
136
|
+
|
|
137
|
+
## Notification states
|
|
138
|
+
* `draft`: default state, notification is not yet sent
|
|
139
|
+
* `accepted`: the send request was successful and the delivery system will attempt to deliver the message to the recipient’s mail server
|
|
140
|
+
* `rejected`: notification is rejected by the delivery system
|
|
141
|
+
* `delivered`: notification is successfully delivered by the delivery system
|
|
142
|
+
* `bounced`: notification is bounced (ie when an email cannot be delivered to the recipient)
|
|
143
|
+
* `opened`: notification is opened
|
|
144
|
+
* `clicked`: notification is clicked
|
|
145
|
+
* `complained`: notification is complained (ie when the recipient of your email marks your email as spam)
|
|
146
|
+
|
|
147
|
+
Please note that the notification state is updated only if the event callback is configured.
|
|
148
|
+
|
|
149
|
+
## Callbacks
|
|
150
|
+
|
|
151
|
+
`EmailAwsSes` delivery system handles SES email sending events. To enable it make sure:
|
|
152
|
+
* you have run the `events` task described in the `Getting Started` section
|
|
153
|
+
* you have set the `configuration_set_name` option described in the `Configuration` section
|
|
154
|
+
* you have run the following task that generates the webhook controller for you
|
|
155
|
+
```bash
|
|
156
|
+
rails g sincerely:aws_ses_webhook_controller SES_WEBHOOK
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Furthermore you need to configure Amazon SES to monitor email sending, see [SES email sending events](https://docs.aws.amazon.com/ses/latest/dg/monitor-using-event-publishing.html) for details.
|
|
160
|
+
|
|
161
|
+
Once the webhook controller is in place, it will:
|
|
162
|
+
* update the state of the notifications
|
|
163
|
+
* create event records for the bounce/complaint/open/click events
|
|
164
|
+
|
|
165
|
+
Delivery events (delivery/bounce) fields:
|
|
166
|
+
* `message_id`: message id generated by the delivery system
|
|
167
|
+
* `delivery_system`: associated delivery system
|
|
168
|
+
* `event_type`: delivery | bounce
|
|
169
|
+
* `recipient`: recipient of the notification
|
|
170
|
+
* `delivery_event_type`: bounce type
|
|
171
|
+
* `delivery_event_subtype`: bounce subtype
|
|
172
|
+
* `options`: other event specific options (if any)
|
|
173
|
+
* `timestamp`: timestamp of the event
|
|
174
|
+
|
|
175
|
+
Engagement events (open/click/compaint) fields:
|
|
176
|
+
* `message_id`: message id generated by the delivery system
|
|
177
|
+
* `delivery_system`: associated delivery system
|
|
178
|
+
* `event_type`: open | click | compaint
|
|
179
|
+
* `recipient`: recipient of the notification
|
|
180
|
+
* `ip_address`: the recipient's IP address
|
|
181
|
+
* `user_agent`: the user agent of the device or email client
|
|
182
|
+
* `link`: the URL of the link that the recipient clicked
|
|
183
|
+
* `feedback_type`: complaint feedback type
|
|
184
|
+
* `options`: other event specific options (if any)
|
|
185
|
+
* `timestamp`: timestamp of the event
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
## How to Run Tests
|
|
189
|
+
You can run unit tests for RubyBlok with the following command:
|
|
190
|
+
```
|
|
191
|
+
bundle exec rspec
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Guide for Contributing
|
|
195
|
+
Contributions are made to this repository via Issues and Pull Requests (PRs).
|
|
196
|
+
Issues should be used to report bugs, request a new feature, or to discuss potential changes before a PR is created.
|
|
197
|
+
|
|
198
|
+
## How to Contact Us
|
|
199
|
+
For any inquiries, reach out to us at: info@rubyblok.com
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
RubyBlok is released under the MIT License.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class ApplicationController < ActionController::Base
|
|
5
|
+
protect_from_forgery with: :exception
|
|
6
|
+
|
|
7
|
+
layout 'sincerely/application'
|
|
8
|
+
|
|
9
|
+
before_action :authenticate_user!
|
|
10
|
+
|
|
11
|
+
helper_method :notification_model
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def authenticate_user!
|
|
16
|
+
return unless Sincerely.authenticate_with
|
|
17
|
+
|
|
18
|
+
instance_exec(&Sincerely.authenticate_with)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def notification_model
|
|
22
|
+
@notification_model ||= Sincerely.notification_model
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def paginate(collection, per_page: 25)
|
|
26
|
+
page = (params[:page] || 1).to_i
|
|
27
|
+
offset = (page - 1) * per_page
|
|
28
|
+
total = collection.count
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
records: collection.offset(offset).limit(per_page),
|
|
32
|
+
total_count: total,
|
|
33
|
+
current_page: page,
|
|
34
|
+
per_page:,
|
|
35
|
+
total_pages: (total.to_f / per_page).ceil
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def time_filter_start
|
|
40
|
+
period = params[:period]
|
|
41
|
+
period = '24h' unless %w[1h 24h 7d 30d 3m all].include?(period)
|
|
42
|
+
|
|
43
|
+
case period
|
|
44
|
+
when '1h' then 1.hour.ago
|
|
45
|
+
when '24h' then 24.hours.ago
|
|
46
|
+
when '7d' then 7.days.ago
|
|
47
|
+
when '30d' then 30.days.ago
|
|
48
|
+
when '3m' then 3.months.ago
|
|
49
|
+
when 'all' then nil
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def apply_time_filter(collection, column: :created_at)
|
|
54
|
+
start_time = time_filter_start
|
|
55
|
+
return collection unless start_time
|
|
56
|
+
|
|
57
|
+
collection.where("#{column} >= ?", start_time)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def apply_notification_filter(scope)
|
|
61
|
+
filter = Sincerely.config.filter_notifications_by
|
|
62
|
+
return scope unless filter.respond_to?(:call)
|
|
63
|
+
|
|
64
|
+
conditions = instance_exec(&filter)
|
|
65
|
+
return scope if conditions.blank?
|
|
66
|
+
|
|
67
|
+
scope.where(conditions)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def apply_event_filter(scope)
|
|
71
|
+
filter = Sincerely.config.filter_notifications_by
|
|
72
|
+
return scope unless filter.respond_to?(:call)
|
|
73
|
+
|
|
74
|
+
conditions = instance_exec(&filter)
|
|
75
|
+
return scope if conditions.blank?
|
|
76
|
+
|
|
77
|
+
message_ids = notification_model.where(conditions).where.not(message_id: nil).pluck(:message_id)
|
|
78
|
+
scope.where(message_id: message_ids)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class DashboardController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
filtered_notifications = apply_time_filter(notification_model)
|
|
7
|
+
filtered_notifications = apply_notification_filter(filtered_notifications)
|
|
8
|
+
|
|
9
|
+
@total_notifications = filtered_notifications.count
|
|
10
|
+
@notifications_by_state = filtered_notifications.group(:delivery_state).count
|
|
11
|
+
@recent_notifications = filtered_notifications.order(created_at: :desc).limit(10)
|
|
12
|
+
|
|
13
|
+
# Delivery rates
|
|
14
|
+
total_sent = filtered_notifications.where.not(delivery_state: %w[draft rejected]).count
|
|
15
|
+
delivered = filtered_notifications.where(delivery_state: %w[delivered opened clicked]).count
|
|
16
|
+
@delivery_rate = total_sent.positive? ? (delivered.to_f / total_sent * 100).round(1) : 0
|
|
17
|
+
|
|
18
|
+
# Engagement rates
|
|
19
|
+
delivered_count = filtered_notifications.where(delivery_state: %w[delivered opened clicked]).count
|
|
20
|
+
engaged = filtered_notifications.where(delivery_state: %w[opened clicked]).count
|
|
21
|
+
@open_rate = delivered_count.positive? ? (engaged.to_f / delivered_count * 100).round(1) : 0
|
|
22
|
+
|
|
23
|
+
# Recent events (filtered by user's notifications)
|
|
24
|
+
user_message_ids = filtered_notifications.where.not(message_id: nil).pluck(:message_id)
|
|
25
|
+
@recent_delivery_events = apply_time_filter(Sincerely::DeliveryEvent)
|
|
26
|
+
.where(message_id: user_message_ids)
|
|
27
|
+
.order(created_at: :desc).limit(5)
|
|
28
|
+
@recent_engagement_events = apply_time_filter(Sincerely::EngagementEvent)
|
|
29
|
+
.where(message_id: user_message_ids)
|
|
30
|
+
.order(created_at: :desc).limit(5)
|
|
31
|
+
|
|
32
|
+
# Bounce rate
|
|
33
|
+
bounced = filtered_notifications.where(delivery_state: 'bounced').count
|
|
34
|
+
@bounce_rate = total_sent.positive? ? (bounced.to_f / total_sent * 100).round(1) : 0
|
|
35
|
+
|
|
36
|
+
# Current period for display
|
|
37
|
+
@current_period = params[:period] || '24h'
|
|
38
|
+
|
|
39
|
+
@notifications_timeline = build_timeline_data(filtered_notifications)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def build_timeline_data(notifications)
|
|
45
|
+
bucket_config = timeline_bucket_config
|
|
46
|
+
buckets = generate_time_buckets(bucket_config)
|
|
47
|
+
interval = bucket_config[:interval]
|
|
48
|
+
series = {}
|
|
49
|
+
|
|
50
|
+
notifications.find_each do |notification|
|
|
51
|
+
bucket_index = find_bucket_index(notification.created_at, buckets, interval)
|
|
52
|
+
next if bucket_index.nil?
|
|
53
|
+
|
|
54
|
+
state = notification.delivery_state
|
|
55
|
+
series[state] ||= Array.new(buckets.length, 0)
|
|
56
|
+
series[state][bucket_index] += 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
labels = buckets.map { |b| format_bucket_label(b, bucket_config) }
|
|
60
|
+
|
|
61
|
+
{ labels:, series:, buckets: }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def timeline_bucket_config
|
|
65
|
+
period = params[:period]
|
|
66
|
+
period = '24h' unless %w[1h 24h 7d 30d 3m all].include?(period)
|
|
67
|
+
|
|
68
|
+
case period
|
|
69
|
+
when '1h'
|
|
70
|
+
{ interval: 5.minutes, format: '%H:%M', start: 1.hour.ago }
|
|
71
|
+
when '24h'
|
|
72
|
+
{ interval: 1.hour, format: '%H:%M', start: 24.hours.ago }
|
|
73
|
+
when '7d'
|
|
74
|
+
{ interval: 6.hours, format: '%a %H:%M', start: 7.days.ago }
|
|
75
|
+
when '30d'
|
|
76
|
+
{ interval: 1.day, format: '%b %d', start: 30.days.ago }
|
|
77
|
+
when '3m'
|
|
78
|
+
{ interval: 1.week, format: '%b %d', start: 3.months.ago }
|
|
79
|
+
when 'all'
|
|
80
|
+
{ interval: 1.month, format: '%b %Y', start: 1.year.ago }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def generate_time_buckets(config)
|
|
85
|
+
buckets = []
|
|
86
|
+
current = config[:start].beginning_of_hour
|
|
87
|
+
now = Time.current
|
|
88
|
+
|
|
89
|
+
while current <= now
|
|
90
|
+
buckets << current
|
|
91
|
+
current += config[:interval]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
buckets
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def find_bucket_index(time, buckets, interval)
|
|
98
|
+
return nil if buckets.empty?
|
|
99
|
+
|
|
100
|
+
buckets.each_with_index do |bucket_start, index|
|
|
101
|
+
bucket_end = bucket_start + interval
|
|
102
|
+
return index if time >= bucket_start && time < bucket_end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
buckets.length - 1 if time >= buckets.last && time < buckets.last + interval
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def format_bucket_label(bucket, config)
|
|
109
|
+
bucket.strftime(config[:format])
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class DeliveryEventsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@events = apply_time_filter(Sincerely::DeliveryEvent).order(created_at: :desc)
|
|
7
|
+
@events = apply_event_filter(@events)
|
|
8
|
+
|
|
9
|
+
@events = @events.where(event_type: params[:event_type]) if params[:event_type].present?
|
|
10
|
+
@events = @events.where('recipient LIKE ?', "%#{params[:recipient]}%") if params[:recipient].present?
|
|
11
|
+
|
|
12
|
+
if params[:template_id].present? || params[:notification_type].present?
|
|
13
|
+
notification_scope = notification_model.all
|
|
14
|
+
if params[:template_id].present?
|
|
15
|
+
notification_scope = notification_scope.where(template_id: params[:template_id])
|
|
16
|
+
end
|
|
17
|
+
if params[:notification_type].present?
|
|
18
|
+
notification_scope = notification_scope.where(notification_type: params[:notification_type])
|
|
19
|
+
end
|
|
20
|
+
message_ids = notification_scope.pluck(:message_id).compact
|
|
21
|
+
@events = @events.where(message_id: message_ids)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@pagination = paginate(@events)
|
|
25
|
+
@events = @pagination[:records]
|
|
26
|
+
|
|
27
|
+
@event_types = Sincerely::DeliveryEvent.distinct.pluck(:event_type).compact
|
|
28
|
+
@templates = Sincerely::Templates::NotificationTemplate.all
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class EngagementEventsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@events = apply_time_filter(Sincerely::EngagementEvent).order(created_at: :desc)
|
|
7
|
+
@events = apply_event_filter(@events)
|
|
8
|
+
|
|
9
|
+
@events = @events.where(event_type: params[:event_type]) if params[:event_type].present?
|
|
10
|
+
@events = @events.where('recipient LIKE ?', "%#{params[:recipient]}%") if params[:recipient].present?
|
|
11
|
+
|
|
12
|
+
# Filter by notification properties via message_id
|
|
13
|
+
if params[:template_id].present? || params[:notification_type].present?
|
|
14
|
+
notification_scope = notification_model.all
|
|
15
|
+
if params[:template_id].present?
|
|
16
|
+
notification_scope = notification_scope.where(template_id: params[:template_id])
|
|
17
|
+
end
|
|
18
|
+
if params[:notification_type].present?
|
|
19
|
+
notification_scope = notification_scope.where(notification_type: params[:notification_type])
|
|
20
|
+
end
|
|
21
|
+
message_ids = notification_scope.pluck(:message_id).compact
|
|
22
|
+
@events = @events.where(message_id: message_ids)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@pagination = paginate(@events)
|
|
26
|
+
@events = @pagination[:records]
|
|
27
|
+
|
|
28
|
+
@event_types = Sincerely::EngagementEvent.distinct.pluck(:event_type).compact
|
|
29
|
+
@templates = Sincerely::Templates::NotificationTemplate.all
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sincerely
|
|
4
|
+
class NotificationsController < ApplicationController
|
|
5
|
+
def index
|
|
6
|
+
@notifications = apply_time_filter(notification_model).order(created_at: :desc)
|
|
7
|
+
|
|
8
|
+
# Apply host app filter (e.g., filter by logged-in user's email)
|
|
9
|
+
@notifications = apply_notification_filter(@notifications)
|
|
10
|
+
|
|
11
|
+
# Apply filters
|
|
12
|
+
@notifications = @notifications.where(delivery_state: params[:status]) if params[:status].present?
|
|
13
|
+
@notifications = @notifications.where(template_id: params[:template_id]) if params[:template_id].present?
|
|
14
|
+
if params[:recipient].present?
|
|
15
|
+
@notifications = @notifications.where('recipient LIKE ?',
|
|
16
|
+
"%#{params[:recipient]}%")
|
|
17
|
+
end
|
|
18
|
+
if params[:notification_type].present?
|
|
19
|
+
@notifications = @notifications.where(notification_type: params[:notification_type])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@notifications = @notifications.where(created_at: Date.parse(params[:date_from])..) if params[:date_from].present?
|
|
23
|
+
if params[:date_to].present?
|
|
24
|
+
@notifications = @notifications.where(created_at: ..Date.parse(params[:date_to]).end_of_day)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@pagination = paginate(@notifications)
|
|
28
|
+
@notifications = @pagination[:records]
|
|
29
|
+
@templates = Sincerely::Templates::NotificationTemplate.all
|
|
30
|
+
@states = notification_model.aasm.states.map(&:name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def show
|
|
34
|
+
@notification = apply_notification_filter(notification_model).find(params[:id])
|
|
35
|
+
@template = @notification.template
|
|
36
|
+
@timeline = build_event_timeline(@notification)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def build_event_timeline(notification)
|
|
42
|
+
events = []
|
|
43
|
+
|
|
44
|
+
Sincerely::DeliveryEvent.where(message_id: notification.message_id).find_each do |event|
|
|
45
|
+
events << {
|
|
46
|
+
type: event.event_type,
|
|
47
|
+
timestamp: event.timestamp || event.created_at,
|
|
48
|
+
details: format_event_details(event.options)
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Sincerely::EngagementEvent.where(message_id: notification.message_id).find_each do |event|
|
|
53
|
+
events << {
|
|
54
|
+
type: event.event_type,
|
|
55
|
+
timestamp: event.timestamp || event.created_at,
|
|
56
|
+
details: format_event_details(event.options)
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
events.sort_by { |e| e[:timestamp] }.reverse
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def format_event_details(data)
|
|
64
|
+
return nil if data.blank?
|
|
65
|
+
|
|
66
|
+
data.is_a?(Hash) ? data.map { |k, v| "#{k}: #{v}" }.join(', ') : data.to_s
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|