flow_chat 0.6.0 → 0.7.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +44 -0
  3. data/.gitignore +2 -1
  4. data/README.md +84 -1229
  5. data/docs/configuration.md +337 -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/testing.md +475 -0
  11. data/docs/ussd-setup.md +306 -0
  12. data/docs/whatsapp-setup.md +162 -0
  13. data/examples/multi_tenant_whatsapp_controller.rb +9 -37
  14. data/examples/simulator_controller.rb +9 -18
  15. data/examples/ussd_controller.rb +32 -38
  16. data/examples/whatsapp_controller.rb +32 -125
  17. data/examples/whatsapp_media_examples.rb +68 -336
  18. data/examples/whatsapp_message_job.rb +5 -3
  19. data/flow_chat.gemspec +6 -2
  20. data/lib/flow_chat/base_processor.rb +48 -2
  21. data/lib/flow_chat/config.rb +5 -0
  22. data/lib/flow_chat/context.rb +13 -1
  23. data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
  24. data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
  25. data/lib/flow_chat/instrumentation/setup.rb +155 -0
  26. data/lib/flow_chat/instrumentation.rb +70 -0
  27. data/lib/flow_chat/prompt.rb +20 -20
  28. data/lib/flow_chat/session/cache_session_store.rb +73 -7
  29. data/lib/flow_chat/session/middleware.rb +37 -4
  30. data/lib/flow_chat/session/rails_session_store.rb +36 -1
  31. data/lib/flow_chat/simulator/controller.rb +7 -7
  32. data/lib/flow_chat/ussd/app.rb +1 -1
  33. data/lib/flow_chat/ussd/gateway/nalo.rb +30 -0
  34. data/lib/flow_chat/ussd/gateway/nsano.rb +33 -0
  35. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
  36. data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
  37. data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
  38. data/lib/flow_chat/ussd/processor.rb +14 -0
  39. data/lib/flow_chat/ussd/renderer.rb +1 -1
  40. data/lib/flow_chat/version.rb +1 -1
  41. data/lib/flow_chat/whatsapp/app.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 +128 -54
  45. data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
  46. data/lib/flow_chat/whatsapp/processor.rb +8 -0
  47. data/lib/flow_chat/whatsapp/renderer.rb +4 -9
  48. data/lib/flow_chat.rb +23 -0
  49. metadata +22 -11
  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
@@ -10,26 +10,39 @@ module FlowChat
10
10
 
11
11
  module Gateway
12
12
  class CloudApi
13
+ include FlowChat::Instrumentation
14
+
15
+ attr_reader :context
16
+
13
17
  def initialize(app, config = nil)
14
18
  @app = app
15
19
  @config = config || FlowChat::Whatsapp::Configuration.from_credentials
16
20
  @client = FlowChat::Whatsapp::Client.new(@config)
21
+
22
+ FlowChat.logger.info { "CloudApi: Initialized WhatsApp Cloud API gateway with phone_number_id: #{@config.phone_number_id}" }
23
+ FlowChat.logger.debug { "CloudApi: Gateway configuration - API base URL: #{FlowChat::Config.whatsapp.api_base_url}" }
17
24
  end
18
25
 
19
26
  def call(context)
27
+ @context = context
20
28
  controller = context.controller
21
29
  request = controller.request
22
30
 
31
+ FlowChat.logger.debug { "CloudApi: Processing #{request.request_method} request to #{request.path}" }
32
+
23
33
  # Handle webhook verification
24
34
  if request.get? && request.params["hub.mode"] == "subscribe"
35
+ FlowChat.logger.info { "CloudApi: Handling webhook verification request" }
25
36
  return handle_verification(context)
26
37
  end
27
38
 
28
39
  # Handle webhook messages
29
40
  if request.post?
41
+ FlowChat.logger.info { "CloudApi: Handling webhook message" }
30
42
  return handle_webhook(context)
31
43
  end
32
44
 
45
+ FlowChat.logger.warn { "CloudApi: Invalid request method or parameters - returning bad request" }
33
46
  controller.head :bad_request
34
47
  end
35
48
 
@@ -41,11 +54,14 @@ module FlowChat
41
54
  def determine_message_handler(context)
