flow_chat 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/docs/configuration.md +29 -6
- data/docs/sessions.md +433 -0
- data/docs/testing.md +2 -2
- data/docs/ussd-setup.md +20 -4
- data/examples/simulator_controller.rb +4 -4
- data/examples/ussd_controller.rb +9 -3
- data/lib/flow_chat/base_app.rb +74 -0
- data/lib/flow_chat/base_executor.rb +57 -0
- data/lib/flow_chat/base_processor.rb +33 -2
- data/lib/flow_chat/config.rb +26 -5
- data/lib/flow_chat/instrumentation/setup.rb +0 -2
- data/lib/flow_chat/interrupt.rb +6 -0
- data/lib/flow_chat/session/middleware.rb +102 -17
- data/lib/flow_chat/simulator/controller.rb +2 -2
- data/lib/flow_chat/simulator/views/simulator.html.erb +5 -5
- data/lib/flow_chat/ussd/app.rb +1 -53
- data/lib/flow_chat/ussd/gateway/nalo.rb +1 -0
- data/lib/flow_chat/ussd/gateway/nsano.rb +3 -2
- data/lib/flow_chat/ussd/middleware/executor.rb +11 -37
- data/lib/flow_chat/ussd/processor.rb +5 -7
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/app.rb +11 -46
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +1 -0
- data/lib/flow_chat/whatsapp/middleware/executor.rb +11 -39
- data/lib/flow_chat/whatsapp/processor.rb +0 -2
- data/lib/flow_chat.rb +1 -11
- metadata +5 -3
- data/lib/flow_chat/ussd/middleware/resumable_session.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: abe12ed32427797b8d7c5dc79766bae76113ec62df415e495f900e4216af5c3f
|
4
|
+
data.tar.gz: d344431fbde29789a0013a78dd099b253b65dc9e8af90ef2d403a5592b5bb7aa
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b05a61d0e7a41db5ecd2137aaa193c7431f1e16277e093af4eacbcf657696aa42a22d177407718618959b021298a3583101004534d3666a51b2607b4f621db88
|
7
|
+
data.tar.gz: a9aab01a2754996205c2b9c1fa8143cdf503f4033c243ed537120c975249eebc0b0cc42f310864a8923b8fd819cc625205cd32463f15e5db93ccd88c2c4ecfe1
|
data/README.md
CHANGED
@@ -186,6 +186,7 @@ See the [Testing Guide](docs/testing.md) for complete setup instructions and tes
|
|
186
186
|
- **[WhatsApp Setup](docs/whatsapp-setup.md)** - Comprehensive WhatsApp configuration
|
187
187
|
- **[USSD Setup](docs/ussd-setup.md)** - USSD gateway configuration and examples
|
188
188
|
- **[Flow Development](docs/flows.md)** - Advanced flow patterns and techniques
|
189
|
+
- **[Session Management](docs/sessions.md)** - Session architecture and configuration
|
189
190
|
- **[Media Support](docs/media.md)** - Rich media handling for WhatsApp
|
190
191
|
- **[Testing Guide](docs/testing.md)** - Complete testing strategies
|
191
192
|
- **[Configuration Reference](docs/configuration.md)** - All configuration options
|
data/docs/configuration.md
CHANGED
@@ -19,6 +19,26 @@ FlowChat::Config.combine_validation_error_with_message = true # default
|
|
19
19
|
FlowChat.setup_instrumentation!
|
20
20
|
```
|
21
21
|
|
22
|
+
## Session Configuration
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
# Session boundaries control how session IDs are constructed
|
26
|
+
FlowChat::Config.session.boundaries = [:flow, :platform] # default
|
27
|
+
FlowChat::Config.session.hash_phone_numbers = true # hash phone numbers for privacy
|
28
|
+
FlowChat::Config.session.identifier = nil # let platforms choose (default)
|
29
|
+
|
30
|
+
# Available boundary options:
|
31
|
+
# :flow - separate sessions per flow class
|
32
|
+
# :platform - separate sessions per platform (ussd, whatsapp)
|
33
|
+
# :gateway - separate sessions per gateway
|
34
|
+
# [] - global sessions (no boundaries)
|
35
|
+
|
36
|
+
# Available identifier options:
|
37
|
+
# nil - platform chooses default (:request_id for USSD, :msisdn for WhatsApp)
|
38
|
+
# :msisdn - use phone number (durable sessions)
|
39
|
+
# :request_id - use request ID (ephemeral sessions)
|
40
|
+
```
|
41
|
+
|
22
42
|
## USSD Configuration
|
23
43
|
|
24
44
|
```ruby
|
@@ -28,10 +48,6 @@ FlowChat::Config.ussd.pagination_next_option = "#" # option to go to next
|
|
28
48
|
FlowChat::Config.ussd.pagination_next_text = "More" # text for next option
|
29
49
|
FlowChat::Config.ussd.pagination_back_option = "0" # option to go back
|
30
50
|
FlowChat::Config.ussd.pagination_back_text = "Back" # text for back option
|
31
|
-
|
32
|
-
# Resumable sessions
|
33
|
-
FlowChat::Config.ussd.resumable_sessions_enabled = true # default
|
34
|
-
FlowChat::Config.ussd.resumable_sessions_timeout_seconds = 300 # 5 minutes
|
35
51
|
```
|
36
52
|
|
37
53
|
## WhatsApp Configuration
|
@@ -174,8 +190,15 @@ processor = FlowChat::Ussd::Processor.new(self) do |config|
|
|
174
190
|
# Optional middleware
|
175
191
|
config.use_middleware MyCustomMiddleware
|
176
192
|
|
177
|
-
#
|
178
|
-
config.
|
193
|
+
# Configure session boundaries
|
194
|
+
config.use_session_config(
|
195
|
+
boundaries: [:flow, :platform], # which boundaries to enforce
|
196
|
+
hash_phone_numbers: true, # hash phone numbers for privacy
|
197
|
+
identifier: :msisdn # use MSISDN for durable sessions (optional)
|
198
|
+
)
|
199
|
+
|
200
|
+
# Shorthand for durable sessions (identifier: :msisdn)
|
201
|
+
config.use_durable_sessions
|
179
202
|
end
|
180
203
|
```
|
181
204
|
|
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
|
+
```
|
data/docs/testing.md
CHANGED
@@ -43,7 +43,7 @@ class SimulatorController < ApplicationController
|
|
43
43
|
name: "USSD Integration",
|
44
44
|
icon: "📱",
|
45
45
|
processor_type: "ussd",
|
46
|
-
|
46
|
+
gateway: "nalo",
|
47
47
|
endpoint: "/ussd",
|
48
48
|
color: "#007bff"
|
49
49
|
},
|
@@ -51,7 +51,7 @@ class SimulatorController < ApplicationController
|
|
51
51
|
name: "WhatsApp Integration",
|
52
52
|
icon: "💬",
|
53
53
|
processor_type: "whatsapp",
|
54
|
-
|
54
|
+
gateway: "cloud_api",
|
55
55
|
endpoint: "/whatsapp/webhook",
|
56
56
|
color: "#25D366"
|
57
57
|
}
|
data/docs/ussd-setup.md
CHANGED
@@ -119,19 +119,35 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor i
|
|
119
119
|
0 Back
|
120
120
|
```
|
121
121
|
|
122
|
-
##
|
122
|
+
## Session Management
|
123
123
|
|
124
|
-
|
124
|
+
Configure session behavior for better user experience:
|
125
125
|
|
126
126
|
```ruby
|
127
127
|
processor = FlowChat::Ussd::Processor.new(self) do |config|
|
128
128
|
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
129
129
|
config.use_session_store FlowChat::Session::CacheSessionStore
|
130
|
-
|
130
|
+
|
131
|
+
# Enable durable sessions (shorthand)
|
132
|
+
config.use_durable_sessions
|
131
133
|
end
|
132
134
|
```
|
133
135
|
|
134
|
-
|
136
|
+
### Session Boundaries
|
137
|
+
|
138
|
+
Session boundaries control how session IDs are constructed:
|
139
|
+
|
140
|
+
- **`:flow`** - Separate sessions per flow class
|
141
|
+
- **`:platform`** - Separate USSD from WhatsApp sessions
|
142
|
+
- **`:gateway`** - Separate sessions per gateway
|
143
|
+
- **`[]`** - Global sessions (no boundaries)
|
144
|
+
|
145
|
+
Session identifier options:
|
146
|
+
|
147
|
+
- **`nil`** - Platform chooses default (`:request_id` for USSD, `:msisdn` for WhatsApp)
|
148
|
+
- **`:msisdn`** - Use phone number (durable sessions)
|
149
|
+
- **`:request_id`** - Use request ID (ephemeral sessions)
|
150
|
+
- **`hash_phone_numbers`** - Hash phone numbers for privacy (recommended)
|
135
151
|
|
136
152
|
## Middleware
|
137
153
|
|
@@ -17,7 +17,7 @@ class SimulatorController < ApplicationController
|
|
17
17
|
name: "Main USSD Endpoint",
|
18
18
|
description: "Primary USSD integration",
|
19
19
|
processor_type: "ussd",
|
20
|
-
|
20
|
+
gateway: "nalo",
|
21
21
|
endpoint: "/ussd",
|
22
22
|
icon: "📱",
|
23
23
|
color: "#28a745"
|
@@ -26,7 +26,7 @@ class SimulatorController < ApplicationController
|
|
26
26
|
name: "Main WhatsApp Endpoint",
|
27
27
|
description: "Primary WhatsApp webhook",
|
28
28
|
processor_type: "whatsapp",
|
29
|
-
|
29
|
+
gateway: "cloud_api",
|
30
30
|
endpoint: "/whatsapp/webhook",
|
31
31
|
icon: "💬",
|
32
32
|
color: "#25D366"
|
@@ -35,7 +35,7 @@ class SimulatorController < ApplicationController
|
|
35
35
|
name: "Tenant A WhatsApp",
|
36
36
|
description: "Multi-tenant endpoint for Tenant A",
|
37
37
|
processor_type: "whatsapp",
|
38
|
-
|
38
|
+
gateway: "cloud_api",
|
39
39
|
endpoint: "/tenants/a/whatsapp/webhook",
|
40
40
|
icon: "🏢",
|
41
41
|
color: "#fd7e14"
|
@@ -44,7 +44,7 @@ class SimulatorController < ApplicationController
|
|
44
44
|
name: "Legacy WhatsApp",
|
45
45
|
description: "Legacy endpoint for compatibility",
|
46
46
|
processor_type: "whatsapp",
|
47
|
-
|
47
|
+
gateway: "cloud_api",
|
48
48
|
endpoint: "/legacy/whatsapp",
|
49
49
|
icon: "📦",
|
50
50
|
color: "#6c757d"
|
data/examples/ussd_controller.rb
CHANGED
@@ -10,8 +10,8 @@ class UssdController < ApplicationController
|
|
10
10
|
# Use Rails session for USSD (shorter sessions)
|
11
11
|
config.use_session_store FlowChat::Session::RailsSessionStore
|
12
12
|
|
13
|
-
# Enable
|
14
|
-
config.
|
13
|
+
# Enable durable sessions (optional)
|
14
|
+
config.use_durable_sessions # Configures flow+platform isolation with durable sessions
|
15
15
|
end
|
16
16
|
|
17
17
|
processor.run WelcomeFlow, :main_page
|
@@ -232,7 +232,13 @@ class UssdController < ApplicationController
|
|
232
232
|
config.use_gateway FlowChat::Ussd::Gateway::Nalo
|
233
233
|
config.use_session_store FlowChat::Session::RailsSessionStore
|
234
234
|
config.use_middleware LoggingMiddleware # Add custom logging
|
235
|
-
config.
|
235
|
+
config.use_durable_sessions # Enable durable sessions
|
236
|
+
|
237
|
+
# Or configure session boundaries explicitly:
|
238
|
+
# config.use_session_config(
|
239
|
+
# boundaries: [:flow, :platform], # which boundaries to enforce
|
240
|
+
# hash_phone_numbers: true # hash phone numbers for privacy
|
241
|
+
# )
|
236
242
|
end
|
237
243
|
|
238
244
|
processor.run WelcomeFlow, :main_page
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module FlowChat
|
2
|
+
class BaseApp
|
3
|
+
attr_reader :session, :input, :context, :navigation_stack
|
4
|
+
|
5
|
+
def initialize(context)
|
6
|
+
@context = context
|
7
|
+
@session = context.session
|
8
|
+
@input = context.input
|
9
|
+
@navigation_stack = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def screen(key)
|
13
|
+
raise ArgumentError, "a block is expected" unless block_given?
|
14
|
+
raise ArgumentError, "screen has already been presented" if navigation_stack.include?(key)
|
15
|
+
|
16
|
+
navigation_stack << key
|
17
|
+
return session.get(key) if session.get(key).present?
|
18
|
+
|
19
|
+
user_input = prepare_user_input
|
20
|
+
prompt = FlowChat::Prompt.new user_input
|
21
|
+
@input = nil # input is being submitted to prompt so we clear it
|
22
|
+
|
23
|
+
value = yield prompt
|
24
|
+
session.set(key, value)
|
25
|
+
value
|
26
|
+
end
|
27
|
+
|
28
|
+
def go_back
|
29
|
+
return false if navigation_stack.empty?
|
30
|
+
|
31
|
+
@context.input = nil
|
32
|
+
current_screen = navigation_stack.last
|
33
|
+
session.delete(current_screen)
|
34
|
+
|
35
|
+
# Restart the flow from the beginning
|
36
|
+
raise FlowChat::Interrupt::RestartFlow.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def say(msg, media: nil)
|
40
|
+
raise FlowChat::Interrupt::Terminate.new(msg, media: media)
|
41
|
+
end
|
42
|
+
|
43
|
+
def phone_number
|
44
|
+
context["request.msisdn"]
|
45
|
+
end
|
46
|
+
|
47
|
+
def message_id
|
48
|
+
context["request.message_id"]
|
49
|
+
end
|
50
|
+
|
51
|
+
def timestamp
|
52
|
+
context["request.timestamp"]
|
53
|
+
end
|
54
|
+
|
55
|
+
def contact_name
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
59
|
+
def location
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def media
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
# Platform-specific methods to be overridden
|
70
|
+
def prepare_user_input
|
71
|
+
input
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|