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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +29 -0
  3. data/.github/workflows/pages.yml +43 -0
  4. data/.gitignore +4 -1
  5. data/CHANGELOG.md +49 -0
  6. data/README.md +166 -295
  7. data/Rakefile +12 -1
  8. data/SECURITY.md +1 -1
  9. data/docs/architecture.md +52 -479
  10. data/docs/async-background-processing.md +31 -265
  11. data/docs/configuration.md +106 -613
  12. data/docs/factory-pattern.md +27 -324
  13. data/docs/gateway-context-variables.md +140 -143
  14. data/docs/gateway-development.md +86 -650
  15. data/docs/getting-started.md +40 -379
  16. data/docs/instrumentation.md +88 -279
  17. data/docs/platforms/instagram.md +278 -0
  18. data/docs/platforms/messenger.md +205 -0
  19. data/docs/platforms/telegram.md +47 -951
  20. data/docs/platforms/ussd.md +38 -653
  21. data/docs/platforms/whatsapp.md +73 -1321
  22. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md +732 -0
  23. data/docs/superpowers/plans/2026-07-09-inbound-media-support.md.tasks.json +58 -0
  24. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md +4064 -0
  25. data/docs/superpowers/plans/2026-08-10-messenger-instagram.md.tasks.json +226 -0
  26. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md +972 -0
  27. data/docs/superpowers/plans/2026-08-16-unified-choice-resolution.md.tasks.json +88 -0
  28. data/docs/superpowers/specs/2026-07-09-inbound-media-support-design.md +195 -0
  29. data/docs/superpowers/specs/2026-08-10-messenger-instagram-design.md +391 -0
  30. data/docs/testing.md +26 -297
  31. data/examples/http_controller.rb +12 -10
  32. data/examples/intercom_configuration_example.rb +19 -24
  33. data/examples/intercom_controller.rb +8 -20
  34. data/examples/multi_tenant_whatsapp_controller.rb +61 -166
  35. data/examples/simulator_controller.rb +0 -1
  36. data/examples/ussd_controller.rb +86 -158
  37. data/examples/whatsapp_controller.rb +16 -15
  38. data/examples/whatsapp_media_examples.rb +27 -79
  39. data/flow_chat.gemspec +22 -2
  40. data/gemfiles/rails_main.gemfile +18 -0
  41. data/lib/flow_chat/app.rb +129 -11
  42. data/lib/flow_chat/async_job.rb +10 -0
  43. data/lib/flow_chat/choice_titles.rb +95 -0
  44. data/lib/flow_chat/config.rb +124 -1
  45. data/lib/flow_chat/delivery_error.rb +9 -0
  46. data/lib/flow_chat/gateway_async_support.rb +19 -1
  47. data/lib/flow_chat/http/configuration_error.rb +9 -0
  48. data/lib/flow_chat/http/gateway/simple.rb +24 -4
  49. data/lib/flow_chat/http/middleware/choice_mapper.rb +94 -0
  50. data/lib/flow_chat/input.rb +86 -0
  51. data/lib/flow_chat/instagram/client.rb +32 -0
  52. data/lib/flow_chat/instagram/configuration.rb +147 -0
  53. data/lib/flow_chat/instagram/configuration_error.rb +7 -0
  54. data/lib/flow_chat/instagram/gateway/send_api.rb +63 -0
  55. data/lib/flow_chat/instagram/middleware/choice_mapper.rb +22 -0
  56. data/lib/flow_chat/instagram/renderer.rb +23 -0
  57. data/lib/flow_chat/instrumentation/metrics_collector.rb +6 -1
  58. data/lib/flow_chat/instrumentation.rb +160 -1
  59. data/lib/flow_chat/intercom/client.rb +34 -28
  60. data/lib/flow_chat/intercom/configuration.rb +2 -49
  61. data/lib/flow_chat/intercom/configuration_error.rb +9 -0
  62. data/lib/flow_chat/intercom/gateway/intercom_api.rb +81 -57
  63. data/lib/flow_chat/intercom/middleware/choice_mapper.rb +101 -0
  64. data/lib/flow_chat/intercom/renderer.rb +57 -5
  65. data/lib/flow_chat/media.rb +121 -0
  66. data/lib/flow_chat/messenger/client.rb +264 -0
  67. data/lib/flow_chat/messenger/configuration.rb +103 -0
  68. data/lib/flow_chat/messenger/configuration_error.rb +9 -0
  69. data/lib/flow_chat/messenger/gateway/send_api.rb +42 -0
  70. data/lib/flow_chat/messenger/middleware/choice_mapper.rb +185 -0
  71. data/lib/flow_chat/messenger/renderer.rb +150 -0
  72. data/lib/flow_chat/meta/challenge.rb +24 -0
  73. data/lib/flow_chat/meta/choice_ladder.rb +37 -0
  74. data/lib/flow_chat/meta/configuration_error.rb +7 -0
  75. data/lib/flow_chat/meta/gateway_identity.rb +38 -0
  76. data/lib/flow_chat/meta/messaging_gateway.rb +471 -0
  77. data/lib/flow_chat/meta/signature.rb +30 -0
  78. data/lib/flow_chat/meta/signature_validation.rb +66 -0
  79. data/lib/flow_chat/meta/webhook_verification.rb +43 -0
  80. data/lib/flow_chat/named_configuration.rb +65 -0
  81. data/lib/flow_chat/prompt.rb +13 -16
  82. data/lib/flow_chat/renderers/markdown_support.rb +109 -0
  83. data/lib/flow_chat/security.rb +76 -0
  84. data/lib/flow_chat/session/middleware.rb +11 -2
  85. data/lib/flow_chat/simulator/controller.rb +31 -15
  86. data/lib/flow_chat/simulator/views/simulator.html.erb +184 -20
  87. data/lib/flow_chat/telegram/client.rb +47 -4
  88. data/lib/flow_chat/telegram/configuration.rb +2 -42
  89. data/lib/flow_chat/telegram/configuration_error.rb +9 -0
  90. data/lib/flow_chat/telegram/gateway/bot_api.rb +46 -28
  91. data/lib/flow_chat/telegram/middleware/choice_mapper.rb +77 -16
  92. data/lib/flow_chat/telegram/renderer.rb +10 -2
  93. data/lib/flow_chat/text_truncator.rb +75 -0
  94. data/lib/flow_chat/ussd/middleware/choice_mapper.rb +10 -0
  95. data/lib/flow_chat/version.rb +1 -1
  96. data/lib/flow_chat/whatsapp/client.rb +14 -7
  97. data/lib/flow_chat/whatsapp/configuration.rb +12 -51
  98. data/lib/flow_chat/whatsapp/configuration_error.rb +9 -0
  99. data/lib/flow_chat/whatsapp/gateway/cloud_api.rb +259 -203
  100. data/lib/flow_chat/whatsapp/middleware/choice_mapper.rb +144 -57
  101. data/lib/flow_chat/whatsapp/renderer.rb +121 -60
  102. data/lib/flow_chat.rb +0 -10
  103. data/lib/tasks/release.rake +11 -1
  104. data/site/.nojekyll +0 -0
  105. data/site/.og-card.html +89 -0
  106. data/site/favicon.svg +6 -0
  107. data/site/index.html +209 -0
  108. data/site/og.png +0 -0
  109. metadata +60 -11
  110. data/lib/flow_chat/whatsapp/id_generator.rb +0 -124