42
55
  # Check if simulator mode was already detected and set in context
43
56
  if context["simulator_mode"]
57
+ FlowChat.logger.debug { "CloudApi: Using simulator message handler" }
44
58
  return :simulator
45
59
  end
46
60
 
47
61
  # Use global WhatsApp configuration
48
- FlowChat::Config.whatsapp.message_handling_mode
62
+ mode = FlowChat::Config.whatsapp.message_handling_mode
63
+ FlowChat.logger.debug { "CloudApi: Using #{mode} message handling mode" }
64
+ mode
49
65
  end
50
66
 
51
67
  def handle_verification(context)
@@ -53,93 +69,127 @@ module FlowChat
53
69
  params = controller.request.params
54
70
 
55
71
  verify_token = @config.verify_token
72
+ provided_token = params["hub.verify_token"]
73
+ challenge = params["hub.challenge"]
56
74
 
57
- if params["hub.verify_token"] == verify_token
58
- controller.render plain: params["hub.challenge"]
75
+ FlowChat.logger.debug { "CloudApi: Webhook verification - provided token matches: #{provided_token == verify_token}" }
76
+
77
+ if provided_token == verify_token
78
+ # Use instrumentation for webhook verification success
79
+ instrument(Events::WEBHOOK_VERIFIED, {
80
+ challenge: challenge,
81
+ platform: :whatsapp
82
+ })
83
+
84
+ controller.render plain: challenge
59
85
  else
86
+ # Use instrumentation for webhook verification failure
87
+ instrument(Events::WEBHOOK_FAILED, {
88
+ reason: "Invalid verify token",
89
+ platform: :whatsapp
90
+ })
91
+
60
92
  controller.head :forbidden
61
93
  end
62
94
  end
63
95
 
64
96
  def handle_webhook(context)
65
97
  controller = context.controller
66
-
98
+
67
99
  # Parse body
68
100
  begin
69
101
  parse_request_body(controller.request)
102
+ FlowChat.logger.debug { "CloudApi: Successfully parsed webhook request body" }
70
103
  rescue JSON::ParserError => e
71
- Rails.logger.warn "Failed to parse webhook body: #{e.message}"
104
+ FlowChat.logger.error { "CloudApi: Failed to parse webhook body: #{e.message}" }
72
105
  return controller.head :bad_request
73
106
  end
74
-
107
+
75
108
  # Check for simulator mode parameter in request (before validation)
76
109
  # But only enable if valid simulator token is provided
77
110
  is_simulator_mode = simulate?(context)
78
111
  if is_simulator_mode
112
+ FlowChat.logger.info { "CloudApi: Simulator mode enabled for this request" }
79
113
  context["simulator_mode"] = true
80
114
  end
81
115
 
82
116
  # Validate webhook signature for security (skip for simulator mode)
83
117
  unless is_simulator_mode || valid_webhook_signature?(controller.request)
84
- Rails.logger.warn "Invalid webhook signature received"
118
+ FlowChat.logger.warn { "CloudApi: Invalid webhook signature received - rejecting request" }
85
119
  return controller.head :unauthorized
86
120
  end
87
121
 
122
+ FlowChat.logger.debug { "CloudApi: Webhook signature validation passed" }
123
+
88
124
  # Extract message data from WhatsApp webhook
89
125
  entry = @body.dig("entry", 0)
90
- return controller.head :ok unless entry
126
+ unless entry
127
+ FlowChat.logger.debug { "CloudApi: No entry found in webhook body - returning OK" }
128
+ return controller.head :ok
129
+ end
91
130
 
92
131
  changes = entry.dig("changes", 0)
93
- return controller.head :ok unless changes
132
+ unless changes
133
+ FlowChat.logger.debug { "CloudApi: No changes found in webhook entry - returning OK" }
134
+ return controller.head :ok
135
+ end
94
136
 
95
137
  value = changes["value"]
96
- return controller.head :ok unless value
138
+ unless value
139
+ FlowChat.logger.debug { "CloudApi: No value found in webhook changes - returning OK" }
140
+ return controller.head :ok
141
+ end
97
142
 
98
143
  # Handle incoming messages
99
144
  if value["messages"]&.any?
