rails_onboarding 0.6.1
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/MIT-LICENSE +20 -0
- data/README.md +1351 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
- data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
- data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
- data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
- data/app/assets/javascripts/rails_onboarding/application.js +231 -0
- data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
- data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
- data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
- data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
- data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
- data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
- data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
- data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
- data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
- data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
- data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
- data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
- data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
- data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
- data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
- data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
- data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
- data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
- data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
- data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
- data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
- data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
- data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
- data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
- data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
- data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
- data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
- data/app/controllers/rails_onboarding/application_controller.rb +9 -0
- data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
- data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
- data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
- data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
- data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
- data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
- data/app/helpers/rails_onboarding/application_helper.rb +4 -0
- data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
- data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
- data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
- data/app/jobs/rails_onboarding/application_job.rb +4 -0
- data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
- data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
- data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
- data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
- data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
- data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
- data/app/models/rails_onboarding/analytics.rb +255 -0
- data/app/models/rails_onboarding/analytics_event.rb +168 -0
- data/app/models/rails_onboarding/application_record.rb +5 -0
- data/app/models/rails_onboarding/flow.rb +41 -0
- data/app/services/rails_onboarding/milestone_service.rb +105 -0
- data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
- data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
- data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
- data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
- data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
- data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
- data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
- data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
- data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
- data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
- data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
- data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
- data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
- data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
- data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
- data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
- data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
- data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
- data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
- data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
- data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
- data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
- data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
- data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
- data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
- data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
- data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
- data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
- data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
- data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
- data/app/views/rails_onboarding/templates/index.html.erb +255 -0
- data/config/importmap.rb +33 -0
- data/config/locales/en.yml +62 -0
- data/config/locales/es.yml +62 -0
- data/config/locales/fr.yml +62 -0
- data/config/routes.rb +79 -0
- data/lib/generators/rails_onboarding/install_generator.rb +124 -0
- data/lib/generators/rails_onboarding/templates/README +120 -0
- data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
- data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
- data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
- data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
- data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
- data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
- data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
- data/lib/generators/rails_onboarding/update_generator.rb +73 -0
- data/lib/rails_onboarding/api_mode.rb +367 -0
- data/lib/rails_onboarding/backfill.rb +130 -0
- data/lib/rails_onboarding/background_jobs.rb +412 -0
- data/lib/rails_onboarding/caching.rb +180 -0
- data/lib/rails_onboarding/cdn_support.rb +202 -0
- data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
- data/lib/rails_onboarding/configuration/analytics.rb +29 -0
- data/lib/rails_onboarding/configuration/integrations.rb +31 -0
- data/lib/rails_onboarding/configuration/milestones.rb +92 -0
- data/lib/rails_onboarding/configuration/personalization.rb +25 -0
- data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
- data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
- data/lib/rails_onboarding/configuration/steps.rb +121 -0
- data/lib/rails_onboarding/configuration/templates.rb +85 -0
- data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
- data/lib/rails_onboarding/configuration.rb +133 -0
- data/lib/rails_onboarding/configuration_errors.rb +19 -0
- data/lib/rails_onboarding/configuration_validator.rb +477 -0
- data/lib/rails_onboarding/controller_helpers.rb +153 -0
- data/lib/rails_onboarding/current.rb +13 -0
- data/lib/rails_onboarding/deprecation.rb +64 -0
- data/lib/rails_onboarding/devise_integration.rb +122 -0
- data/lib/rails_onboarding/engine.rb +245 -0
- data/lib/rails_onboarding/error_recovery.rb +173 -0
- data/lib/rails_onboarding/lazy_loading.rb +155 -0
- data/lib/rails_onboarding/multi_tenant.rb +306 -0
- data/lib/rails_onboarding/requirements_validator.rb +315 -0
- data/lib/rails_onboarding/responsive_helper.rb +218 -0
- data/lib/rails_onboarding/session_manager.rb +187 -0
- data/lib/rails_onboarding/skip_logic.rb +159 -0
- data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
- data/lib/rails_onboarding/version.rb +3 -0
- data/lib/rails_onboarding.rb +44 -0
- data/lib/tasks/rails_onboarding_analytics.rake +136 -0
- data/lib/tasks/rails_onboarding_tasks.rake +162 -0
- metadata +303 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Background job support for queuing emails and notifications
|
|
5
|
+
# Compatible with ActiveJob, Sidekiq, Resque, and DelayedJob
|
|
6
|
+
module BackgroundJobs
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
# Throttling constants to prevent queue overflow
|
|
10
|
+
MAX_JOBS_PER_USER_PER_HOUR = 100
|
|
11
|
+
MAX_TOTAL_JOBS_PER_MINUTE = 1000
|
|
12
|
+
|
|
13
|
+
module ClassMethods
|
|
14
|
+
# Configure background job options
|
|
15
|
+
def configure_background_jobs(options = {})
|
|
16
|
+
@background_job_options = {
|
|
17
|
+
adapter: options.fetch(:adapter, :active_job),
|
|
18
|
+
queue: options.fetch(:queue, :default),
|
|
19
|
+
enable_emails: options.fetch(:enable_emails, true),
|
|
20
|
+
enable_notifications: options.fetch(:enable_notifications, true),
|
|
21
|
+
retry_limit: options.fetch(:retry_limit, 3),
|
|
22
|
+
retry_delay: options.fetch(:retry_delay, 5.minutes),
|
|
23
|
+
enable_throttling: options.fetch(:enable_throttling, true),
|
|
24
|
+
max_jobs_per_user_per_hour: options.fetch(:max_jobs_per_user_per_hour, MAX_JOBS_PER_USER_PER_HOUR),
|
|
25
|
+
max_total_jobs_per_minute: options.fetch(:max_total_jobs_per_minute, MAX_TOTAL_JOBS_PER_MINUTE)
|
|
26
|
+
}.merge(options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def background_job_options
|
|
30
|
+
@background_job_options || {}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Queue onboarding emails
|
|
35
|
+
def queue_onboarding_welcome_email(user)
|
|
36
|
+
return unless RailsOnboarding.active_job_available?
|
|
37
|
+
return unless background_jobs_enabled?(:emails)
|
|
38
|
+
return unless can_queue_job_for_user?(user)
|
|
39
|
+
|
|
40
|
+
OnboardingMailerJob.perform_later(user.id, :welcome)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def queue_onboarding_reminder_email(user)
|
|
44
|
+
return unless RailsOnboarding.active_job_available?
|
|
45
|
+
return unless background_jobs_enabled?(:emails)
|
|
46
|
+
return unless can_queue_job_for_user?(user)
|
|
47
|
+
|
|
48
|
+
OnboardingMailerJob.set(wait: 1.day).perform_later(user.id, :reminder)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def queue_onboarding_completion_email(user)
|
|
52
|
+
return unless RailsOnboarding.active_job_available?
|
|
53
|
+
return unless background_jobs_enabled?(:emails)
|
|
54
|
+
return unless can_queue_job_for_user?(user)
|
|
55
|
+
|
|
56
|
+
OnboardingMailerJob.perform_later(user.id, :completion)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Queue notifications
|
|
60
|
+
def queue_onboarding_notification(user, notification_type, data = {})
|
|
61
|
+
return unless RailsOnboarding.active_job_available?
|
|
62
|
+
return unless background_jobs_enabled?(:notifications)
|
|
63
|
+
return unless can_queue_job_for_user?(user)
|
|
64
|
+
|
|
65
|
+
OnboardingNotificationJob.perform_later(user.id, notification_type, data)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Queue analytics events
|
|
69
|
+
def queue_analytics_event(event_name, user, data = {})
|
|
70
|
+
return unless RailsOnboarding.active_job_available?
|
|
71
|
+
return unless can_queue_job_for_user?(user)
|
|
72
|
+
|
|
73
|
+
OnboardingAnalyticsJob.perform_later(event_name, user.id, data)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Queue milestone achievements
|
|
77
|
+
def queue_milestone_achievement(user, milestone_id)
|
|
78
|
+
return unless RailsOnboarding.active_job_available?
|
|
79
|
+
return unless can_queue_job_for_user?(user)
|
|
80
|
+
|
|
81
|
+
MilestoneAchievementJob.perform_later(user.id, milestone_id)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def background_jobs_enabled?(type = nil)
|
|
87
|
+
options = self.class.background_job_options
|
|
88
|
+
return false if options.empty?
|
|
89
|
+
|
|
90
|
+
case type
|
|
91
|
+
when :emails
|
|
92
|
+
options[:enable_emails]
|
|
93
|
+
when :notifications
|
|
94
|
+
options[:enable_notifications]
|
|
95
|
+
else
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Check if we can queue a job for this user (throttling)
|
|
101
|
+
def can_queue_job_for_user?(user)
|
|
102
|
+
options = self.class.background_job_options
|
|
103
|
+
return true unless options[:enable_throttling]
|
|
104
|
+
|
|
105
|
+
# Check per-user rate limit
|
|
106
|
+
user_job_count = get_user_job_count(user.id)
|
|
107
|
+
if user_job_count >= options[:max_jobs_per_user_per_hour]
|
|
108
|
+
Rails.logger.warn "RailsOnboarding: Job throttled for user #{user.id} (#{user_job_count} jobs/hour)"
|
|
109
|
+
return false
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Check global rate limit
|
|
113
|
+
total_job_count = get_total_job_count
|
|
114
|
+
if total_job_count >= options[:max_total_jobs_per_minute]
|
|
115
|
+
Rails.logger.warn "RailsOnboarding: Job throttled globally (#{total_job_count} jobs/minute)"
|
|
116
|
+
return false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Increment counters
|
|
120
|
+
increment_user_job_count(user.id)
|
|
121
|
+
increment_total_job_count
|
|
122
|
+
|
|
123
|
+
true
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Get job count for user in the last hour
|
|
127
|
+
def get_user_job_count(user_id)
|
|
128
|
+
cache_key = "rails_onboarding:job_count:user:#{user_id}"
|
|
129
|
+
Rails.cache.read(cache_key) || 0
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Increment job count for user
|
|
133
|
+
def increment_user_job_count(user_id)
|
|
134
|
+
cache_key = "rails_onboarding:job_count:user:#{user_id}"
|
|
135
|
+
count = Rails.cache.increment(cache_key, 1, expires_in: 1.hour)
|
|
136
|
+
count || Rails.cache.write(cache_key, 1, expires_in: 1.hour)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get total job count in the last minute
|
|
140
|
+
def get_total_job_count
|
|
141
|
+
cache_key = "rails_onboarding:job_count:total:#{Time.current.strftime('%Y%m%d%H%M')}"
|
|
142
|
+
Rails.cache.read(cache_key) || 0
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Increment total job count
|
|
146
|
+
def increment_total_job_count
|
|
147
|
+
cache_key = "rails_onboarding:job_count:total:#{Time.current.strftime('%Y%m%d%H%M')}"
|
|
148
|
+
count = Rails.cache.increment(cache_key, 1, expires_in: 1.minute)
|
|
149
|
+
count || Rails.cache.write(cache_key, 1, expires_in: 1.minute)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Check if ActiveJob is available
|
|
154
|
+
def self.active_job_available?
|
|
155
|
+
defined?(::ActiveJob::Base)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check if ActionMailer is available
|
|
159
|
+
def self.action_mailer_available?
|
|
160
|
+
defined?(::ActionMailer::Base)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if ActionMailer is properly configured
|
|
164
|
+
def self.action_mailer_configured?
|
|
165
|
+
return false unless action_mailer_available?
|
|
166
|
+
|
|
167
|
+
# Check if delivery method is configured (not :test in production)
|
|
168
|
+
if Rails.env.production?
|
|
169
|
+
return false if ::ActionMailer::Base.delivery_method == :test
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Check if SMTP settings are present when using SMTP
|
|
173
|
+
if ::ActionMailer::Base.delivery_method == :smtp
|
|
174
|
+
smtp_settings = ::ActionMailer::Base.smtp_settings
|
|
175
|
+
return false if smtp_settings.nil? || smtp_settings.empty?
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
true
|
|
179
|
+
rescue => e
|
|
180
|
+
Rails.logger.warn "RailsOnboarding: Error checking ActionMailer configuration: #{e.message}"
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Base job class for RailsOnboarding (only defined if ActiveJob is available)
|
|
185
|
+
if active_job_available?
|
|
186
|
+
class ApplicationJob < ::ActiveJob::Base
|
|
187
|
+
# Automatically retry jobs that encountered a deadlock
|
|
188
|
+
retry_on ActiveRecord::Deadlocked
|
|
189
|
+
|
|
190
|
+
# Most jobs are safe to ignore if the underlying records are no longer available
|
|
191
|
+
discard_on ::ActiveJob::DeserializationError
|
|
192
|
+
|
|
193
|
+
queue_as :default
|
|
194
|
+
|
|
195
|
+
def self.queue_name
|
|
196
|
+
RailsOnboarding::BackgroundJobs.background_job_options[:queue] || :default
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Job for sending onboarding emails (only defined if ActiveJob is available)
|
|
202
|
+
if active_job_available?
|
|
203
|
+
class OnboardingMailerJob < ApplicationJob
|
|
204
|
+
queue_as { RailsOnboarding::BackgroundJobs.background_job_options[:queue] || :default }
|
|
205
|
+
|
|
206
|
+
def perform(user_id, email_type)
|
|
207
|
+
unless RailsOnboarding.action_mailer_configured?
|
|
208
|
+
Rails.logger.warn "RailsOnboarding: ActionMailer not configured, skipping email"
|
|
209
|
+
return
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
user = find_user(user_id)
|
|
213
|
+
return unless user
|
|
214
|
+
|
|
215
|
+
case email_type.to_sym
|
|
216
|
+
when :welcome
|
|
217
|
+
OnboardingMailer.welcome_email(user).deliver_now
|
|
218
|
+
when :reminder
|
|
219
|
+
# Only send reminder if onboarding is not completed
|
|
220
|
+
OnboardingMailer.reminder_email(user).deliver_now unless user.onboarding_completed?
|
|
221
|
+
when :completion
|
|
222
|
+
OnboardingMailer.completion_email(user).deliver_now
|
|
223
|
+
when :step_completed
|
|
224
|
+
OnboardingMailer.step_completed_email(user).deliver_now
|
|
225
|
+
else
|
|
226
|
+
Rails.logger.warn "Unknown email type: #{email_type}"
|
|
227
|
+
end
|
|
228
|
+
rescue StandardError => e
|
|
229
|
+
Rails.logger.error "Failed to send onboarding email: #{e.message}"
|
|
230
|
+
raise e if should_retry?
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def find_user(user_id)
|
|
236
|
+
user_class = RailsOnboarding.configuration.user_class_name.constantize
|
|
237
|
+
user_class.find_by(id: user_id)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def should_retry?
|
|
241
|
+
executions < (RailsOnboarding::BackgroundJobs.background_job_options[:retry_limit] || 3)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Job for sending onboarding notifications (only defined if ActiveJob is available)
|
|
247
|
+
if active_job_available?
|
|
248
|
+
class OnboardingNotificationJob < ApplicationJob
|
|
249
|
+
queue_as { RailsOnboarding::BackgroundJobs.background_job_options[:queue] || :default }
|
|
250
|
+
|
|
251
|
+
def perform(user_id, notification_type, data = {})
|
|
252
|
+
user = find_user(user_id)
|
|
253
|
+
return unless user
|
|
254
|
+
|
|
255
|
+
# Create notification record if notification system exists
|
|
256
|
+
if defined?(Noticed) && user.respond_to?(:notifications)
|
|
257
|
+
# Using Noticed gem
|
|
258
|
+
create_noticed_notification(user, notification_type, data)
|
|
259
|
+
elsif user.respond_to?(:notify)
|
|
260
|
+
# Custom notification system
|
|
261
|
+
user.notify(notification_type, data)
|
|
262
|
+
else
|
|
263
|
+
# Fallback: log notification
|
|
264
|
+
Rails.logger.info "Onboarding notification for user #{user_id}: #{notification_type}"
|
|
265
|
+
end
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
Rails.logger.error "Failed to send notification: #{e.message}"
|
|
268
|
+
raise e if should_retry?
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
private
|
|
272
|
+
|
|
273
|
+
def find_user(user_id)
|
|
274
|
+
user_class = RailsOnboarding.configuration.user_class_name.constantize
|
|
275
|
+
user_class.find_by(id: user_id)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def create_noticed_notification(user, notification_type, data)
|
|
279
|
+
notification_class = "RailsOnboarding::#{notification_type.to_s.camelize}Notification"
|
|
280
|
+
|
|
281
|
+
if Object.const_defined?(notification_class)
|
|
282
|
+
notification_class.constantize.with(data).deliver(user)
|
|
283
|
+
else
|
|
284
|
+
Rails.logger.warn "Notification class not found: #{notification_class}"
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def should_retry?
|
|
289
|
+
executions < (RailsOnboarding::BackgroundJobs.background_job_options[:retry_limit] || 3)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Job for tracking analytics events (only defined if ActiveJob is available)
|
|
295
|
+
if active_job_available?
|
|
296
|
+
class OnboardingAnalyticsJob < ApplicationJob
|
|
297
|
+
queue_as { RailsOnboarding::BackgroundJobs.background_job_options[:queue] || :default }
|
|
298
|
+
|
|
299
|
+
def perform(event_name, user_id, data = {})
|
|
300
|
+
user = find_user(user_id)
|
|
301
|
+
return unless user
|
|
302
|
+
|
|
303
|
+
# Create analytics event
|
|
304
|
+
if defined?(RailsOnboarding::AnalyticsEvent)
|
|
305
|
+
RailsOnboarding::AnalyticsEvent.create!(
|
|
306
|
+
event_name: event_name,
|
|
307
|
+
user_id: user_id,
|
|
308
|
+
event_data: data,
|
|
309
|
+
occurred_at: Time.current
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Send to external analytics services
|
|
314
|
+
track_external_analytics(event_name, user, data)
|
|
315
|
+
rescue StandardError => e
|
|
316
|
+
Rails.logger.error "Failed to track analytics event: #{e.message}"
|
|
317
|
+
# Don't retry analytics events - they're not critical
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def find_user(user_id)
|
|
323
|
+
user_class = RailsOnboarding.configuration.user_class_name.constantize
|
|
324
|
+
user_class.find_by(id: user_id)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def track_external_analytics(event_name, user, data)
|
|
328
|
+
# Integrate with external analytics services
|
|
329
|
+
# Segment
|
|
330
|
+
if defined?(Analytics)
|
|
331
|
+
Analytics.track(
|
|
332
|
+
user_id: user.id,
|
|
333
|
+
event: "Onboarding: #{event_name}",
|
|
334
|
+
properties: data
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Mixpanel
|
|
339
|
+
if defined?(Mixpanel)
|
|
340
|
+
Mixpanel.track(user.id, "Onboarding: #{event_name}", data)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Google Analytics 4
|
|
344
|
+
if defined?(Gabba)
|
|
345
|
+
# Track with GA4
|
|
346
|
+
end
|
|
347
|
+
rescue StandardError => e
|
|
348
|
+
Rails.logger.warn "Failed to track to external analytics: #{e.message}"
|
|
349
|
+
# Don't raise - external analytics failures shouldn't break the job
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Job for processing milestone achievements (only defined if ActiveJob is available)
|
|
355
|
+
if active_job_available?
|
|
356
|
+
class MilestoneAchievementJob < ApplicationJob
|
|
357
|
+
queue_as { RailsOnboarding::BackgroundJobs.background_job_options[:queue] || :default }
|
|
358
|
+
|
|
359
|
+
def perform(user_id, milestone_id)
|
|
360
|
+
user = find_user(user_id)
|
|
361
|
+
return unless user
|
|
362
|
+
|
|
363
|
+
milestone = find_milestone(milestone_id)
|
|
364
|
+
return unless milestone
|
|
365
|
+
|
|
366
|
+
# Award milestone
|
|
367
|
+
if user.respond_to?(:award_milestone)
|
|
368
|
+
user.award_milestone(milestone)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Send celebration notification
|
|
372
|
+
if defined?(RailsOnboarding::OnboardingNotificationJob)
|
|
373
|
+
RailsOnboarding::OnboardingNotificationJob.perform_later(
|
|
374
|
+
user_id,
|
|
375
|
+
:milestone_achieved,
|
|
376
|
+
{ milestone_id: milestone_id, title: milestone[:title] }
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Track analytics
|
|
381
|
+
if defined?(RailsOnboarding::OnboardingAnalyticsJob)
|
|
382
|
+
RailsOnboarding::OnboardingAnalyticsJob.perform_later(
|
|
383
|
+
"milestone_achieved",
|
|
384
|
+
user_id,
|
|
385
|
+
{ milestone_id: milestone_id }
|
|
386
|
+
)
|
|
387
|
+
end
|
|
388
|
+
rescue StandardError => e
|
|
389
|
+
Rails.logger.error "Failed to process milestone achievement: #{e.message}"
|
|
390
|
+
raise e if should_retry?
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
private
|
|
394
|
+
|
|
395
|
+
def find_user(user_id)
|
|
396
|
+
user_class = RailsOnboarding.configuration.user_class_name.constantize
|
|
397
|
+
user_class.find_by(id: user_id)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def find_milestone(milestone_id)
|
|
401
|
+
RailsOnboarding.configuration.milestones.find { |m| m[:id] == milestone_id }
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def should_retry?
|
|
405
|
+
executions < (RailsOnboarding::BackgroundJobs.background_job_options[:retry_limit] || 3)
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# Mailer is now defined in app/mailers/rails_onboarding/onboarding_mailer.rb
|
|
411
|
+
# and will be autoloaded by Rails when needed
|
|
412
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsOnboarding
|
|
4
|
+
# Caching module for improving performance of onboarding operations
|
|
5
|
+
#
|
|
6
|
+
# This module provides caching capabilities for:
|
|
7
|
+
# - Configuration data
|
|
8
|
+
# - User onboarding state
|
|
9
|
+
# - Step information
|
|
10
|
+
# - Milestone data
|
|
11
|
+
#
|
|
12
|
+
# By default, uses Rails.cache with configurable TTL values
|
|
13
|
+
module Caching
|
|
14
|
+
extend ActiveSupport::Concern
|
|
15
|
+
|
|
16
|
+
class_methods do
|
|
17
|
+
# Cache configuration for a specific user class
|
|
18
|
+
#
|
|
19
|
+
# @param config_key [String] The configuration key to cache
|
|
20
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 1 hour)
|
|
21
|
+
# @return [Object] The cached configuration value
|
|
22
|
+
def cached_config(config_key, ttl: 3600)
|
|
23
|
+
cache_key = "rails_onboarding:config:#{config_key}"
|
|
24
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
25
|
+
RailsOnboarding.configuration.send(config_key)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Clear all configuration caches
|
|
30
|
+
#
|
|
31
|
+
# Call this when configuration changes
|
|
32
|
+
def clear_config_cache
|
|
33
|
+
Rails.cache.delete_matched("rails_onboarding:config:*")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Cache steps configuration
|
|
37
|
+
#
|
|
38
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 1 hour)
|
|
39
|
+
# @return [Array<Hash>] The cached steps
|
|
40
|
+
def cached_steps(ttl: 3600)
|
|
41
|
+
cached_config(:steps, ttl: ttl)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Cache milestones configuration
|
|
45
|
+
#
|
|
46
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 1 hour)
|
|
47
|
+
# @return [Array<Hash>] The cached milestones
|
|
48
|
+
def cached_milestones(ttl: 3600)
|
|
49
|
+
cached_config(:milestones, ttl: ttl)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Cache feature tooltips configuration
|
|
53
|
+
#
|
|
54
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 1 hour)
|
|
55
|
+
# @return [Hash] The cached feature tooltips
|
|
56
|
+
def cached_feature_tooltips(ttl: 3600)
|
|
57
|
+
cached_config(:feature_tooltips, ttl: ttl)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
included do
|
|
62
|
+
# Cache user's onboarding state
|
|
63
|
+
after_save :clear_onboarding_cache, if: :onboarding_attributes_changed?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get cached onboarding progress
|
|
67
|
+
#
|
|
68
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 5 minutes)
|
|
69
|
+
# @return [Integer] The cached progress percentage
|
|
70
|
+
def cached_onboarding_progress(ttl: 300)
|
|
71
|
+
return onboarding_progress if has_dirty_onboarding_attributes?
|
|
72
|
+
|
|
73
|
+
cache_key = "rails_onboarding:user:#{id}:progress"
|
|
74
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
75
|
+
onboarding_progress
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Get cached current onboarding step
|
|
80
|
+
#
|
|
81
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 5 minutes)
|
|
82
|
+
# @return [Hash, nil] The cached current step
|
|
83
|
+
def cached_current_onboarding_step(ttl: 300)
|
|
84
|
+
return current_onboarding_step if has_dirty_onboarding_attributes?
|
|
85
|
+
|
|
86
|
+
cache_key = "rails_onboarding:user:#{id}:current_step"
|
|
87
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
88
|
+
current_onboarding_step
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get cached milestone achievements
|
|
93
|
+
#
|
|
94
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 10 minutes)
|
|
95
|
+
# @return [Array<String>] The cached achieved milestones
|
|
96
|
+
def cached_achieved_milestones(ttl: 600)
|
|
97
|
+
return achieved_milestones if has_dirty_milestone_attributes?
|
|
98
|
+
|
|
99
|
+
cache_key = "rails_onboarding:user:#{id}:milestones"
|
|
100
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
101
|
+
achieved_milestones
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Get cached available tooltips
|
|
106
|
+
#
|
|
107
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 10 minutes)
|
|
108
|
+
# @return [Hash] Available tooltips for this user
|
|
109
|
+
def cached_available_tooltips(ttl: 600)
|
|
110
|
+
return {} unless RailsOnboarding.configuration.enable_tooltips
|
|
111
|
+
|
|
112
|
+
cache_key = "rails_onboarding:user:#{id}:available_tooltips"
|
|
113
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
114
|
+
tooltips = {}
|
|
115
|
+
RailsOnboarding.configuration.feature_tooltips.each do |feature, config|
|
|
116
|
+
tooltips[feature] = config if show_feature_tooltip?(feature)
|
|
117
|
+
end
|
|
118
|
+
tooltips
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Clear all caches for this user
|
|
123
|
+
def clear_onboarding_cache
|
|
124
|
+
Rails.cache.delete("rails_onboarding:user:#{id}:progress")
|
|
125
|
+
Rails.cache.delete("rails_onboarding:user:#{id}:current_step")
|
|
126
|
+
Rails.cache.delete("rails_onboarding:user:#{id}:milestones")
|
|
127
|
+
Rails.cache.delete("rails_onboarding:user:#{id}:available_tooltips")
|
|
128
|
+
Rails.cache.delete("rails_onboarding:user:#{id}:needs_onboarding")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Check if user needs onboarding (cached)
|
|
132
|
+
#
|
|
133
|
+
# @param ttl [Integer] Time-to-live in seconds (default: 1 minute)
|
|
134
|
+
# @return [Boolean] Whether user needs onboarding
|
|
135
|
+
def cached_needs_onboarding?(ttl: 60)
|
|
136
|
+
return needs_onboarding? if has_dirty_onboarding_attributes?
|
|
137
|
+
|
|
138
|
+
cache_key = "rails_onboarding:user:#{id}:needs_onboarding"
|
|
139
|
+
Rails.cache.fetch(cache_key, expires_in: ttl) do
|
|
140
|
+
needs_onboarding?
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
# Check if onboarding attributes were just saved (for after_save callback)
|
|
147
|
+
# Uses previous_changes which contains the changes from the last save
|
|
148
|
+
def onboarding_attributes_changed?
|
|
149
|
+
return false unless persisted?
|
|
150
|
+
|
|
151
|
+
previous_changes.keys.any? do |attr|
|
|
152
|
+
attr.start_with?("onboarding_")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Check if onboarding attributes have unsaved changes (for caching bypass)
|
|
157
|
+
# Uses changes which contains dirty/unsaved attributes
|
|
158
|
+
def has_dirty_onboarding_attributes?
|
|
159
|
+
changes.keys.any? do |attr|
|
|
160
|
+
attr.start_with?("onboarding_")
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Check if milestone attributes were just saved (for after_save callback)
|
|
165
|
+
def milestone_attributes_changed?
|
|
166
|
+
return false unless persisted?
|
|
167
|
+
|
|
168
|
+
previous_changes.key?("milestones_achieved") ||
|
|
169
|
+
previous_changes.key?("milestone_points") ||
|
|
170
|
+
previous_changes.key?("last_milestone_at")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Check if milestone attributes have unsaved changes (for caching bypass)
|
|
174
|
+
def has_dirty_milestone_attributes?
|
|
175
|
+
changes.key?("milestones_achieved") ||
|
|
176
|
+
changes.key?("milestone_points") ||
|
|
177
|
+
changes.key?("last_milestone_at")
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|