flow_chat 0.6.1 → 0.8.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +85 -1229
  5. data/docs/configuration.md +360 -0
  6. data/docs/flows.md +320 -0
  7. data/docs/images/simulator.png +0 -0
  8. data/docs/instrumentation.md +216 -0
  9. data/docs/media.md +153 -0
  10. data/docs/sessions.md +433 -0
  11. data/docs/testing.md +475 -0
  12. data/docs/ussd-setup.md +322 -0
  13. data/docs/whatsapp-setup.md +162 -0
  14. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  15. data/examples/simulator_controller.rb +13 -22
  16. data/examples/ussd_controller.rb +41 -41
  17. data/examples/whatsapp_controller.rb +32 -125
  18. data/examples/whatsapp_media_examples.rb +68 -336
  19. data/examples/whatsapp_message_job.rb +5 -3
  20. data/flow_chat.gemspec +6 -2
  21. data/lib/flow_chat/base_processor.rb +79 -2
  22. data/lib/flow_chat/config.rb +31 -5
  23. data/lib/flow_chat/context.rb +13 -1
  24. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  25. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  26. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  27. data/lib/flow_chat/instrumentation.rb +70 -0
  28. data/lib/flow_chat/prompt.rb +20 -20
  29. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  30. data/lib/flow_chat/session/middleware.rb +130 -12
  31. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  32. data/lib/flow_chat/simulator/controller.rb +8 -8
  33. data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
  34. data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
  35. data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
  36. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  37. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  38. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  39. data/lib/flow_chat/ussd/processor.rb +16 -4
  40. data/lib/flow_chat/ussd/renderer.rb +1 -1
  41. data/lib/flow_chat/version.rb +1 -1
  42. data/lib/flow_chat/whatsapp/client.rb +99 -12
  43. data/lib/flow_chat/whatsapp/configuration.rb +35 -4
  44. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +121 -34
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +7 -1
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +23 -12
  50. data/.travis.yml +0 -6
  51. data/app/controllers/demo_controller.rb +0 -101
  52. data/app/flow_chat/demo_restaurant_flow.rb +0 -889
  53. data/config/routes_demo.rb +0 -59
  54. data/examples/initializer.rb +0 -86
  55. data/examples/media_prompts_examples.rb +0 -27
  56. data/images/ussd_simulator.png +0 -0
  57. data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
@@ -8,6 +8,11 @@ module FlowChat
8
8
  # When false, only the validation error message is shown to the user.
9
9
  mattr_accessor :combine_validation_error_with_message, default: true
10
10
 
11
+ # Session configuration object
12
+ def self.session
13
+ @session ||= SessionConfig.new
14
+ end
15
+
11
16
  # USSD-specific configuration object
12
17
  def self.ussd
13
18
  @ussd ||= UssdConfig.new
@@ -18,10 +23,29 @@ module FlowChat
18
23
  @whatsapp ||= WhatsappConfig.new
19
24
  end
20
25
 
26
+ class SessionConfig
27
+ attr_accessor :boundaries, :hash_phone_numbers, :identifier
28
+
29
+ def initialize
30
+ # Session boundaries control how session IDs are constructed
31
+ # :flow = separate sessions per flow
32
+ # :gateway = separate sessions per gateway
33
+ # :platform = separate sessions per platform (ussd, whatsapp)
34
+ @boundaries = [:flow, :gateway, :platform]
35
+
36
+ # Always hash phone numbers for privacy
37
+ @hash_phone_numbers = true
38
+
39
+ # Session identifier type (nil = let platforms choose their default)
40
+ # :msisdn = durable sessions (durable across timeouts)
41
+ # :request_id = ephemeral sessions (new session each time)
42
+ @identifier = nil
43
+ end
44
+ end
45
+
21
46
  class UssdConfig
22
47
  attr_accessor :pagination_page_size, :pagination_back_option, :pagination_back_text,
23
- :pagination_next_option, :pagination_next_text,
24
- :resumable_sessions_enabled, :resumable_sessions_global, :resumable_sessions_timeout_seconds
48
+ :pagination_next_option, :pagination_next_text
25
49
 