100
145
  message = value["messages"].first
101
146
  contact = value["contacts"]&.first
102
147
 
103
- context["request.id"] = message["from"]
148
+ phone_number = message["from"]
149
+ message_id = message["id"]
150
+ contact_name = contact&.dig("profile", "name")
151
+
152
+ # Use instrumentation for message received
153
+ instrument(Events::MESSAGE_RECEIVED, {
154
+ from: phone_number,
155
+ message: context.input,
156
+ message_type: message["type"],
157
+ message_id: message_id,
158
+ platform: :whatsapp
159
+ })
160
+
161
+ context["request.id"] = phone_number
104
162
  context["request.gateway"] = :whatsapp_cloud_api
105
- context["request.message_id"] = message["id"]
106
- context["request.msisdn"] = Phonelib.parse(message["from"]).e164
107
- context["request.contact_name"] = contact&.dig("profile", "name")
163
+ context["request.message_id"] = message_id
164
+ context["request.msisdn"] = Phonelib.parse(phone_number).e164
165
+ context["request.contact_name"] = contact_name
108
166
  context["request.timestamp"] = message["timestamp"]
109
167
 
110
168
  # Extract message content based on type
111
169
  extract_message_content(message, context)
112
170
 
171
+ FlowChat.logger.debug { "CloudApi: Message content extracted - Type: #{message["type"]}, Input: '#{context.input}'" }
172
+
113
173
  # Determine message handling mode
114
174
  handler_mode = determine_message_handler(context)
115
175
 
116
176
  # Process the message based on handling mode
117
- begin
118
- case handler_mode
119
- when :inline
120
- handle_message_inline(context, controller)
121
- when :background
122
- handle_message_background(context, controller)
123
- when :simulator
124
- # Return early from simulator mode to preserve the JSON response
125
- return handle_message_simulator(context, controller)
126
- end
127
- rescue => e
128
- # Log the error and set appropriate HTTP status
129
- Rails.logger.error "Error processing WhatsApp message: #{e.message}"
130
- Rails.logger.error e.backtrace&.join("\n") if e.backtrace
131
-
132
- # Return error status to WhatsApp so they know processing failed
133
- controller.head :internal_server_error
134
-
135
- # Re-raise the error to bubble it up for proper error tracking/monitoring
136
- raise e
177
+ case handler_mode
178
+ when :inline
179
+ handle_message_inline(context, controller)
180
+ when :background
181
+ handle_message_background(context, controller)
182
+ when :simulator
183
+ # Return early from simulator mode to preserve the JSON response
184
+ return handle_message_simulator(context, controller)
137
185
  end
138
186
  end
139
187
 
140
188
  # Handle message status updates
141
189
  if value["statuses"]&.any?
142
- Rails.logger.info "WhatsApp status update: #{value["statuses"]}"
190
+ statuses = value["statuses"]
191
+ FlowChat.logger.info { "CloudApi: Received #{statuses.size} status update(s)" }
192
+ FlowChat.logger.debug { "CloudApi: Status updates: #{statuses.inspect}" }
143
193
  end
144
194
 
145
195
  controller.head :ok
@@ -149,18 +199,23 @@ module FlowChat
149
199
  def valid_webhook_signature?(request)
150
200
  # Check if signature validation is explicitly disabled
151
201
  if @config.skip_signature_validation
202
+ FlowChat.logger.debug { "CloudApi: Webhook signature validation is disabled" }
152
203
  return true
153
204
  end
154
205
 
155
206
  # Require app_secret for signature validation
156
207
  unless @config.app_secret && !@config.app_secret.empty?
157
- raise FlowChat::Whatsapp::ConfigurationError,
158
- "WhatsApp app_secret is required for webhook signature validation. " \
159
- "Either configure app_secret or set skip_signature_validation=true to explicitly disable validation."
208
+ error_msg = "WhatsApp app_secret is required for webhook signature validation. " \
209
+ "Either configure app_secret or set skip_signature_validation=true to explicitly disable validation."
210
+ FlowChat.logger.error { "CloudApi: #{error_msg}" }
211
+ raise FlowChat::Whatsapp::ConfigurationError, error_msg
160
212
  end
161
213
 
