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.
Files changed (148) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1351 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/rails_onboarding/admin/chart_controller.js +25 -0
  6. data/app/assets/javascripts/rails_onboarding/admin/filter_controller.js +49 -0
  7. data/app/assets/javascripts/rails_onboarding/admin/flash_controller.js +21 -0
  8. data/app/assets/javascripts/rails_onboarding/admin/flow_editor_controller.js +86 -0
  9. data/app/assets/javascripts/rails_onboarding/application.js +231 -0
  10. data/app/assets/javascripts/rails_onboarding/milestone_celebration_controller.js +177 -0
  11. data/app/assets/javascripts/rails_onboarding/milestone_dashboard_controller.js +132 -0
  12. data/app/assets/javascripts/rails_onboarding/milestone_detail_controller.js +34 -0
  13. data/app/assets/javascripts/rails_onboarding/navigation_controller.js +277 -0
  14. data/app/assets/javascripts/rails_onboarding/onboarding_controller.js +326 -0
  15. data/app/assets/javascripts/rails_onboarding/progress_controller.js +203 -0
  16. data/app/assets/javascripts/rails_onboarding/progressive_disclosure_controller.js +218 -0
  17. data/app/assets/javascripts/rails_onboarding/tooltip_controller.js +1237 -0
  18. data/app/assets/javascripts/rails_onboarding/tooltip_scheduler_controller.js +511 -0
  19. data/app/assets/javascripts/rails_onboarding/tour_controller.js +763 -0
  20. data/app/assets/stylesheets/rails_onboarding/accessibility.css +536 -0
  21. data/app/assets/stylesheets/rails_onboarding/admin.css +816 -0
  22. data/app/assets/stylesheets/rails_onboarding/application.css +1145 -0
  23. data/app/assets/stylesheets/rails_onboarding/flash_messages.css +237 -0
  24. data/app/assets/stylesheets/rails_onboarding/milestones.css +545 -0
  25. data/app/assets/stylesheets/rails_onboarding/mobile.css +856 -0
  26. data/app/assets/stylesheets/rails_onboarding/progressive_disclosure.css +299 -0
  27. data/app/assets/stylesheets/rails_onboarding/tooltips.css +468 -0
  28. data/app/assets/stylesheets/rails_onboarding/tour.css +495 -0
  29. data/app/assets/stylesheets/rails_onboarding/utilities.css +393 -0
  30. data/app/controllers/concerns/rails_onboarding/admin_authorization.rb +98 -0
  31. data/app/controllers/concerns/rails_onboarding/rate_limitable.rb +104 -0
  32. data/app/controllers/rails_onboarding/ab_tests_controller.rb +257 -0
  33. data/app/controllers/rails_onboarding/admin/ab_tests_controller.rb +121 -0
  34. data/app/controllers/rails_onboarding/admin/base_controller.rb +68 -0
  35. data/app/controllers/rails_onboarding/admin/dashboard_controller.rb +189 -0
  36. data/app/controllers/rails_onboarding/admin/flows_controller.rb +175 -0
  37. data/app/controllers/rails_onboarding/admin/users_controller.rb +366 -0
  38. data/app/controllers/rails_onboarding/application_controller.rb +9 -0
  39. data/app/controllers/rails_onboarding/milestones_controller.rb +126 -0
  40. data/app/controllers/rails_onboarding/onboarding_controller.rb +409 -0
  41. data/app/controllers/rails_onboarding/progressive_features_controller.rb +134 -0
  42. data/app/controllers/rails_onboarding/templates_controller.rb +162 -0
  43. data/app/controllers/rails_onboarding/test_sessions_controller.rb +16 -0
  44. data/app/controllers/rails_onboarding/tooltips_controller.rb +92 -0
  45. data/app/helpers/rails_onboarding/application_helper.rb +4 -0
  46. data/app/helpers/rails_onboarding/i18n_helper.rb +137 -0
  47. data/app/helpers/rails_onboarding/personalization_helper.rb +184 -0
  48. data/app/helpers/rails_onboarding/templates_helper.rb +253 -0
  49. data/app/jobs/rails_onboarding/application_job.rb +4 -0
  50. data/app/mailers/rails_onboarding/application_mailer.rb +6 -0
  51. data/app/mailers/rails_onboarding/onboarding_mailer.rb +72 -0
  52. data/app/models/concerns/rails_onboarding/ab_testable.rb +149 -0
  53. data/app/models/concerns/rails_onboarding/onboardable.rb +649 -0
  54. data/app/models/concerns/rails_onboarding/personalizable.rb +211 -0
  55. data/app/models/concerns/rails_onboarding/progressive_disclosure.rb +276 -0
  56. data/app/models/rails_onboarding/analytics.rb +255 -0
  57. data/app/models/rails_onboarding/analytics_event.rb +168 -0
  58. data/app/models/rails_onboarding/application_record.rb +5 -0
  59. data/app/models/rails_onboarding/flow.rb +41 -0
  60. data/app/services/rails_onboarding/milestone_service.rb +105 -0
  61. data/app/views/layouts/rails_onboarding/admin.html.erb +106 -0
  62. data/app/views/layouts/rails_onboarding/application.html.erb +65 -0
  63. data/app/views/rails_onboarding/ab_tests/index.html.erb +209 -0
  64. data/app/views/rails_onboarding/ab_tests/results.html.erb +253 -0
  65. data/app/views/rails_onboarding/admin/ab_tests/_ab_test_card.html.erb +24 -0
  66. data/app/views/rails_onboarding/admin/ab_tests/index.html.erb +57 -0
  67. data/app/views/rails_onboarding/admin/ab_tests/show.html.erb +41 -0
  68. data/app/views/rails_onboarding/admin/dashboard/index.html.erb +224 -0
  69. data/app/views/rails_onboarding/admin/flows/_form.html.erb +147 -0
  70. data/app/views/rails_onboarding/admin/flows/edit.html.erb +7 -0
  71. data/app/views/rails_onboarding/admin/flows/index.html.erb +125 -0
  72. data/app/views/rails_onboarding/admin/flows/new.html.erb +7 -0
  73. data/app/views/rails_onboarding/admin/flows/preview.html.erb +40 -0
  74. data/app/views/rails_onboarding/admin/flows/show.html.erb +114 -0
  75. data/app/views/rails_onboarding/admin/users/index.html.erb +180 -0
  76. data/app/views/rails_onboarding/admin/users/show.html.erb +187 -0
  77. data/app/views/rails_onboarding/milestones/index.html.erb +64 -0
  78. data/app/views/rails_onboarding/milestones/show.html.erb +57 -0
  79. data/app/views/rails_onboarding/onboarding/_custom_footer.html.erb +2 -0
  80. data/app/views/rails_onboarding/onboarding/_progress_indicator.html.erb +56 -0
  81. data/app/views/rails_onboarding/onboarding/_step_navigation.html.erb +44 -0
  82. data/app/views/rails_onboarding/onboarding/step.html.erb +38 -0
  83. data/app/views/rails_onboarding/onboarding/welcome.html.erb +37 -0
  84. data/app/views/rails_onboarding/onboarding_mailer/completion_email.html.erb +109 -0
  85. data/app/views/rails_onboarding/onboarding_mailer/completion_email.text.erb +29 -0
  86. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.html.erb +99 -0
  87. data/app/views/rails_onboarding/onboarding_mailer/reminder_email.text.erb +20 -0
  88. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.html.erb +105 -0
  89. data/app/views/rails_onboarding/onboarding_mailer/step_completed_email.text.erb +19 -0
  90. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.html.erb +74 -0
  91. data/app/views/rails_onboarding/onboarding_mailer/welcome_email.text.erb +17 -0
  92. data/app/views/rails_onboarding/shared/_flash.html.erb +25 -0
  93. data/app/views/rails_onboarding/shared/_milestone_badge.html.erb +22 -0
  94. data/app/views/rails_onboarding/shared/_milestone_celebration.html.erb +44 -0
  95. data/app/views/rails_onboarding/shared/_onboarding_banner.html.erb +48 -0
  96. data/app/views/rails_onboarding/templates/index.html.erb +255 -0
  97. data/config/importmap.rb +33 -0
  98. data/config/locales/en.yml +62 -0
  99. data/config/locales/es.yml +62 -0
  100. data/config/locales/fr.yml +62 -0
  101. data/config/routes.rb +79 -0
  102. data/lib/generators/rails_onboarding/install_generator.rb +124 -0
  103. data/lib/generators/rails_onboarding/templates/README +120 -0
  104. data/lib/generators/rails_onboarding/templates/add_analytics_to_rails_onboarding.rb +33 -0
  105. data/lib/generators/rails_onboarding/templates/add_milestone_tracking_to_users.rb +38 -0
  106. data/lib/generators/rails_onboarding/templates/add_onboarding_indexes.rb +93 -0
  107. data/lib/generators/rails_onboarding/templates/add_onboarding_to_users.rb +51 -0
  108. data/lib/generators/rails_onboarding/templates/add_robustness_fields_to_users.rb.tt +20 -0
  109. data/lib/generators/rails_onboarding/templates/create_rails_onboarding_flows.rb +18 -0
  110. data/lib/generators/rails_onboarding/templates/onboarding.css +18 -0
  111. data/lib/generators/rails_onboarding/templates/rails_onboarding.rb +75 -0
  112. data/lib/generators/rails_onboarding/update_generator.rb +73 -0
  113. data/lib/rails_onboarding/api_mode.rb +367 -0
  114. data/lib/rails_onboarding/backfill.rb +130 -0
  115. data/lib/rails_onboarding/background_jobs.rb +412 -0
  116. data/lib/rails_onboarding/caching.rb +180 -0
  117. data/lib/rails_onboarding/cdn_support.rb +202 -0
  118. data/lib/rails_onboarding/configuration/ab_testing.rb +24 -0
  119. data/lib/rails_onboarding/configuration/analytics.rb +29 -0
  120. data/lib/rails_onboarding/configuration/integrations.rb +31 -0
  121. data/lib/rails_onboarding/configuration/milestones.rb +92 -0
  122. data/lib/rails_onboarding/configuration/personalization.rb +25 -0
  123. data/lib/rails_onboarding/configuration/progressive_disclosure.rb +41 -0
  124. data/lib/rails_onboarding/configuration/rate_limiting.rb +18 -0
  125. data/lib/rails_onboarding/configuration/steps.rb +121 -0
  126. data/lib/rails_onboarding/configuration/templates.rb +85 -0
  127. data/lib/rails_onboarding/configuration/tooltips.rb +32 -0
  128. data/lib/rails_onboarding/configuration.rb +133 -0
  129. data/lib/rails_onboarding/configuration_errors.rb +19 -0
  130. data/lib/rails_onboarding/configuration_validator.rb +477 -0
  131. data/lib/rails_onboarding/controller_helpers.rb +153 -0
  132. data/lib/rails_onboarding/current.rb +13 -0
  133. data/lib/rails_onboarding/deprecation.rb +64 -0
  134. data/lib/rails_onboarding/devise_integration.rb +122 -0
  135. data/lib/rails_onboarding/engine.rb +245 -0
  136. data/lib/rails_onboarding/error_recovery.rb +173 -0
  137. data/lib/rails_onboarding/lazy_loading.rb +155 -0
  138. data/lib/rails_onboarding/multi_tenant.rb +306 -0
  139. data/lib/rails_onboarding/requirements_validator.rb +315 -0
  140. data/lib/rails_onboarding/responsive_helper.rb +218 -0
  141. data/lib/rails_onboarding/session_manager.rb +187 -0
  142. data/lib/rails_onboarding/skip_logic.rb +159 -0
  143. data/lib/rails_onboarding/turbo_compatibility.rb +229 -0
  144. data/lib/rails_onboarding/version.rb +3 -0
  145. data/lib/rails_onboarding.rb +44 -0
  146. data/lib/tasks/rails_onboarding_analytics.rake +136 -0
  147. data/lib/tasks/rails_onboarding_tasks.rake +162 -0
  148. 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