26
50
  def initialize
27
51
  @pagination_page_size = 140
@@ -29,9 +53,6 @@ module FlowChat
29
53
  @pagination_back_text = "Back"
30
54
  @pagination_next_option = "#"
31
55
  @pagination_next_text = "More"
32
- @resumable_sessions_enabled = false
33
- @resumable_sessions_global = true
34
- @resumable_sessions_timeout_seconds = 300
35
56
  end
36
57
  end
37
58
 
@@ -68,4 +89,9 @@ module FlowChat
68
89
  end
69
90
  end
70
91
  end
92
+
93
+ # Shorthand for accessing the logger throughout the application
94
+ def self.logger
95
+ Config.logger
96
+ end
71
97
  end
@@ -1,26 +1,38 @@
1
1
  module FlowChat
2
2
  class Context
3
+ include FlowChat::Instrumentation
4
+
3
5
  def initialize
4
6
  @data = {}.with_indifferent_access
7
+
8
+ # Use instrumentation for context creation
9
+ self.class.instrument(Events::CONTEXT_CREATED, {
10
+ gateway: @data["request.gateway"]
11
+ })
5
12
  end
6
13
 
7
14
  def [](key)
8
- @data[key]
15
+ value = @data[key]
16
+ FlowChat.logger.debug { "Context: Getting '#{key}' = #{value.inspect}" } if key != "session.store" # Avoid logging session store object
17
+ value
9
18
  end
10
19
 
11
20
  def []=(key, value)
21
+ FlowChat.logger.debug { "Context: Setting '#{key}' = #{value.inspect}" } if key != "session.store" && key != "controller" # Avoid logging large objects
12
22
  @data[key] = value
13
23
  end
14
24
 
15
25
  def input = @data["request.input"]
16
26
 
17
27
  def input=(value)
28
+ FlowChat.logger.debug { "Context: Setting input = '#{value}'" }
18
29
  @data["request.input"] = value
19
30
  end
20
31
 
21
32
  def session = @data["session"]
22
33
 
23
34
  def session=(value)
35
+ FlowChat.logger.debug { "Context: Setting session = #{value.class.name}" }
24
36
  @data["session"] = value
25
37
  end
26
38
 