162
214
  signature_header = request.headers["X-Hub-Signature-256"]
163
- return false unless signature_header
215
+ unless signature_header
216
+ FlowChat.logger.warn { "CloudApi: No X-Hub-Signature-256 header found in request" }
217
+ return false
218
+ end
164
219
 
165
220
  # Extract signature from header (format: "sha256=<signature>")
166
221
  expected_signature = signature_header.sub("sha256=", "")
@@ -178,11 +233,19 @@ module FlowChat
178
233
  )
179
234
 
180
235
  # Compare signatures using secure comparison to prevent timing attacks
181
- secure_compare(expected_signature, calculated_signature)
236
+ signature_valid = secure_compare(expected_signature, calculated_signature)
237
+
238
+ if signature_valid
239
+ FlowChat.logger.debug { "CloudApi: Webhook signature validation successful" }
240
+ else
241
+ FlowChat.logger.warn { "CloudApi: Webhook signature validation failed - signatures do not match" }
242
+ end
243
+
244
+ signature_valid
182
245
  rescue FlowChat::Whatsapp::ConfigurationError
183
246
  raise
184
247
  rescue => e
185
- Rails.logger.error "Error validating webhook signature: #{e.message}"
248
+ FlowChat.logger.error { "CloudApi: Error validating webhook signature: #{e.class.name}: #{e.message}" }
186
249
  false
187
250
  end
188
251
 
@@ -197,24 +260,35 @@ module FlowChat
197
260
  end
198
261
 
199
262
  def extract_message_content(message, context)
200
- case message["type"]
263
+ message_type = message["type"]
264
+ FlowChat.logger.debug { "CloudApi: Extracting content from #{message_type} message" }
265
+
266
+ case message_type
201
267
  when "text"
202
- context.input = message.dig("text", "body")
268
+ content = message.dig("text", "body")
269
+ context.input = content
270
+ FlowChat.logger.debug { "CloudApi: Text message content: '#{content}'" }
203
271
  when "interactive"
204
272
  # Handle button/list replies
205
273
  if message.dig("interactive", "type") == "button_reply"
206
- context.input = message.dig("interactive", "button_reply", "id")
274
+ content = message.dig("interactive", "button_reply", "id")
275
+ context.input = content
276
+ FlowChat.logger.debug { "CloudApi: Button reply ID: '#{content}'" }
207
277
  elsif message.dig("interactive", "type") == "list_reply"
208
- context.input = message.dig("interactive", "list_reply", "id")
278
+ content = message.dig("interactive", "list_reply", "id")
279
+ context.input = content
280
+ FlowChat.logger.debug { "CloudApi: List reply ID: '#{content}'" }
209
281
  end
210
282
  when "location"
