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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +44 -0
- data/.gitignore +2 -1
- data/README.md +85 -1229
- data/docs/configuration.md +360 -0
- data/docs/flows.md +320 -0
- data/docs/images/simulator.png +0 -0
- data/docs/instrumentation.md +216 -0
- data/docs/media.md +153 -0
- data/docs/sessions.md +433 -0
- data/docs/testing.md +475 -0
- data/docs/ussd-setup.md +322 -0
- data/docs/whatsapp-setup.md +162 -0
- data/examples/multi_tenant_whatsapp_controller.rb +9 -37
- data/examples/simulator_controller.rb +13 -22
- data/examples/ussd_controller.rb +41 -41
- data/examples/whatsapp_controller.rb +32 -125
- data/examples/whatsapp_media_examples.rb +68 -336
- data/examples/whatsapp_message_job.rb +5 -3
- data/flow_chat.gemspec +6 -2
- data/lib/flow_chat/base_processor.rb +79 -2
- data/lib/flow_chat/config.rb +31 -5
- data/lib/flow_chat/context.rb +13 -1
- data/lib/flow_chat/instrumentation/log_subscriber.rb +176 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +197 -0
- data/lib/flow_chat/instrumentation/setup.rb +155 -0
- data/lib/flow_chat/instrumentation.rb +70 -0
- data/lib/flow_chat/prompt.rb +20 -20
- data/lib/flow_chat/session/cache_session_store.rb +73 -7
- data/lib/flow_chat/session/middleware.rb +130 -12
- data/lib/flow_chat/session/rails_session_store.rb +36 -1
- data/lib/flow_chat/simulator/controller.rb +8 -8
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/gateway/nalo.rb +31 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +36 -2
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +109 -0
- data/lib/flow_chat/ussd/middleware/executor.rb +24 -2
- data/lib/flow_chat/ussd/middleware/pagination.rb +87 -7
- data/lib/flow_chat/ussd/processor.rb +16 -4
- data/lib/flow_chat/ussd/renderer.rb +1 -1
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +99 -12
- data/lib/flow_chat/whatsapp/configuration.rb +35 -4
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +121 -34
- data/lib/flow_chat/whatsapp/middleware/executor.rb +24 -2
- data/lib/flow_chat/whatsapp/processor.rb +7 -1
- data/lib/flow_chat/whatsapp/renderer.rb +4 -9
- data/lib/flow_chat.rb +23 -0
- metadata +23 -12
- data/.travis.yml +0 -6
- data/app/controllers/demo_controller.rb +0 -101
- data/app/flow_chat/demo_restaurant_flow.rb +0 -889
- data/config/routes_demo.rb +0 -59
- data/examples/initializer.rb +0 -86
- data/examples/media_prompts_examples.rb +0 -27
- data/images/ussd_simulator.png +0 -0
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
data/docs/sessions.md
ADDED
@@ -0,0 +1,433 @@
|
|
1
|
+
# Session Management
|
2
|
+
|
3
|
+
FlowChat provides a powerful and flexible session management system that enables persistent conversational state across multiple requests. This document covers the architecture, configuration, and best practices for session management.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Sessions in FlowChat store user conversation state between requests, enabling:
|
8
|
+
|
9
|
+
- **Multi-step workflows** - Collect information across multiple prompts
|
10
|
+
- **Context preservation** - Remember user inputs and conversation history
|
11
|
+
- **Cross-platform consistency** - Same session behavior across USSD and WhatsApp
|
12
|
+
- **Privacy protection** - Automatic phone number hashing for security
|
13
|
+
- **Flexible isolation** - Control session boundaries per deployment needs
|
14
|
+
|
15
|
+
## Architecture
|
16
|
+
|
17
|
+
The session system is built around three core components:
|
18
|
+
|
19
|
+
### 1. Session Configuration (`FlowChat::Config::SessionConfig`)
|
20
|
+
|
21
|
+
Controls how session IDs are generated and sessions are isolated:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
# Global session configuration
|
25
|
+
FlowChat::Config.session.boundaries = [:flow, :gateway, :platform] # isolation boundaries
|
26
|
+
FlowChat::Config.session.hash_phone_numbers = true # privacy protection
|
27
|
+
FlowChat::Config.session.identifier = nil # platform chooses default
|
28
|
+
```
|
29
|
+
|
30
|
+
### 2. Session Middleware (`FlowChat::Session::Middleware`)
|
31
|
+
|
32
|
+
Automatically manages session creation and ID generation based on configuration:
|
33
|
+
|
34
|
+
- Generates consistent session IDs based on boundaries and identifiers
|
35
|
+
- Creates session store instances for each request
|
36
|
+
- Handles platform-specific defaults (USSD = ephemeral, WhatsApp = durable)
|
37
|
+
- Provides instrumentation events for monitoring
|
38
|
+
|
39
|
+
### 3. Session Stores
|
40
|
+
|
41
|
+
Store actual session data with different persistence strategies:
|
42
|
+
|
43
|
+
- **`FlowChat::Session::CacheSessionStore`** - Uses Rails cache (recommended)
|
44
|
+
- **`FlowChat::Session::RailsSessionStore`** - Uses Rails session (limited)
|
45
|
+
|
46
|
+
## Session Boundaries
|
47
|
+
|
48
|
+
Boundaries control how session IDs are constructed, determining when sessions are shared vs. isolated:
|
49
|
+
|
50
|
+
### Available Boundaries
|
51
|
+
|
52
|
+
- **`:flow`** - Separate sessions per flow class
|
53
|
+
- **`:platform`** - Separate sessions per platform (ussd, whatsapp)
|
54
|
+
- **`:gateway`** - Separate sessions per gateway
|
55
|
+
- **`:url`** - Separate sessions per request URL (host + path)
|
56
|
+
- **`[]`** - Global sessions (no boundaries)
|
57
|
+
|
58
|
+
### Examples
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Default: Full isolation
|
62
|
+
FlowChat::Config.session.boundaries = [:flow, :gateway, :platform]
|
63
|
+
# Session ID: "registration_flow:nalo:ussd:abc123"
|
64
|
+
|
65
|
+
# Flow isolation only
|
66
|
+
FlowChat::Config.session.boundaries = [:flow]
|
67
|
+
# Session ID: "registration_flow:abc123"
|
68
|
+
|
69
|
+
# Platform isolation only
|
70
|
+
FlowChat::Config.session.boundaries = [:platform]
|
71
|
+
# Session ID: "ussd:abc123"
|
72
|
+
|
73
|
+
# Global sessions
|
74
|
+
FlowChat::Config.session.boundaries = []
|
75
|
+
# Session ID: "abc123"
|
76
|
+
```
|
77
|
+
|
78
|
+
## Session Identifiers
|
79
|
+
|
80
|
+
Identifiers determine what makes a session unique:
|
81
|
+
|
82
|
+
### Available Identifiers
|
83
|
+
|
84
|
+
- **`nil`** - Platform chooses default (recommended)
|
85
|
+
- USSD: `:request_id` (ephemeral sessions)
|
86
|
+
- WhatsApp: `:msisdn` (durable sessions)
|
87
|
+
- **`:msisdn`** - Use phone number (durable sessions)
|
88
|
+
- **`:request_id`** - Use request ID (ephemeral sessions)
|
89
|
+
|
90
|
+
### Platform Defaults
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
# USSD: ephemeral sessions by default
|
94
|
+
identifier: :request_id
|
95
|
+
# New session each time USSD times out
|
96
|
+
|
97
|
+
# WhatsApp: durable sessions by default
|
98
|
+
identifier: :msisdn
|
99
|
+
# Same session resumes across conversations
|
100
|
+
```
|
101
|
+
|
102
|
+
### Phone Number Hashing
|
103
|
+
|
104
|
+
When using `:msisdn` identifier, phone numbers are automatically hashed for privacy:
|
105
|
+
|
106
|
+
```ruby
|
107
|
+
FlowChat::Config.session.hash_phone_numbers = true # default
|
108
|
+
# "+256700123456" becomes "a1b2c3d4" (8-character hash)
|
109
|
+
|
110
|
+
FlowChat::Config.session.hash_phone_numbers = false
|
111
|
+
# "+256700123456" used directly (not recommended for production)
|
112
|
+
```
|
113
|
+
|
114
|
+
## Configuration Examples
|
115
|
+
|
116
|
+
### Basic USSD Configuration
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
120
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
121
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
122
|
+
|
123
|
+
# Use shorthand for standard durable sessions
|
124
|
+
config.use_durable_sessions
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### Custom Session Configuration
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
132
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
133
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
134
|
+
|
135
|
+
# Explicit session configuration
|
136
|
+
config.use_session_config(
|
137
|
+
boundaries: [:flow, :platform], # isolate by flow and platform
|
138
|
+
hash_phone_numbers: true, # hash phone numbers for privacy
|
139
|
+
identifier: :msisdn # use phone number for durable sessions
|
140
|
+
)
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
### Cross-gateway Sessions
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
148
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
149
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
150
|
+
|
151
|
+
# Allow sessions to work across different gateways
|
152
|
+
config.use_session_config(boundaries: [:flow, :platform])
|
153
|
+
end
|
154
|
+
```
|
155
|
+
|
156
|
+
### Cross-Platform Sessions
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
160
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
161
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
162
|
+
|
163
|
+
# Allow same user to continue on USSD or WhatsApp
|
164
|
+
config.use_cross_platform_sessions
|
165
|
+
# Equivalent to: boundaries: [:flow]
|
166
|
+
end
|
167
|
+
```
|
168
|
+
|
169
|
+
### URL-Based Session Isolation
|
170
|
+
|
171
|
+
Perfect for multi-tenant applications:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
175
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
176
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
177
|
+
|
178
|
+
# Isolate sessions by URL (great for multi-tenant SaaS)
|
179
|
+
config.use_url_isolation
|
180
|
+
# Adds :url to existing boundaries
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
**URL Boundary Examples:**
|
185
|
+
- `tenant1.example.com/ussd` vs `tenant2.example.com/ussd` - Different sessions
|
186
|
+
- `api.example.com/v1/ussd` vs `api.example.com/v2/ussd` - Different sessions
|
187
|
+
- `dev.example.com/ussd` vs `prod.example.com/ussd` - Different sessions
|
188
|
+
|
189
|
+
**URL Processing:**
|
190
|
+
- Combines host + path: `example.com/api/v1/ussd`
|
191
|
+
- Sanitizes special characters: `tenant-1.com/ussd` → `tenant_1.com/ussd`
|
192
|
+
- Hashes long URLs (>50 chars): `verylongdomain.../path` → `url_a1b2c3d4`
|
193
|
+
|
194
|
+
### Global Sessions
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
198
|
+
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
199
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
200
|
+
|
201
|
+
# Single session shared across everything
|
202
|
+
config.use_session_config(boundaries: [])
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
206
|
+
## Session Stores
|
207
|
+
|
208
|
+
### Cache Session Store (Recommended)
|
209
|
+
|
210
|
+
Uses Rails cache backend with automatic TTL management:
|
211
|
+
|
212
|
+
```ruby
|
213
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
214
|
+
|
215
|
+
# Requires Rails cache to be configured
|
216
|
+
# config/environments/production.rb
|
217
|
+
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
218
|
+
```
|
219
|
+
|
220
|
+
**Features:**
|
221
|
+
- Automatic session expiration via cache TTL
|
222
|
+
- Redis/Memcached support for distributed deployments
|
223
|
+
- High performance
|
224
|
+
- Memory efficient
|
225
|
+
|
226
|
+
### Rails Session Store
|
227
|
+
|
228
|
+
Uses Rails session for storage (limited scope):
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
config.use_session_store FlowChat::Session::RailsSessionStore
|
232
|
+
```
|
233
|
+
|
234
|
+
**Limitations:**
|
235
|
+
- Tied to HTTP session lifecycle
|
236
|
+
- Limited storage capacity
|
237
|
+
- Not suitable for long-running conversations
|
238
|
+
|
239
|
+
## Session Data Usage
|
240
|
+
|
241
|
+
### In Flows
|
242
|
+
|
243
|
+
Sessions are automatically available in flows:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class RegistrationFlow < FlowChat::Flow
|
247
|
+
def main_page
|
248
|
+
# Store data
|
249
|
+
app.session.set("step", "registration")
|
250
|
+
app.session.set("user_data", {name: "John", age: 25})
|
251
|
+
|
252
|
+
# Retrieve data
|
253
|
+
step = app.session.get("step")
|
254
|
+
user_data = app.session.get("user_data")
|
255
|
+
|
256
|
+
# Check existence
|
257
|
+
if app.session.get("completed")
|
258
|
+
app.say "Registration already completed!"
|
259
|
+
return
|
260
|
+
end
|
261
|
+
|
262
|
+
# Continue flow...
|
263
|
+
name = app.screen(:name) { |prompt| prompt.ask "Name?" }
|
264
|
+
app.session.set("name", name)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
```
|
268
|
+
|
269
|
+
### Session Store API
|
270
|
+
|
271
|
+
All session stores implement a consistent interface:
|
272
|
+
|
273
|
+
```ruby
|
274
|
+
# Basic operations
|
275
|
+
session.set(key, value) # Store data
|
276
|
+
value = session.get(key) # Retrieve data
|
277
|
+
session.delete(key) # Delete specific key
|
278
|
+
session.clear # Clear all session data
|
279
|
+
session.destroy # Destroy entire session
|
280
|
+
|
281
|
+
# Utility methods
|
282
|
+
session.exists? # Check if session has any data
|
283
|
+
```
|
284
|
+
|
285
|
+
## Best Practices
|
286
|
+
|
287
|
+
### 1. Choose Appropriate Boundaries
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
# High-traffic public services
|
291
|
+
boundaries: [:flow, :gateway, :platform] # Full isolation
|
292
|
+
|
293
|
+
# Single-tenant applications
|
294
|
+
boundaries: [:flow] # Simpler, allows cross-platform
|
295
|
+
|
296
|
+
# Global state services (rare)
|
297
|
+
boundaries: [] # Shared state across everything
|
298
|
+
```
|
299
|
+
|
300
|
+
### 2. Consider Session Lifecycle
|
301
|
+
|
302
|
+
```ruby
|
303
|
+
# USSD: Short sessions, frequent timeouts
|
304
|
+
identifier: :request_id # New session each timeout (default)
|
305
|
+
|
306
|
+
# WhatsApp: Long conversations, persistent
|
307
|
+
identifier: :msisdn # Resume across days/weeks (default)
|
308
|
+
|
309
|
+
# Custom requirements
|
310
|
+
identifier: :msisdn # Make USSD durable
|
311
|
+
identifier: :request_id # Make WhatsApp ephemeral
|
312
|
+
```
|
313
|
+
|
314
|
+
### 3. Handle Session Expiration
|
315
|
+
|
316
|
+
```ruby
|
317
|
+
class RegistrationFlow < FlowChat::Flow
|
318
|
+
def main_page
|
319
|
+
# Check if session expired
|
320
|
+
if app.session.get("user_id").nil?
|
321
|
+
restart_registration
|
322
|
+
return
|
323
|
+
end
|
324
|
+
|
325
|
+
# Continue existing session
|
326
|
+
continue_registration
|
327
|
+
end
|
328
|
+
|
329
|
+
private
|
330
|
+
|
331
|
+
def restart_registration
|
332
|
+
app.say "Session expired. Let's start over."
|
333
|
+
# Reset flow to beginning
|
334
|
+
end
|
335
|
+
end
|
336
|
+
```
|
337
|
+
|
338
|
+
### 4. Optimize Session Data
|
339
|
+
|
340
|
+
```ruby
|
341
|
+
# Store only necessary data
|
342
|
+
app.session.set("user_id", 123) # Good: minimal data
|
343
|
+
app.session.set("user", user_object) # Avoid: large objects
|
344
|
+
|
345
|
+
# Clean up when done
|
346
|
+
def complete_registration
|
347
|
+
app.session.set("completed", true)
|
348
|
+
app.session.delete("temp_data") # Clean up temporary data
|
349
|
+
end
|
350
|
+
```
|
351
|
+
|
352
|
+
### 5. Security Considerations
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
# Always hash phone numbers in production
|
356
|
+
FlowChat::Config.session.hash_phone_numbers = true
|
357
|
+
|
358
|
+
# Use secure cache backends
|
359
|
+
config.cache_store = :redis_cache_store, {
|
360
|
+
url: ENV['REDIS_URL'],
|
361
|
+
ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
|
362
|
+
}
|
363
|
+
|
364
|
+
# Set appropriate TTLs
|
365
|
+
cache_options = { expires_in: 30.minutes } # Reasonable session timeout
|
366
|
+
```
|
367
|
+
|
368
|
+
## Troubleshooting
|
369
|
+
|
370
|
+
### Session Not Persisting
|
371
|
+
|
372
|
+
1. **Check session store configuration:**
|
373
|
+
```ruby
|
374
|
+
# Ensure session store is configured
|
375
|
+
config.use_session_store FlowChat::Session::CacheSessionStore
|
376
|
+
```
|
377
|
+
|
378
|
+
2. **Verify cache backend:**
|
379
|
+
```ruby
|
380
|
+
# Test cache is working
|
381
|
+
Rails.cache.write("test", "value")
|
382
|
+
puts Rails.cache.read("test") # Should output "value"
|
383
|
+
```
|
384
|
+
|
385
|
+
3. **Check session boundaries:**
|
386
|
+
```ruby
|
387
|
+
# Debug session ID generation
|
388
|
+
FlowChat.logger.level = Logger::DEBUG
|
389
|
+
# Look for "Session::Middleware: Generated session ID: ..." messages
|
390
|
+
```
|
391
|
+
|
392
|
+
### Different Session IDs
|
393
|
+
|
394
|
+
1. **Inconsistent request data:**
|
395
|
+
- Verify `request.gateway` is consistent
|
396
|
+
- Check `request.platform` is set correctly
|
397
|
+
- Ensure `request.msisdn` format is consistent
|
398
|
+
|
399
|
+
2. **Boundary configuration mismatch:**
|
400
|
+
```ruby
|
401
|
+
# Ensure same boundaries across requests
|
402
|
+
config.use_session_config(boundaries: [:flow, :platform])
|
403
|
+
```
|
404
|
+
|
405
|
+
### Session Data Lost
|
406
|
+
|
407
|
+
1. **Cache expiration:**
|
408
|
+
```ruby
|
409
|
+
# Increase TTL if needed
|
410
|
+
FlowChat::Config.cache = Rails.cache
|
411
|
+
# Configure longer expiration in cache store
|
412
|
+
```
|
413
|
+
|
414
|
+
2. **Session ID changes:**
|
415
|
+
- Check logs for "Generated session ID" messages
|
416
|
+
- Verify identifier consistency (`:msisdn` vs `:request_id`)
|
417
|
+
|
418
|
+
## Monitoring and Instrumentation
|
419
|
+
|
420
|
+
FlowChat emits events for session operations:
|
421
|
+
|
422
|
+
```ruby
|
423
|
+
# Subscribe to session events
|
424
|
+
ActiveSupport::Notifications.subscribe("session.created.flow_chat") do |event|
|
425
|
+
Rails.logger.info "Session created: #{event.payload[:session_id]}"
|
426
|
+
end
|
427
|
+
|
428
|
+
# Monitor session usage
|
429
|
+
ActiveSupport::Notifications.subscribe(/session\..*\.flow_chat/) do |name, start, finish, id, payload|
|
430
|
+
# Track session operations for analytics
|
431
|
+
Analytics.track("flowchat.session.#{name.split('.')[1]}", payload)
|
432
|
+
end
|
433
|
+
```
|