flow_chat 0.9.0 → 0.10.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/.github/workflows/ci.yml +29 -0
- data/.github/workflows/pages.yml +43 -0
- data/.gitignore +4 -1
- data/CHANGELOG.md +49 -0
- data/README.md +166 -295
- data/Rakefile +12 -1
- data/SECURITY.md +1 -1
- data/docs/architecture.md +52 -479
- data/docs/async-background-processing.md +31 -265
- data/docs/configuration.md +106 -613
- data/docs/factory-pattern.md +27 -324
- data/docs/gateway-context-variables.md +140 -143
- data/docs/gateway-development.md +86 -650
- data/docs/getting-started.md +40 -379
- data/docs/instrumentation.md +88 -279
- data/docs/platforms/instagram.md +278 -0
- data/docs/platforms/messenger.md +205 -0
- data/docs/platforms/telegram.md +47 -951
- data/docs/platforms/ussd.md +38 -653
- data/docs/platforms/whatsapp.md +73 -1321
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
- data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
- data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
- data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
- data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
- data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
- data/docs/testing.md +26 -297
- data/examples/http_controller.rb +12 -10
- data/examples/intercom_configuration_example.rb +19 -24
- data/examples/intercom_controller.rb +8 -20
- data/examples/multi_tenant_whatsapp_controller.rb +61 -166
- data/examples/simulator_controller.rb +0 -1
- data/examples/ussd_controller.rb +86 -158
- data/examples/whatsapp_controller.rb +16 -15
- data/examples/whatsapp_media_examples.rb +27 -79
- data/flow_chat.gemspec +22 -2
- data/gemfiles/rails_main.gemfile +18 -0
- data/lib/flow_chat/app.rb +129 -11
- data/lib/flow_chat/async_job.rb +10 -0
- data/lib/flow_chat/choice_titles.rb +95 -0
- data/lib/flow_chat/config.rb +124 -1
- data/lib/flow_chat/delivery_error.rb +9 -0
- data/lib/flow_chat/gateway_async_support.rb +19 -1
- data/lib/flow_chat/http/configuration_error.rb +9 -0
- data/lib/flow_chat/http/gateway/simple.rb +24 -4
- data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
- data/lib/flow_chat/input.rb +86 -0
- data/lib/flow_chat/instagram/client.rb +32 -0
- data/lib/flow_chat/instagram/configuration.rb +147 -0
- data/lib/flow_chat/instagram/configuration_error.rb +7 -0
- data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
- data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
- data/lib/flow_chat/instagram/renderer.rb +23 -0
- data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
- data/lib/flow_chat/instrumentation.rb +160 -1
- data/lib/flow_chat/intercom/client.rb +34 -28
- data/lib/flow_chat/intercom/configuration.rb +2 -49
- data/lib/flow_chat/intercom/configuration_error.rb +9 -0
- data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
- data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
- data/lib/flow_chat/intercom/renderer.rb +57 -5
- data/lib/flow_chat/media.rb +121 -0
- data/lib/flow_chat/messenger/client.rb +264 -0
- data/lib/flow_chat/messenger/configuration.rb +103 -0
- data/lib/flow_chat/messenger/configuration_error.rb +9 -0
- data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
- data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
- data/lib/flow_chat/messenger/renderer.rb +150 -0
- data/lib/flow_chat/meta/challenge.rb +24 -0
- data/lib/flow_chat/meta/choice_ladder.rb +37 -0
- data/lib/flow_chat/meta/configuration_error.rb +7 -0
- data/lib/flow_chat/meta/gateway_identity.rb +38 -0
- data/lib/flow_chat/meta/messaging_gateway.rb +471 -0
- data/lib/flow_chat/meta/signature.rb +30 -0
- data/lib/flow_chat/meta/signature_validation.rb +66 -0
- data/lib/flow_chat/meta/webhook_verification.rb +43 -0
- data/lib/flow_chat/named_configuration.rb +65 -0
- data/lib/flow_chat/prompt.rb +13 -16
- data/lib/flow_chat/renderers/markdown_support.rb +109 -0
- data/lib/flow_chat/security.rb +76 -0
- data/lib/flow_chat/session/middleware.rb +11 -2
- data/lib/flow_chat/simulator/controller.rb +31 -15
- data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
- data/lib/flow_chat/telegram/client.rb +47 -4
- data/lib/flow_chat/telegram/configuration.rb +2 -42
- data/lib/flow_chat/telegram/configuration_error.rb +9 -0
- data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
- data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
- data/lib/flow_chat/telegram/renderer.rb +10 -2
- data/lib/flow_chat/text_truncator.rb +75 -0
- data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
- data/lib/flow_chat/version.rb +1 -1
- data/lib/flow_chat/whatsapp/client.rb +14 -7
- data/lib/flow_chat/whatsapp/configuration.rb +12 -51
- data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
- data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +259 -203
- data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
- data/lib/flow_chat/whatsapp/renderer.rb +121 -60
- data/lib/flow_chat.rb +0 -10
- data/lib/tasks/release.rake +11 -1
- data/site/.nojekyll +0 -0
- data/site/.og-card.html +89 -0
- data/site/favicon.svg +6 -0
- data/site/index.html +209 -0
- data/site/og.png +0 -0
- metadata +60 -11
- data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
data/docs/instrumentation.md
CHANGED
|
@@ -1,327 +1,136 @@
|
|
|
1
1
|
# Instrumentation
|
|
2
2
|
|
|
3
|
-
FlowChat
|
|
3
|
+
FlowChat emits `ActiveSupport::Notifications` events at each stage of a request, so you can feed metrics, traces, and structured logs into your own backend. The events fire whether or not anything subscribes; FlowChat also ships a log subscriber and a metrics collector that subscribe for you.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Event names
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
# Subscribe to all FlowChat events
|
|
9
|
-
ActiveSupport::Notifications.subscribe(/\.flow_chat$/) do |event|
|
|
10
|
-
Rails.logger.info "FlowChat: #{event.name} - #{event.payload}"
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
# Subscribe to specific events
|
|
14
|
-
ActiveSupport::Notifications.subscribe("api.error.flow_chat") do |event|
|
|
15
|
-
Sentry.capture_message("API Error", extra: event.payload)
|
|
16
|
-
end
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Event Reference
|
|
20
|
-
|
|
21
|
-
All events are namespaced with `.flow_chat` suffix. Use `FlowChat::Instrumentation::Events` constants for consistency.
|
|
22
|
-
|
|
23
|
-
### Core Framework Events
|
|
24
|
-
|
|
25
|
-
| Event | Description | Key Payload |
|
|
26
|
-
|-------|-------------|-------------|
|
|
27
|
-
| `flow.execution.start` | Flow method begins executing | `flow_name`, `action`, `session_id` |
|
|
28
|
-
| `flow.execution.end` | Flow method completes | `flow_name`, `action`, `duration` |
|
|
29
|
-
| `flow.execution.error` | Unhandled error in flow | `flow_name`, `error`, `backtrace` |
|
|
30
|
-
| `context.created` | New request context created | `request_id`, `platform` |
|
|
31
|
-
|
|
32
|
-
### Session Events
|
|
33
|
-
|
|
34
|
-
| Event | Description | Key Payload |
|
|
35
|
-
|-------|-------------|-------------|
|
|
36
|
-
| `session.created` | New session started | `session_id`, `boundaries` |
|
|
37
|
-
| `session.destroyed` | Session ended/cleared | `session_id` |
|
|
38
|
-
| `session.data.get` | Session data read | `session_id`, `key` |
|
|
39
|
-
| `session.data.set` | Session data written | `session_id`, `key` |
|
|
40
|
-
| `session.cache.hit` | Session found in cache | `session_id` |
|
|
41
|
-
| `session.cache.miss` | Session not in cache | `session_id` |
|
|
42
|
-
|
|
43
|
-
### Messaging Events
|
|
44
|
-
|
|
45
|
-
| Event | Description | Key Payload |
|
|
46
|
-
|-------|-------------|-------------|
|
|
47
|
-
| `message.received` | Inbound message from user | `platform`, `from`, `message_type` |
|
|
48
|
-
| `message.sent` | Outbound message to user | `platform`, `to`, `message_type`, `content_length` |
|
|
49
|
-
| `media.upload` | Media file uploaded (WhatsApp) | `platform`, `filename`, `mime_type`, `size` |
|
|
50
|
-
| `pagination.triggered` | USSD pagination activated | `session_id`, `page`, `total_pages` |
|
|
51
|
-
|
|
52
|
-
### Webhook Events
|
|
53
|
-
|
|
54
|
-
| Event | Description | Key Payload |
|
|
55
|
-
|-------|-------------|-------------|
|
|
56
|
-
| `webhook.verified` | Webhook signature valid (WhatsApp) | `platform`, `gateway` |
|
|
57
|
-
| `webhook.failed` | Webhook validation failed (WhatsApp) | `platform`, `reason` |
|
|
58
|
-
|
|
59
|
-
### API Events
|
|
60
|
-
|
|
61
|
-
| Event | Description | Key Payload |
|
|
62
|
-
|-------|-------------|-------------|
|
|
63
|
-
| `api.error` | API call failed | `platform`, `message`, error details |
|
|
7
|
+
Every FlowChat event is published under its name with a `.flow_chat` suffix. The name in the table is what you pass to `instrument`; the string you subscribe to adds the suffix, for example `flow.execution.end.flow_chat`.
|
|
64
8
|
|
|
65
|
-
|
|
9
|
+
| Event | When it fires |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `flow.execution.start` | A flow action begins. |
|
|
12
|
+
| `flow.execution.end` | A flow action finishes (carries `duration`). |
|
|
13
|
+
| `flow.execution.error` | A flow action raised. |
|
|
14
|
+
| `context.created` | A request context is built. |
|
|
15
|
+
| `session.created` | A session is created. |
|
|
16
|
+
| `session.destroyed` | A session is destroyed (flow terminated). |
|
|
17
|
+
| `session.data.get` / `session.data.set` | A session value is read or written. |
|
|
18
|
+
| `session.cache.hit` / `session.cache.miss` | A session cache lookup. |
|
|
19
|
+
| `message.received` | An inbound message arrives (text or an attachment). |
|
|
20
|
+
| `message.sent` | A response is sent to the user. |
|
|
21
|
+
| `message.delivery_failed` | A reply the flow produced that the platform would not take. |
|
|
22
|
+
| `message.status` | A platform's own report of what became of a message we sent. |
|
|
23
|
+
| `webhook.verified` / `webhook.failed` | A gateway verified or rejected a webhook. |
|
|
24
|
+
| `api.request` / `api.error` | An outbound platform API call, or its failure. |
|
|
25
|
+
| `media.upload` | Media is uploaded to a platform. |
|
|
26
|
+
| `pagination.triggered` | A USSD response was split into pages. |
|
|
27
|
+
| `webhook.received` | A verified webhook this gateway does not model, handed on whole. |
|
|
66
28
|
|
|
67
|
-
|
|
29
|
+
Payloads are enriched with `request_id`, `session_id`, `flow_name`, `gateway`, and `platform` when the context has them, plus a `timestamp`.
|
|
68
30
|
|
|
69
|
-
|
|
70
|
-
|-------|---------------|-------------|
|
|
71
|
-
| `api.request` | Wrap outbound API calls | `platform`, `endpoint` |
|
|
72
|
-
| `middleware.before` | Custom middleware entry | `middleware_class` |
|
|
73
|
-
| `middleware.after` | Custom middleware exit | `middleware_class`, `duration` |
|
|
74
|
-
| `conversation.assigned` | Intercom assignment tracking | `conversation_id`, `admin_id` |
|
|
75
|
-
| `conversation.tagged` | Intercom tag tracking | `conversation_id`, `tag` |
|
|
76
|
-
| `conversation.state_changed` | Intercom state tracking | `conversation_id`, `state` |
|
|
31
|
+
## Webhooks that are not messaging
|
|
77
32
|
|
|
78
|
-
|
|
33
|
+
FlowChat's job is messaging: the inbound turn, the reply it produces, and what became of that reply. A platform sends a great deal more. WhatsApp alone will report account bans, template approvals, phone number quality, imported chat history, contact address books, and replies a human typed in the WhatsApp Business App, and it adds new fields regularly.
|
|
79
34
|
|
|
80
|
-
|
|
35
|
+
None of that is a customer turn, so **none of it runs a flow**, and none of it is interpreted here. FlowChat verifies the signature, answers the platform, and publishes the change under `webhook.received` with the field that named it. What it means is your application's decision:
|
|
81
36
|
|
|
82
|
-
### Event Payload by Platform
|
|
83
|
-
|
|
84
|
-
**Telegram:**
|
|
85
37
|
```ruby
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
**WhatsApp:**
|
|
98
|
-
```ruby
|
|
99
|
-
{
|
|
100
|
-
platform: :whatsapp,
|
|
101
|
-
message: "WhatsApp API request failed",
|
|
102
|
-
phone_number_id: "123456789",
|
|
103
|
-
recipient: "+1234567890",
|
|
104
|
-
message_type: "text",
|
|
105
|
-
response_code: "401",
|
|
106
|
-
error_type: "OAuthException",
|
|
107
|
-
error_code: 190,
|
|
108
|
-
error_subcode: 463,
|
|
109
|
-
error_message: "Error validating access token"
|
|
110
|
-
}
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
**Intercom:**
|
|
114
|
-
```ruby
|
|
115
|
-
{
|
|
116
|
-
platform: :intercom,
|
|
117
|
-
message: "Intercom authentication failed",
|
|
118
|
-
app_id: "abc123def",
|
|
119
|
-
conversation_id: "conv_123",
|
|
120
|
-
admin_id: "admin_456"
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### Error Handling Behavior
|
|
125
|
-
|
|
126
|
-
| Error Type | Telegram | WhatsApp | Intercom |
|
|
127
|
-
|------------|----------|----------|----------|
|
|
128
|
-
| Invalid credentials | Returns `{"ok"=>false}` | Returns `nil` | Raises `ConfigurationError` |
|
|
129
|
-
| API error response | Returns error hash | Returns `nil` | Returns `nil` |
|
|
130
|
-
| Network timeout | Re-raises exception | Re-raises exception | Re-raises exception |
|
|
131
|
-
| Connection refused | Returns `{"ok"=>false}` | Returns `nil` | Returns `nil` |
|
|
132
|
-
|
|
133
|
-
**Note:** Network timeouts (`Net::OpenTimeout`, `Net::ReadTimeout`) are intentionally re-raised without instrumentation, allowing callers to implement retry logic at a higher level.
|
|
134
|
-
|
|
135
|
-
### Example: Error Monitoring
|
|
136
|
-
|
|
137
|
-
```ruby
|
|
138
|
-
# config/initializers/flow_chat_monitoring.rb
|
|
139
|
-
|
|
140
|
-
ActiveSupport::Notifications.subscribe("api.error.flow_chat") do |event|
|
|
141
|
-
payload = event.payload
|
|
142
|
-
|
|
143
|
-
# Log with structured data
|
|
144
|
-
Rails.logger.error({
|
|
145
|
-
event: "flow_chat.api_error",
|
|
146
|
-
platform: payload[:platform],
|
|
147
|
-
message: payload[:message],
|
|
148
|
-
error_code: payload[:error_code],
|
|
149
|
-
recipient: payload[:recipient] || payload[:chat_id] || payload[:conversation_id]
|
|
150
|
-
}.to_json)
|
|
151
|
-
|
|
152
|
-
# Send to error tracking service
|
|
153
|
-
Sentry.capture_message(
|
|
154
|
-
"FlowChat API Error: #{payload[:message]}",
|
|
155
|
-
level: :error,
|
|
156
|
-
extra: payload
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Increment metrics
|
|
160
|
-
StatsD.increment("flow_chat.api_error", tags: ["platform:#{payload[:platform]}"])
|
|
38
|
+
ActiveSupport::Notifications.subscribe("webhook.received.flow_chat") do |*, payload|
|
|
39
|
+
case payload[:field]
|
|
40
|
+
when "smb_message_echoes"
|
|
41
|
+
# A human answered from the WhatsApp Business App. Most applications will want
|
|
42
|
+
# to stop the bot replying on top of them.
|
|
43
|
+
MyApp::Echoes.record(payload[:business_phone_number_id], payload[:value]["message_echoes"])
|
|
44
|
+
when "history"
|
|
45
|
+
MyApp::HistoryImport.enqueue(payload[:business_phone_number_id], payload[:value]["history"])
|
|
46
|
+
when "account_update"
|
|
47
|
+
MyApp::Connections.review(payload[:business_phone_number_id], payload[:value])
|
|
48
|
+
end
|
|
161
49
|
end
|
|
162
50
|
```
|
|
163
51
|
|
|
164
|
-
|
|
52
|
+
The payload carries `field`, the whole `value`, and the business phone number when the change names one. Account-level changes do not name one, so expect it to be nil there.
|
|
165
53
|
|
|
166
|
-
|
|
54
|
+
Subscribing to a field costs nothing here: an unmodelled field is published whether or not anything listens, and logged at info so a subscription you have not written yet is still visible. Adding support for a new field is a change in your application, not a new release of this gem.
|
|
167
55
|
|
|
168
|
-
|
|
169
|
-
# Errors are automatically reported with context:
|
|
170
|
-
Rails.error.report(exception, handled: true, context: {
|
|
171
|
-
platform: :whatsapp,
|
|
172
|
-
recipient: "+1234567890",
|
|
173
|
-
message_type: "text"
|
|
174
|
-
})
|
|
175
|
-
```
|
|
56
|
+
## Reacting to `api.error`
|
|
176
57
|
|
|
177
|
-
|
|
58
|
+
The `message` on an `api.error` payload is prose, written for someone reading
|
|
59
|
+
logs. Do not branch on it: rewording a log line would change your behaviour.
|
|
60
|
+
Read these instead.
|
|
178
61
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
62
|
+
| key | meaning |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `error_class` | The exception's class, whenever one was raised. |
|
|
65
|
+
| `error_type` | What kind of failure it is, named by the adapter. Intercom reports `authentication`, `resource_not_found` and `server_error`; WhatsApp passes through Meta's own `type`, such as `OAuthException`. |
|
|
66
|
+
| `error_code` | The platform's own code. Telegram's `error_code`, Meta's `code`, Intercom's HTTP status. |
|
|
183
67
|
|
|
184
|
-
|
|
68
|
+
WhatsApp also carries `error_subcode` and `error_message` from Meta, and
|
|
69
|
+
Telegram carries `error_description`. Identify the connection from
|
|
70
|
+
`phone_number_id`, `bot_id` or `app_id` as appropriate.
|
|
185
71
|
|
|
186
|
-
|
|
72
|
+
Not every failure reports. Network timeouts are re-raised so your own retry
|
|
73
|
+
logic sees them, and an Intercom rate limit raises `RateLimitError` rather than
|
|
74
|
+
reporting, so a subscriber reacting to `api.error` will not mistake either for
|
|
75
|
+
a dead credential.
|
|
187
76
|
|
|
188
77
|
```ruby
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
instrument(Events::API_REQUEST, { endpoint: "payment_gateway" }) do
|
|
194
|
-
result = PaymentGateway.charge(amount)
|
|
195
|
-
|
|
196
|
-
if result.success?
|
|
197
|
-
app.say "Payment successful!"
|
|
198
|
-
else
|
|
199
|
-
instrument(Events::API_ERROR, {
|
|
200
|
-
message: "Payment failed",
|
|
201
|
-
error_code: result.error_code
|
|
202
|
-
})
|
|
203
|
-
app.say "Payment failed: #{result.error}"
|
|
204
|
-
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
78
|
+
ActiveSupport::Notifications.subscribe("api.error.flow_chat") do |*, payload|
|
|
79
|
+
next unless payload[:error_type] == "authentication"
|
|
80
|
+
|
|
81
|
+
AlertOwner.call(platform: payload[:platform], app_id: payload[:app_id])
|
|
207
82
|
end
|
|
208
83
|
```
|
|
209
84
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
```ruby
|
|
213
|
-
class MetricsMiddleware
|
|
214
|
-
include FlowChat::Instrumentation
|
|
215
|
-
|
|
216
|
-
def initialize(app)
|
|
217
|
-
@app = app
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def call(context)
|
|
221
|
-
@context = context
|
|
85
|
+
## Delivery callbacks
|
|
222
86
|
|
|
223
|
-
|
|
87
|
+
Events are broadcasts and carry no context, because anyone may subscribe and the context holds the gateway client and the raw inbound body. When the application that owns the turn needs to reach its own records, it uses a callback instead.
|
|
224
88
|
|
|
225
|
-
|
|
226
|
-
result = @app.call(context)
|
|
227
|
-
duration = Time.current - start_time
|
|
89
|
+
A gateway that delivers out of band (Telegram, WhatsApp, Intercom) sends after the middleware stack has unwound. So a row the application wrote during the turn was written before anything knew whether the send worked, or what the platform would call it. These two callbacks are the only places that know:
|
|
228
90
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
result
|
|
235
|
-
end
|
|
236
|
-
|
|
237
|
-
# Required for context enrichment
|
|
238
|
-
attr_reader :context
|
|
91
|
+
```ruby
|
|
92
|
+
FlowChat::Config.on_delivery_success = lambda do |context, result|
|
|
93
|
+
id = context[FlowChat::Instrumentation::DELIVERED_MESSAGE_ID_KEY]
|
|
94
|
+
MyApp::Message.find(context["myapp.bot_message_id"]).update!(platform_message_id: id) if id
|
|
239
95
|
end
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### Module-Level Instrumentation
|
|
243
96
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
custom_key: "custom_value"
|
|
248
|
-
})
|
|
97
|
+
FlowChat::Config.on_delivery_failure = lambda do |context, error|
|
|
98
|
+
MyApp::Message.find(context["myapp.bot_message_id"]).update!(status: :failed, error: error.message)
|
|
99
|
+
end
|
|
249
100
|
```
|
|
250
101
|
|
|
251
|
-
|
|
102
|
+
`DELIVERED_MESSAGE_ID_KEY` is `"delivery.platform_message_id"`, and every out-of-band gateway sets it to whatever its own platform called the message: WhatsApp's `wamid`, Telegram's numeric `message_id`, Intercom's conversation part id. It is nil when a platform names none. Reading one key is the point, so an application does not carry a case statement over platforms.
|
|
252
103
|
|
|
253
|
-
|
|
104
|
+
HTTP and USSD set nothing, since their reply travels in the response they are already returning and has no separate delivery to succeed or fail.
|
|
254
105
|
|
|
255
|
-
|
|
256
|
-
- `session_id` - Current session ID
|
|
257
|
-
- `flow_name` - Active flow class name
|
|
258
|
-
- `gateway` - Gateway handling the request
|
|
259
|
-
- `platform` - Platform (`:ussd`, `:whatsapp`, `:telegram`, `:intercom`)
|
|
106
|
+
Neither callback may change what happened. `on_delivery_success` cannot alter the send's return value, `on_delivery_failure` cannot replace the delivery error, and an exception raised in either is logged and dropped.
|
|
260
107
|
|
|
261
|
-
|
|
262
|
-
- `timestamp` - Event timestamp (`Time.current`)
|
|
108
|
+
## Subscribing
|
|
263
109
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
### Pattern Matching
|
|
267
|
-
|
|
268
|
-
```ruby
|
|
269
|
-
# All FlowChat events
|
|
270
|
-
ActiveSupport::Notifications.subscribe(/\.flow_chat$/) { |event| ... }
|
|
271
|
-
|
|
272
|
-
# All error events
|
|
273
|
-
ActiveSupport::Notifications.subscribe(/error\.flow_chat$/) { |event| ... }
|
|
274
|
-
|
|
275
|
-
# All session events
|
|
276
|
-
ActiveSupport::Notifications.subscribe(/^session\..*\.flow_chat$/) { |event| ... }
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### Block vs Callable
|
|
110
|
+
Subscribe with `ActiveSupport::Notifications`, remembering the `.flow_chat` suffix:
|
|
280
111
|
|
|
281
112
|
```ruby
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
puts event.payload
|
|
113
|
+
ActiveSupport::Notifications.subscribe("flow.execution.end.flow_chat") do |*, payload|
|
|
114
|
+
StatsD.timing("flow_chat.flow.#{payload[:flow_name]}", payload[:duration])
|
|
285
115
|
end
|
|
286
116
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def call(event)
|
|
290
|
-
# Access timing info
|
|
291
|
-
puts "Duration: #{event.duration}ms"
|
|
292
|
-
puts "Payload: #{event.payload}"
|
|
293
|
-
end
|
|
117
|
+
ActiveSupport::Notifications.subscribe("message.received.flow_chat") do |*, payload|
|
|
118
|
+
StatsD.increment("flow_chat.message.received.#{payload[:platform]}")
|
|
294
119
|
end
|
|
295
|
-
|
|
296
|
-
ActiveSupport::Notifications.subscribe("message.sent.flow_chat", MessageLogger.new)
|
|
297
120
|
```
|
|
298
121
|
|
|
299
|
-
##
|
|
300
|
-
|
|
301
|
-
```ruby
|
|
302
|
-
class FlowInstrumentationTest < ActiveSupport::TestCase
|
|
303
|
-
def test_api_error_instrumentation
|
|
304
|
-
events = []
|
|
305
|
-
|
|
306
|
-
ActiveSupport::Notifications.subscribe("api.error.flow_chat") do |event|
|
|
307
|
-
events << event
|
|
308
|
-
end
|
|
122
|
+
## Built-in metrics
|
|
309
123
|
|
|
310
|
-
|
|
311
|
-
client.send_message_with_invalid_token
|
|
124
|
+
`FlowChat.metrics` returns a metrics collector that subscribes to the events above and keeps running counters and timings (flows executed, errors by class, sessions created by gateway, cache hits, and so on). Read a snapshot:
|
|
312
125
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
ActiveSupport::Notifications.unsubscribe("api.error.flow_chat")
|
|
317
|
-
end
|
|
318
|
-
end
|
|
126
|
+
```ruby
|
|
127
|
+
FlowChat.metrics.snapshot # => a Hash of counters and timings
|
|
128
|
+
FlowChat.metrics.get_category("flows") # => just the flows.* metrics
|
|
319
129
|
```
|
|
320
130
|
|
|
321
|
-
|
|
131
|
+
FlowChat also ships a `LogSubscriber` that logs the same events through `FlowChat::Config.logger`. Both are wired up by `FlowChat::Instrumentation::Setup`; call `FlowChat.setup_instrumentation!` during boot to enable them, or access `FlowChat.metrics` to start the collector on first use.
|
|
132
|
+
|
|
133
|
+
## Related
|
|
322
134
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
3. **Filter events** - Only subscribe to events you need to avoid overhead
|
|
326
|
-
4. **Use structured logging** - Log payloads as JSON for easier querying
|
|
327
|
-
5. **Set up alerting** - Configure alerts on `api.error` events for proactive monitoring
|
|
135
|
+
- [Configuration](configuration.md)
|
|
136
|
+
- [Architecture](architecture.md)
|