211
- context["request.location"] = {
283
+ location = {
212
284
  latitude: message.dig("location", "latitude"),
213
285
  longitude: message.dig("location", "longitude"),
214
286
  name: message.dig("location", "name"),
215
287
  address: message.dig("location", "address")
216
288
  }
289
+ context["request.location"] = location
217
290
  context.input = "$location$"
291
+ FlowChat.logger.debug { "CloudApi: Location received - Lat: #{location[:latitude]}, Lng: #{location[:longitude]}" }
218
292
  when "image", "document", "audio", "video"
219
293
  context["request.media"] = {
220
294
  type: message["type"],
@@ -243,7 +317,7 @@ module FlowChat
243
317
  if response
244
318
  _type, prompt, choices, media = response
245
319
  rendered_message = render_response(prompt, choices, media)
246
-
320
+
247
321
  # Queue only the response delivery asynchronously
248
322
  send_data = {
249
323
  msisdn: context["request.msisdn"],
@@ -273,7 +347,7 @@ module FlowChat
273
347
  if response
274
348
  _type, prompt, choices, media = response
275
349
  rendered_message = render_response(prompt, choices, media)
276
-
350
+
277
351
  # For simulator mode, return the response data in the HTTP response
278
352
  # instead of actually sending via WhatsApp API
279
353
  message_payload = @client.build_message_payload(rendered_message, context["request.msisdn"])
@@ -297,7 +371,7 @@ module FlowChat
297
371
  def simulate?(context)
298
372
  # Check if simulator mode is enabled for this processor
299
373
  return false unless context["enable_simulator"]
300
-
374
+
301
375
  # Then check if simulator mode is requested and valid
302
376
  @body.dig("simulator_mode") && valid_simulator_cookie?(context)
303
377
  end
@@ -305,27 +379,27 @@ module FlowChat
305
379
  def valid_simulator_cookie?(context)
306
380
  simulator_secret = FlowChat::Config.simulator_secret
307
381
  return false unless simulator_secret && !simulator_secret.empty?
308
-
382
+
309
383
  # Check for simulator cookie
310
384
  request = context.controller.request
311
385
  simulator_cookie = request.cookies["flowchat_simulator"]
312
386
  return false unless simulator_cookie
313
-
387
+
314
388
  # Verify the cookie is a valid HMAC signature
315
389
  # Cookie format: "timestamp:signature" where signature = HMAC(simulator_secret, "simulator:timestamp")
316
390
  begin
317
391
  timestamp_str, signature = simulator_cookie.split(":", 2)
318
392
  return false unless timestamp_str && signature
319
-
393
+
320
394
  # Check timestamp is recent (within 24 hours for reasonable session duration)
321
395
  timestamp = timestamp_str.to_i
322
396
  return false if timestamp <= 0
323
397
  return false if (Time.now.to_i - timestamp).abs > 86400 # 24 hours
324
-
398
+
325
399
  # Calculate expected signature
326
400
  message = "simulator:#{timestamp_str}"
327
401
  expected_signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), simulator_secret, message)
328
-
402
+
329
403
  # Use secure comparison
330
404
  secure_compare(signature, expected_signature)
331
405
  rescue => e
@@ -4,24 +4,46 @@ module FlowChat
4
4
  class Executor
5
5
  def initialize(app)
6
6
  @app = app
7
+ FlowChat.logger.debug { "Whatsapp::Executor: Initialized WhatsApp executor middleware" }
7
8
  end
8
9
 
9
10
  def call(context)
11
+ flow_class = context.flow
12
+ action = context["flow.action"]
13
+ session_id = context["session.id"]
14
+
15
+ FlowChat.logger.info { "Whatsapp::Executor: Executing flow #{flow_class.name}##{action} for session #{session_id}" }
16
+
10
17
  whatsapp_app = build_whatsapp_app context
11
- flow = context.flow.new whatsapp_app
12
- flow.send context["flow.action"]
18
+ FlowChat.logger.debug { "Whatsapp::Executor: WhatsApp app built for flow execution" }
19
+
20
+ flow = flow_class.new whatsapp_app
21
+ FlowChat.logger.debug { "Whatsapp::Executor: Flow instance created, invoking #{action} method" }
22
+
23
+ flow.send action
24
+ FlowChat.logger.warn { "Whatsapp::Executor: Flow execution failed to interact with user for #{flow_class.name}##{action}" }
25
+ raise FlowChat::Interrupt::Terminate, "Unexpected end of flow."
13
26
  rescue FlowChat::Interrupt::Prompt => e
27
+ FlowChat.logger.info { "Whatsapp::Executor: Flow prompted user - Session: #{session_id}, Prompt: '#{e.prompt.truncate(100)}'" }
28
+ FlowChat.logger.debug { "Whatsapp::Executor: Prompt details - Choices: #{e.choices&.size || 0}, Has media: #{!e.media.nil?}" }
14
29
  # Return the same triplet format as USSD for consistency
15
30
  [:prompt, e.prompt, e.choices, e.media]
16
31
  rescue FlowChat::Interrupt::Terminate => e
32
+ FlowChat.logger.info { "Whatsapp::Executor: Flow terminated - Session: #{session_id}, Message: '#{e.prompt.truncate(100)}'" }
33
+ FlowChat.logger.debug { "Whatsapp::Executor: Destroying session #{session_id}" }
17
34
  # Clean up session and return terminal message
18
35
  context.session.destroy
19
36
  [:terminate, e.prompt, nil, e.media]
37
+ rescue => error
38
+ FlowChat.logger.error { "Whatsapp::Executor: Flow execution failed - #{flow_class.name}##{action}, Session: #{session_id}, Error: #{error.class.name}: #{error.message}" }
39
+ FlowChat.logger.debug { "Whatsapp::Executor: Stack trace: #{error.backtrace.join("\n")}" }
40
+ raise
20
41
  end
21
42
 
22
43
  private
23
44
 
24
45
  def build_whatsapp_app(context)
46
+ FlowChat.logger.debug { "Whatsapp::Executor: Building WhatsApp app instance" }
25
47
  FlowChat::Whatsapp::App.new(context)
26
48
  end
27
49
  end
@@ -2,6 +2,7 @@ module FlowChat
2
2
  module Whatsapp
3
3
  class Processor < FlowChat::BaseProcessor
4
4
  def use_whatsapp_config(config)
5
+ FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp config: #{config.class.name}" }
5
6
  @whatsapp_config = config
6
7
  self
7
8
  end
@@ -13,13 +14,20 @@ module FlowChat
13
14
  end
14
15
 
15
16
  def build_middleware_stack
17
+ FlowChat.logger.debug { "Whatsapp::Processor: Building WhatsApp middleware stack" }
16
18
  create_middleware_stack("whatsapp")
17
19
  end
18
20
 
19
21
  def configure_middleware_stack(builder)
22
+ FlowChat.logger.debug { "Whatsapp::Processor: Configuring WhatsApp middleware stack" }
20
23
  builder.use FlowChat::Session::Middleware
24
+ FlowChat.logger.debug { "Whatsapp::Processor: Added Session::Middleware" }
25
+
21
26
  builder.use middleware
27
+ FlowChat.logger.debug { "Whatsapp::Processor: Added custom middleware" }
28
+
22
29
  builder.use FlowChat::Whatsapp::Middleware::Executor
30
+ FlowChat.logger.debug { "Whatsapp::Processor: Added Whatsapp::Middleware::Executor" }
23
31
  end
24
32
  end
25
33
  end
@@ -49,15 +49,10 @@ module FlowChat
49
49
  end
50
50
 
51
51
  def build_selection_message
52
- # Determine the best way to present choices
53
- if choices.is_a?(Array)
54
- # Convert array to hash with index-based keys
55
- choice_hash = choices.each_with_index.to_h { |choice, index| [index.to_s, choice] }
56
- build_interactive_message(choice_hash)
57
- elsif choices.is_a?(Hash)
52
+ if choices.is_a?(Hash)
58
53
  build_interactive_message(choices)
59
54
  else
60
- raise ArgumentError, "choices must be an Array or Hash"
55
+ raise ArgumentError, "choices must be a Hash"
61
56
  end
62
57
  end
63
58
 
@@ -121,7 +116,7 @@ module FlowChat
121
116
  }