@@ -0,0 +1,176 @@
1
+ module FlowChat
2
+ module Instrumentation
3
+ class LogSubscriber
4
+ # Flow execution events
5
+ def flow_execution_start(event)
6
+ payload = event.payload
7
+ FlowChat.logger.info { "Flow Execution Started: #{payload[:flow_name]}##{payload[:action]} [Session: #{payload[:session_id]}]" }
8
+ end
9
+
10
+ def flow_execution_end(event)
11
+ payload = event.payload
12
+ duration = event.duration.round(2)
13
+ FlowChat.logger.info { "Flow Execution Completed: #{payload[:flow_name]}##{payload[:action]} (#{duration}ms) [Session: #{payload[:session_id]}]" }
14
+ end
15
+
16
+ def flow_execution_error(event)
17
+ payload = event.payload
18
+ duration = event.duration.round(2)
19
+ FlowChat.logger.error { "Flow Execution Failed: #{payload[:flow_name]}##{payload[:action]} (#{duration}ms) - #{payload[:error_class]}: #{payload[:error_message]} [Session: #{payload[:session_id]}]" }
20
+ end
21
+
22
+ # Session events
23
+ def session_created(event)
24
+ payload = event.payload
25
+ FlowChat.logger.info { "Session Created: #{payload[:session_id]} [Store: #{payload[:store_type]}, Gateway: #{payload[:gateway]}]" }
26
+ end
27
+
28
+ def session_destroyed(event)
29
+ payload = event.payload
30
+ FlowChat.logger.info { "Session Destroyed: #{payload[:session_id]} [Gateway: #{payload[:gateway]}]" }
31
+ end
32
+
33
+ def session_cache_hit(event)
34
+ payload = event.payload
35
+ FlowChat.logger.debug { "Session Cache Hit: #{payload[:session_id]} - Key: #{payload[:key]}" }
36
+ end
37
+
38
+ def session_cache_miss(event)
39
+ payload = event.payload
40
+ FlowChat.logger.debug { "Session Cache Miss: #{payload[:session_id]} - Key: #{payload[:key]}" }
41
+ end
42
+
43
+ def session_data_set(event)
44
+ payload = event.payload
45
+ FlowChat.logger.debug { "Session Data Set: #{payload[:session_id]} - Key: #{payload[:key]}" }
46
+ end
47
+
48
+ def session_data_get(event)
49
+ payload = event.payload
50
+ FlowChat.logger.debug { "Session Data Get: #{payload[:session_id]} - Key: #{payload[:key]} = #{payload[:value].inspect}" }
51
+ end
52
+
53
+ # Middleware events
54
+ def middleware_before(event)
55
+ payload = event.payload
56
+ FlowChat.logger.debug { "Middleware Before: #{payload[:middleware_name]} [Session: #{payload[:session_id]}]" }
57
+ end
58
+
59
+ def middleware_after(event)
60
+ payload = event.payload
61
+ duration = event.duration.round(2)
62
+ FlowChat.logger.debug { "Middleware After: #{payload[:middleware_name]} (#{duration}ms) [Session: #{payload[:session_id]}]" }
63
+ end
64
+
65
+ # Platform-agnostic events (new scalable approach)
66
+ def message_received(event)
67
+ payload = event.payload
68
+ platform = payload[:platform] || "unknown"
69
+ platform_name = format_platform_name(platform)
70
+
71
+ case platform.to_sym
72
+ when :whatsapp
73
+ contact_info = payload[:contact_name] ? " (#{payload[:contact_name]})" : ""
74
+ FlowChat.logger.info { "#{platform_name} Message Received: #{payload[:from]}#{contact_info} - Type: #{payload[:message_type]} [ID: #{payload[:message_id]}]" }
75
+ when :ussd
76
+ FlowChat.logger.info { "#{platform_name} Message Received: #{payload[:from]} - Input: '#{payload[:message]}' [Session: #{payload[:session_id]}]" }
77
+ else
78
+ FlowChat.logger.info { "#{platform_name} Message Received: #{payload[:from]} - Message: '#{payload[:message]}' [Session: #{payload[:session_id]}]" }
79
+ end
80
+ end
81
+
82
+ def message_sent(event)
83
+ payload = event.payload
84
+ platform = payload[:platform] || "unknown"
85
+ platform_name = format_platform_name(platform)
86
+ duration = event.duration.round(2)
87
+
88
+ case platform.to_sym
89
+ when :whatsapp
90
+ FlowChat.logger.info { "#{platform_name} Message Sent: #{payload[:to]} - Type: #{payload[:message_type]} (#{duration}ms) [Length: #{payload[:content_length]} chars]" }
91
+ when :ussd
92
+ FlowChat.logger.info { "#{platform_name} Message Sent: #{payload[:to]} - Type: #{payload[:message_type]} (#{duration}ms) [Session: #{payload[:session_id]}]" }
93
+ else
94
+ FlowChat.logger.info { "#{platform_name} Message Sent: #{payload[:to]} - Type: #{payload[:message_type]} (#{duration}ms)" }
95
+ end
96
+ end
97
+
98
+ def webhook_verified(event)
99
+ payload = event.payload
100
+ platform = payload[:platform] || "unknown"
101
+ platform_name = format_platform_name(platform)
102
+ FlowChat.logger.info { "#{platform_name} Webhook Verified Successfully [Challenge: #{payload[:challenge]}]" }
103
+ end
104
+
105
+ def webhook_failed(event)
106
+ payload = event.payload
107
+ platform = payload[:platform] || "unknown"
108
+ platform_name = format_platform_name(platform)
109
+ FlowChat.logger.warn { "#{platform_name} Webhook Verification Failed: #{payload[:reason]}" }
110
+ end
111
+
112
+ def media_upload(event)
113
+ payload = event.payload
114
+ platform = payload[:platform] || "unknown"
115
+ platform_name = format_platform_name(platform)
116
+ duration = event.duration.round(2)
117
+
118
+ if payload[:success] != false # Check for explicit false, not just falsy
119
+ FlowChat.logger.info { "#{platform_name} Media Upload: #{payload[:filename]} (#{format_bytes(payload[:size])}, #{duration}ms) - Success" }
120
+ else
121
+ FlowChat.logger.error { "#{platform_name} Media Upload Failed: #{payload[:filename]} (#{duration}ms) - #{payload[:error]}" }
122
+ end
123
+ end
124
+
125
+ def pagination_triggered(event)
126
+ payload = event.payload
127
+ platform = payload[:platform] || "unknown"
128
+ platform_name = format_platform_name(platform)
129
+ FlowChat.logger.info { "#{platform_name} Pagination Triggered: Page #{payload[:current_page]}/#{payload[:total_pages]} (#{payload[:content_length]} chars) [Session: #{payload[:session_id]}]" }
130
+ end
131
+
132
+ def api_request(event)
133
+ payload = event.payload
134
+ platform = payload[:platform] || "unknown"
135
+ platform_name = format_platform_name(platform)
136
+ duration = event.duration.round(2)
137
+
138
+ if payload[:success]
139
+ FlowChat.logger.debug { "#{platform_name} API Request: #{payload[:method]} #{payload[:endpoint]} (#{duration}ms) - Success" }
140
+ else
141
+ FlowChat.logger.error { "#{platform_name} API Request: #{payload[:method]} #{payload[:endpoint]} (#{duration}ms) - Failed: #{payload[:status]} #{payload[:error]}" }
142
+ end
143
+ end
144
+
145
+ # Context events
146
+ def context_created(event)
147
+ payload = event.payload
148
+ FlowChat.logger.debug { "Context Created [Gateway: #{payload[:gateway] || "unknown"}]" }
149
+ end
150
+
151
+ private
152
+
153
+ # Format platform name for display
154
+ def format_platform_name(platform)
155
+ case platform.to_sym
156
+ when :whatsapp then "WhatsApp"
157
+ when :ussd then "USSD"
158
+ else platform.to_s.capitalize
159
+ end
160
+ end
161
+
162
+ # Format bytes in a human-readable way
163
+ def format_bytes(bytes)
164
+ return "unknown size" unless bytes
165
+
166
+ if bytes < 1024
167
+ "#{bytes} bytes"
168
+ elsif bytes < 1024 * 1024
169
+ "#{(bytes / 1024.0).round(1)} KB"
170
+ else
171
+ "#{(bytes / (1024.0 * 1024.0)).round(1)} MB"
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,197 @@
1
+ module FlowChat
2
+ module Instrumentation
3
+ class MetricsCollector
4
+ attr_reader :metrics
5
+
6
+ def initialize
7
+ @metrics = {}
8
+ @mutex = Mutex.new
9
+ subscribe_to_events
10
+ end
11
+
12
+ # Get current metrics snapshot
13
+ def snapshot
14
+ @mutex.synchronize { @metrics.dup }
15
+ end
16
+
17
+ # Reset all metrics
18
+ def reset!
19
+ @mutex.synchronize { @metrics.clear }
20
+ end
21
+
22
+ # Get metrics for a specific category
23
+ def get_category(category)
24
+ @mutex.synchronize do
25
+ @metrics.select { |key, _| key.to_s.start_with?("#{category}.") }
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def subscribe_to_events
32
+ # Flow execution metrics
33
+ ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |event|
34
+ increment_counter("flows.executed")
35
+ track_timing("flows.execution_time", event.duration)
36
+ increment_counter("flows.by_name.#{event.payload[:flow_name]}")
37
+ end
38
+
39
+ ActiveSupport::Notifications.subscribe("flow.execution.error.flow_chat") do |event|
40
+ increment_counter("flows.errors")
41
+ increment_counter("flows.errors.by_class.#{event.payload[:error_class]}")
42
+ increment_counter("flows.errors.by_flow.#{event.payload[:flow_name]}")
43
+ end
44
+
45
+ # Session metrics
46
+ ActiveSupport::Notifications.subscribe("session.created.flow_chat") do |event|
47
+ increment_counter("sessions.created")
48
+ increment_counter("sessions.created.by_gateway.#{event.payload[:gateway]}")
49
+ end
50
+
51
+ ActiveSupport::Notifications.subscribe("session.destroyed.flow_chat") do |event|
52
+ increment_counter("sessions.destroyed")
53
+ end
54
+
55
+ ActiveSupport::Notifications.subscribe("session.cache.hit.flow_chat") do |event|
56
+ increment_counter("sessions.cache.hits")
57
+ end
58
+
59
+ ActiveSupport::Notifications.subscribe("session.cache.miss.flow_chat") do |event|
60
+ increment_counter("sessions.cache.misses")
61
+ end
62
+
63
+ ActiveSupport::Notifications.subscribe("session.data.get.flow_chat") do |event|
64
+ increment_counter("sessions.data.get")
65
+ end
66
+
67
+ ActiveSupport::Notifications.subscribe("session.data.set.flow_chat") do |event|
68
+ increment_counter("sessions.data.set")
69
+ end
70
+
71
+ ActiveSupport::Notifications.subscribe("message.received.flow_chat") do |event|
72
+ platform = event.payload[:platform] || "unknown"
73
+ increment_counter("#{platform}.messages.received")
74
+ if event.payload[:message_type]
75
+ increment_counter("#{platform}.messages.received.by_type.#{event.payload[:message_type]}")
76
+ end
77
+ end
78
+
79
+ ActiveSupport::Notifications.subscribe("message.sent.flow_chat") do |event|
80
+ platform = event.payload[:platform] || "unknown"
81
+ increment_counter("#{platform}.messages.sent")
82
+ if event.payload[:message_type]
83
+ increment_counter("#{platform}.messages.sent.by_type.#{event.payload[:message_type]}")
84
+ end
85
+ track_timing("#{platform}.api.response_time", event.duration)
86
+ end
87
+
88
+ ActiveSupport::Notifications.subscribe("webhook.verified.flow_chat") do |event|
89
+ platform = event.payload[:platform] || "unknown"
90
+ increment_counter("#{platform}.webhook.verified")
91
+ end
92
+
93
+ ActiveSupport::Notifications.subscribe("webhook.failed.flow_chat") do |event|
94
+ platform = event.payload[:platform] || "unknown"
95
+ increment_counter("#{platform}.webhook.failures")
96
+ if event.payload[:reason]
97
+ increment_counter("#{platform}.webhook.failures.by_reason.#{event.payload[:reason]}")
98
+ end
99
+ end
100
+
101
+ ActiveSupport::Notifications.subscribe("media.upload.flow_chat") do |event|
102
+ platform = event.payload[:platform] || "unknown"
103
+ if event.payload[:success] != false
104
+ increment_counter("#{platform}.media.uploads.success")
105
+ if event.payload[:size]
106
+ track_histogram("#{platform}.media.upload_size", event.payload[:size])
107
+ end
108
+ else
109
+ increment_counter("#{platform}.media.uploads.failure")
110
+ end
111
+ track_timing("#{platform}.media.upload_time", event.duration)
112
+ end
113
+
114
+ ActiveSupport::Notifications.subscribe("pagination.triggered.flow_chat") do |event|
115
+ platform = event.payload[:platform] || "unknown"
116
+ increment_counter("#{platform}.pagination.triggered")
117
+ if event.payload[:content_length]
118
+ track_histogram("#{platform}.pagination.content_length", event.payload[:content_length])
119
+ end
120
+ end
121
+
122
+ ActiveSupport::Notifications.subscribe("api.request.flow_chat") do |event|
123
+ platform = event.payload[:platform] || "unknown"
124
+ if event.payload[:success]
125
+ increment_counter("#{platform}.api.requests.success")
126
+ else
127
+ increment_counter("#{platform}.api.requests.failure")
128
+ if event.payload[:status]
129
+ increment_counter("#{platform}.api.requests.failure.by_status.#{event.payload[:status]}")
130
+ end
131
+ end
132
+ track_timing("#{platform}.api.request_time", event.duration)
133
+ end
134
+ end
135
+
136
+ def increment_counter(key, value = 1)
137
+ @mutex.synchronize do
138
+ @metrics[key] ||= 0
139
+ @metrics[key] += value
140
+ end
141
+ end
142
+
143
+ def track_timing(key, duration_ms)
144
+ timing_key = "#{key}.timings"
145
+ @mutex.synchronize do
146
+ @metrics[timing_key] ||= []
147
+ @metrics[timing_key] << duration_ms
148
+
149
+ # Keep only last 1000 measurements for memory efficiency
150
+ @metrics[timing_key] = @metrics[timing_key].last(1000) if @metrics[timing_key].size > 1000
151
+
152
+ # Calculate and store aggregates
153
+ timings = @metrics[timing_key]
154
+ @metrics["#{key}.avg"] = timings.sum / timings.size
155
+ @metrics["#{key}.min"] = timings.min
156
+ @metrics["#{key}.max"] = timings.max
157
+ @metrics["#{key}.p50"] = percentile(timings, 50)
158
+ @metrics["#{key}.p95"] = percentile(timings, 95)
159
+ @metrics["#{key}.p99"] = percentile(timings, 99)
160
+ end
161
+ end
162
+
163
+ def track_histogram(key, value)
164
+ histogram_key = "#{key}.histogram"
165
+ @mutex.synchronize do
166
+ @metrics[histogram_key] ||= []
167
+ @metrics[histogram_key] << value
168
+
169
+ # Keep only last 1000 measurements
170
+ @metrics[histogram_key] = @metrics[histogram_key].last(1000) if @metrics[histogram_key].size > 1000
171
+
172
+ # Calculate aggregates
173
+ values = @metrics[histogram_key]
174
+ @metrics["#{key}.total"] = values.sum
175
+ @metrics["#{key}.avg"] = values.sum / values.size
176
+ @metrics["#{key}.min"] = values.min
177
+ @metrics["#{key}.max"] = values.max
178
+ end
179
+ end
180
+
181
+ def percentile(array, percentile)
182
+ return nil if array.empty?
183
+
184
+ sorted = array.sort
185
+ k = (percentile / 100.0) * (sorted.length - 1)
186
+ f = k.floor
187
+ c = k.ceil
188
+
189
+ return sorted[k] if f == c
190
+
191
+ d0 = sorted[f] * (c - k)
192
+ d1 = sorted[c] * (k - f)
193
+ d0 + d1
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,155 @@
1
+ module FlowChat
2
+ module Instrumentation
3
+ module Setup
4
+ class << self
5
+ attr_accessor :log_subscriber, :metrics_collector
6
+
7
+ # Initialize instrumentation with default subscribers
8
+ def initialize!
9
+ setup_log_subscriber if FlowChat::Config.logger
10
+ setup_metrics_collector
11
+
12
+ FlowChat.logger&.info { "FlowChat::Instrumentation: Initialized with logging and metrics collection" }
13
+ end
14
+
15
+ # Set up both logging and metrics collection
16
+ def setup_instrumentation!(options = {})
17
+ setup_logging!(options)
18
+ setup_metrics!(options)
19
+ end
20
+
21
+ # Set up logging (LogSubscriber)
22
+ def setup_logging!(options = {})
23
+ return if @log_subscriber_setup
24
+
25
+ require_relative "log_subscriber"
26
+ setup_log_subscriber(options)
27
+ @log_subscriber_setup = true
28
+ end
29
+
30
+ # Set up metrics collection (MetricsCollector)
31
+ def setup_metrics!(options = {})
32
+ return if @metrics_collector_setup
33
+
34
+ require_relative "metrics_collector"
35
+ setup_metrics_collector(options)
36
+ @metrics_collector_setup = true
37
+ end
38
+
39
+ # Cleanup all subscribers
40
+ def cleanup!
41
+ @log_subscriber = nil
42
+ @metrics_collector = nil
43
+
44
+ # Note: ActiveSupport::Notifications doesn't provide an easy way to
45
+ # unsubscribe all subscribers, so this is mainly for reference cleanup
46
+ FlowChat.logger&.info { "FlowChat::Instrumentation: Cleaned up instrumentation" }
47
+ end
48
+
49
+ # Get current metrics (thread-safe)
50
+ def metrics
51
+ @metrics_collector&.snapshot || {}
52
+ end
53
+
54
+ # Reset metrics
55
+ def reset_metrics!
56
+ @metrics_collector&.reset!
57
+ end
58
+
59
+ # Subscribe to custom events
60
+ def subscribe(event_pattern, &block)
61
+ ActiveSupport::Notifications.subscribe(event_pattern, &block)
62
+ end
63
+
64
+ # Instrument a one-off event
65
+ def instrument(event_name, payload = {}, &block)
66
+ full_event_name = "#{event_name}.flow_chat"
67
+
68
+ enriched_payload = {
69
+ timestamp: Time.current
70
+ }.merge(payload).compact
71
+
72
+ ActiveSupport::Notifications.instrument(full_event_name, enriched_payload, &block)
73
+ end
74
+
75
+ # Access the metrics collector instance
76
+ def metrics_collector
77
+ @metrics_collector ||= FlowChat::Instrumentation::MetricsCollector.new
78
+ end
79
+
80
+ # Reset instrumentation (useful for testing)
81
+ def reset!
82
+ @log_subscriber_setup = false
83
+ @metrics_collector_setup = false
84
+ @log_subscriber = nil
85
+ @metrics_collector = nil
86
+ end
87
+
88
+ private
89
+
90
+ def setup_log_subscriber(options = {})
91
+ # Check if Rails is available and use its initialization callback
92
+ if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
93
+ Rails.application.config.after_initialize do
94
+ initialize_log_subscriber
95
+ end
96
+ else
97
+ # Initialize immediately for non-Rails environments
98
+ initialize_log_subscriber
99
+ end
100
+ end
101
+
102
+ def initialize_log_subscriber
103
+ return if @log_subscriber
104
+
105
+ @log_subscriber = FlowChat::Instrumentation::LogSubscriber.new
106
+
107
+ # Manually subscribe to all FlowChat events
108
+ subscribe_to_events
109
+ end
110
+
111
+ def subscribe_to_events
112
+ # Core framework events
113
+ subscribe_event("flow.execution.start.flow_chat", :flow_execution_start)
114
+ subscribe_event("flow.execution.end.flow_chat", :flow_execution_end)
115
+ subscribe_event("flow.execution.error.flow_chat", :flow_execution_error)
116
+
117
+ # Session events
118
+ subscribe_event("session.created.flow_chat", :session_created)
119
+ subscribe_event("session.destroyed.flow_chat", :session_destroyed)
120
+ subscribe_event("session.data.get.flow_chat", :session_data_get)
121
+ subscribe_event("session.data.set.flow_chat", :session_data_set)
122
+ subscribe_event("session.cache.hit.flow_chat", :session_cache_hit)
123
+ subscribe_event("session.cache.miss.flow_chat", :session_cache_miss)
124
+
125
+ # Platform-agnostic events (new scalable approach)
126
+ subscribe_event("message.received.flow_chat", :message_received)
127
+ subscribe_event("message.sent.flow_chat", :message_sent)
128
+ subscribe_event("webhook.verified.flow_chat", :webhook_verified)
129
+ subscribe_event("webhook.failed.flow_chat", :webhook_failed)
130
+ subscribe_event("api.request.flow_chat", :api_request)
131
+ subscribe_event("media.upload.flow_chat", :media_upload)
132
+ subscribe_event("pagination.triggered.flow_chat", :pagination_triggered)
133
+
134
+ # Middleware events
135
+ subscribe_event("middleware.before.flow_chat", :middleware_before)
136
+ subscribe_event("middleware.after.flow_chat", :middleware_after)
137
+
138
+ # Context events
139
+ subscribe_event("context.created.flow_chat", :context_created)
140
+ end
141
+
142
+ def subscribe_event(event_name, method_name)
143
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
144
+ event = ActiveSupport::Notifications::Event.new(*args)
145
+ @log_subscriber.send(method_name, event) if @log_subscriber.respond_to?(method_name)
146
+ end
147
+ end
148
+
149
+ def setup_metrics_collector(options = {})
150
+ @metrics_collector = FlowChat::Instrumentation::MetricsCollector.new
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end