Class: RailsOnboarding::ErrorRecovery

Inherits:
Object
  • Object
show all
Defined in:
lib/rails_onboarding/error_recovery.rb

Overview

Error Recovery Service Handles failed steps gracefully with retry logic and error state management

Constant Summary collapse

MAX_RETRIES =
3
RETRY_DELAY =
2.seconds

Class Method Summary collapse

Class Method Details

.clear_error_state(user, action) ⇒ Object

Clear error state for a successful action



92
93
94
95
96
97
98
99
100
# File 'lib/rails_onboarding/error_recovery.rb', line 92

def clear_error_state(user, action)
  return unless user

  if user.respond_to?(:onboarding_failed_actions)
    user.onboarding_failed_actions ||= []
    user.onboarding_failed_actions.reject! { |a| a[:action] == action.to_s }
    user.save(validate: false) rescue nil
  end
end

.failed_actions(user) ⇒ Object

Get failed actions for a user



109
110
111
112
# File 'lib/rails_onboarding/error_recovery.rb', line 109

def failed_actions(user)
  return [] unless user&.respond_to?(:onboarding_failed_actions)
  user.onboarding_failed_actions || []
end

.has_errors?(user) ⇒ Boolean

Check if user has any failed actions

Returns:

  • (Boolean)


103
104
105
106
# File 'lib/rails_onboarding/error_recovery.rb', line 103

def has_errors?(user)
  return false unless user&.respond_to?(:onboarding_failed_actions)
  user.onboarding_failed_actions&.any? || false
end

.mark_as_failed(user, action, error) ⇒ Object

Mark an action as failed after all retries



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/rails_onboarding/error_recovery.rb', line 75

def mark_as_failed(user, action, error)
  return unless user

  if user.respond_to?(:onboarding_failed_actions)
    user.onboarding_failed_actions ||= []
    user.onboarding_failed_actions << {
      action: action.to_s,
      error: error&.message,
      failed_at: Time.current.iso8601
    }
    user.save(validate: false) rescue nil
  end

  Rails.logger.error("Action #{action} failed for user #{user.id} after all retries")
end

.record_error(user, action, error, attempt) ⇒ Object

Record an error for analytics and debugging



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/rails_onboarding/error_recovery.rb', line 46

def record_error(user, action, error, attempt)
  return unless user && error

  error_data = {
    action: action.to_s,
    error_class: error.class.name,
    error_message: error.message,
    attempt: attempt,
    timestamp: Time.current.iso8601
  }

  # Store in user's error log if available
  if user.respond_to?(:onboarding_errors)
    user.onboarding_errors ||= []
    user.onboarding_errors << error_data
    user.save(validate: false) rescue nil
  end

  # Track in analytics
  if defined?(RailsOnboarding::AnalyticsEvent)
    RailsOnboarding::AnalyticsEvent.track_custom_event(
      user: user,
      event_name: "onboarding_error",
      event_data: error_data
    )
  end
end

.reset_errors(user) ⇒ Object

Reset all errors for a user



121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/rails_onboarding/error_recovery.rb', line 121

def reset_errors(user)
  return unless user

  if user.respond_to?(:onboarding_errors=)
    user.onboarding_errors = []
  end

  if user.respond_to?(:onboarding_failed_actions=)
    user.onboarding_failed_actions = []
  end

  user.save(validate: false) rescue nil
end

.retry_failed_action(user, action, &block) ⇒ Object

Retry a failed action



115
116
117
118
# File 'lib/rails_onboarding/error_recovery.rb', line 115

def retry_failed_action(user, action, &block)
  clear_error_state(user, action)
  with_recovery(user, action, &block)
end

.with_exponential_backoff(max_attempts: 3, base_delay: 1, max_delay: 30, exponential_base: 2) ⇒ Object

Retry with exponential backoff



136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/rails_onboarding/error_recovery.rb', line 136

def with_exponential_backoff(max_attempts: 3, base_delay: 1, max_delay: 30, exponential_base: 2)
  attempt = 0
  begin
    attempt += 1
    yield
  rescue StandardError => e
    if attempt < max_attempts
      delay = [ base_delay * (exponential_base ** (attempt - 1)), max_delay ].min
      Rails.logger.warn "Attempt #{attempt} failed: #{e.message}. Retrying in #{delay}s..."
      sleep delay
      retry
    else
      Rails.logger.error "All #{max_attempts} attempts failed: #{e.message}"
      raise
    end
  end
end

.with_fallback(fallback_value = nil) ⇒ Object

Execute with fallback value



165
166
167
168
169
170
# File 'lib/rails_onboarding/error_recovery.rb', line 165

def with_fallback(fallback_value = nil)
  yield
rescue StandardError => e
  Rails.logger.warn "Operation failed, using fallback: #{e.message}"
  fallback_value
end

.with_recovery(user, action, max_retries: MAX_RETRIES) { ... } ⇒ Boolean

Execute a block with error recovery

Parameters:

  • user (User)

    The user performing the action

  • action (Symbol)

    The action being performed

  • max_retries (Integer) (defaults to: MAX_RETRIES)

    Maximum number of retries

Yields:

  • The block to execute

Returns:

  • (Boolean)

    True if successful, false otherwise



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/rails_onboarding/error_recovery.rb', line 16

def with_recovery(user, action, max_retries: MAX_RETRIES, &block)
  attempt = 0
  last_error = nil

  loop do
    attempt += 1

    begin
      result = block.call
      clear_error_state(user, action)
      return result
    rescue StandardError => e
      last_error = e
      Rails.logger.error("Error in #{action} (attempt #{attempt}/#{max_retries}): #{e.message}")
      Rails.logger.error(e.backtrace.join("\n")) if Rails.env.development?

      record_error(user, action, e, attempt)

      break if attempt >= max_retries

      sleep RETRY_DELAY if defined?(sleep)
    end
  end

  # All retries failed
  mark_as_failed(user, action, last_error)
  false
end

.with_timeout(seconds: 30) ⇒ Object

Execute with timeout



155
156
157
158
159
160
161
162
# File 'lib/rails_onboarding/error_recovery.rb', line 155

def with_timeout(seconds: 30)
  Timeout.timeout(seconds) do
    yield
  end
rescue Timeout::Error => e
  Rails.logger.error "Operation timed out after #{seconds} seconds"
  raise
end