122
117
  when :video
123
118
  {
124
- type: "video",
119
+ type: "video",
125
120
  video: {link: url}
126
121
  }
127
122
  when :document
@@ -188,4 +183,4 @@ module FlowChat
188
183
  end
189
184
  end
190
185
  end
191
- end
186
+ end
data/lib/flow_chat.rb CHANGED
@@ -2,6 +2,8 @@ require "zeitwerk"
2
2
  require "active_support"
3
3
  require "active_support/core_ext/time"
4
4
  require "active_support/core_ext/object/blank"
5
+ require "active_support/core_ext/string/filters"
6
+ require "active_support/core_ext/enumerable"
5
7
 
6
8
  loader = Zeitwerk::Loader.for_gem
7
9
  loader.enable_reloading if defined?(Rails.env) && Rails.env.development?
@@ -11,6 +13,27 @@ module FlowChat
11
13
  def self.root
12
14
  Pathname.new __dir__
13
15
  end
16
+
17
+ def self.setup_instrumentation!
18
+ require_relative "flow_chat/instrumentation/setup"
19
+ FlowChat::Instrumentation::Setup.setup_instrumentation!
20
+ end
21
+
22
+ # Access to instrumentation
23
+ def self.instrument(event_name, payload = {}, &block)
24
+ FlowChat::Instrumentation.instrument(event_name, payload, &block)
25
+ end
26
+
27
+ def self.metrics
28
+ FlowChat::Instrumentation::Setup.metrics_collector
29
+ end
14
30
  end