@@ -1,327 +1,136 @@
1
1
  # Instrumentation
2
2
 
3
- FlowChat provides comprehensive instrumentation via ActiveSupport::Notifications, enabling monitoring, logging, metrics collection, and error tracking across all platforms.
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
- ## Quick Start
5
+ ## Event names
6
6
 
7
- ```ruby
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
- ### Events for Custom Use
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
- The following event constants are provided for use in your own instrumentation but are not emitted by FlowChat internally:
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
- | Event | Suggested Use | Key Payload |
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
- ## API Error Instrumentation
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
- The `api.error` event provides detailed information when API calls fail. This is useful for monitoring, alerting, and debugging integration issues.
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
- platform: :telegram,
88
- message: "Telegram API error: Unauthorized",
89
- bot_id: "123456789",
90
- api_method: "sendMessage",
91
- error_code: 401,
92
- error_description: "Unauthorized",
93
- chat_id: 987654321
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
- ### Example: Rails Error Reporting
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
- FlowChat automatically reports errors to `Rails.error` when available (Rails 7+):
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
- ```ruby
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
- You can subscribe to these in your error reporting configuration:
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
- ```ruby
180
- # config/initializers/error_reporting.rb
181
- Rails.error.subscribe(MyErrorReporter.new)
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
- ## Custom Instrumentation
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
- ### In Flows
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
- class PaymentFlow < FlowChat::Flow
190
- def process_payment
191
- amount = app.screen(:amount) { |p| p.ask "Enter amount:" }
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
- ### In Custom Middleware
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
- instrument(Events::MIDDLEWARE_BEFORE, { middleware_class: self.class.name })
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
- start_time = Time.current
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
- instrument(Events::MIDDLEWARE_AFTER, {
230
- middleware_class: self.class.name,
231
- duration: duration
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
- ```ruby
245
- # Direct instrumentation without including the module
246
- FlowChat::Instrumentation.instrument("custom.event", {
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
- ## Payload Enrichment
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
- When instrumenting from objects with a `context` accessor, payloads are automatically enriched with:
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
- - `request_id` - Unique request identifier
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
- All payloads also include:
262
- - `timestamp` - Event timestamp (`Time.current`)
108
+ ## Subscribing
263
109
 
264
- ## Subscribing to Events
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
- # Block form (simple)
283
- ActiveSupport::Notifications.subscribe("message.sent.flow_chat") do |event|
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
- # Callable form (for complex subscribers)
288
- class MessageLogger
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
- ## Testing
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
- # Trigger the error condition
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
- assert_equal 1, events.size
314
- assert_equal :whatsapp, events.first.payload[:platform]
315
- ensure
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
- ## Production Recommendations
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
- 1. **Subscribe early** - Set up subscriptions in initializers before requests arrive
324
- 2. **Keep handlers fast** - Use async processing for slow operations (logging to external services)
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)