15
31
 
16
32
  loader.eager_load
33
+
34
+ # Auto-setup instrumentation in Rails environments
35
+ if defined?(Rails)
36
+ Rails.application.config.after_initialize do
37
+ FlowChat.setup_instrumentation!
38
+ end
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flow_chat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-06-05 00:00:00.000000000 Z
11
+ date: 2025-06-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -80,7 +80,11 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 0.4.2
83
- description: Framework for building Menu based conversations (e.g. USSD) in Rails.
83
+ description: "FlowChat is a Rails framework for building sophisticated conversational
84
+ interfaces across USSD and WhatsApp platforms. \nCreate interactive flows with menus,
85
+ prompts, validation, media support, and session management. Features include \nmulti-tenancy,
86
+ background job processing, built-in simulator for testing, and comprehensive middleware
87
+ support.\n"
84
88
  email:
85
89
  - sfroelich01@gmail.com
86
90
  executables: []
@@ -88,21 +92,24 @@ extensions: []
88
92
  extra_rdoc_files: []
89
93
  files:
90
94
  - ".DS_Store"
95
+ - ".github/workflows/ci.yml"
91
96
  - ".gitignore"
92
97
  - ".ruby-version"
93
- - ".travis.yml"
94
98
  - Gemfile
95
99
  - LICENSE.txt
96
100
  - README.md
97
101
  - Rakefile
98
102
  - SECURITY.md
99
- - app/controllers/demo_controller.rb
100
- - app/flow_chat/demo_restaurant_flow.rb
101
103
  - bin/console
102
104
  - bin/setup
103
- - config/routes_demo.rb
104
- - examples/initializer.rb
105
- - examples/media_prompts_examples.rb
105
+ - docs/configuration.md
106
+ - docs/flows.md
107
+ - docs/images/simulator.png
108
+ - docs/instrumentation.md
109
+ - docs/media.md
110
+ - docs/testing.md
111
+ - docs/ussd-setup.md
112
+ - docs/whatsapp-setup.md
106
113
  - examples/multi_tenant_whatsapp_controller.rb
107
114
  - examples/simulator_controller.rb
108
115
  - examples/ussd_controller.rb
@@ -110,12 +117,15 @@ files:
110
117
  - examples/whatsapp_media_examples.rb
111
118
  - examples/whatsapp_message_job.rb
112
119
  - flow_chat.gemspec
113
- - images/ussd_simulator.png
114
120
  - lib/flow_chat.rb
115
121
  - lib/flow_chat/base_processor.rb
116
122
  - lib/flow_chat/config.rb
117
123
  - lib/flow_chat/context.rb
118
124
  - lib/flow_chat/flow.rb
125
+ - lib/flow_chat/instrumentation.rb
126
+ - lib/flow_chat/instrumentation/log_subscriber.rb
127
+ - lib/flow_chat/instrumentation/metrics_collector.rb
128
+ - lib/flow_chat/instrumentation/setup.rb
119
129
  - lib/flow_chat/interrupt.rb
120
130
  - lib/flow_chat/prompt.rb
121
131
  - lib/flow_chat/session/cache_session_store.rb
@@ -126,6 +136,7 @@ files:
126
136
  - lib/flow_chat/ussd/app.rb
127
137
  - lib/flow_chat/ussd/gateway/nalo.rb
128
138
  - lib/flow_chat/ussd/gateway/nsano.rb
139
+ - lib/flow_chat/ussd/middleware/choice_mapper.rb
129
140
  - lib/flow_chat/ussd/middleware/executor.rb
130
141
  - lib/flow_chat/ussd/middleware/pagination.rb
131
142
  - lib/flow_chat/ussd/middleware/resumable_session.rb
@@ -167,5 +178,5 @@ requirements: []
167
178
  rubygems_version: 3.4.10
168
179
  signing_key:
169
180
  specification_version: 4
170
- summary: Framework for building Menu based conversations (e.g. USSD) in Rails.
181
+ summary: Build conversational interfaces for USSD and WhatsApp with Rails
171
182
  test_files: []
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.7.1
6
- before_install: gem install bundler -v 2.1.4