Module: FlowChat::Instrumentation
- Extended by:
- ActiveSupport::Concern
- Included in:
- Context, Http::Gateway::Simple, FlowChat::Intercom::Client, FlowChat::Intercom::Gateway::IntercomApi, Messenger::Client, Meta::MessagingGateway, Processor, Session::CacheSessionStore, Session::Middleware, Telegram::Client, Telegram::Gateway::BotApi, Ussd::Gateway::Nalo, Ussd::Middleware::Pagination, Whatsapp::Client, Whatsapp::Gateway::CloudApi
- Defined in:
- lib/flow_chat/instrumentation.rb,
lib/flow_chat/instrumentation/setup.rb,
lib/flow_chat/instrumentation/log_subscriber.rb,
lib/flow_chat/instrumentation/metrics_collector.rb
Defined Under Namespace
Modules: Events, Setup Classes: LogSubscriber, MetricsCollector
Constant Summary collapse
- DELIVERED_MESSAGE_ID_KEY =
Where a delivered reply's platform message id is left on the context, the same way for every gateway. nil when the platform does not name one.
"delivery.platform_message_id"- DELIVERY_DURATION_KEY =
How long the send itself took, in milliseconds, left here by report_delivery_failure for the gateway to put on MESSAGE_SENT.
Measured rather than taken from ActiveSupport::Notifications' own event duration: a block event is published whatever the block returns, so timing the send that way meant publishing MESSAGE_SENT for sends that failed. The event is emitted after the fact instead, which leaves its own duration at zero, so the real figure is carried in the payload.
"delivery.duration_ms"
Class Method Summary collapse
-
.instrument(event_name, payload = {}, &block) ⇒ Object
Module-level method for direct calls like FlowChat::Instrumentation.instrument.
-
.report_api_error(message, error: nil, **context) ⇒ Object
Shared helper for reporting API errors with instrumentation and Rails.error.
Instance Method Summary collapse
- #elapsed_ms_since(started_at) ⇒ Object
-
#inbound_message?(context) ⇒ Boolean
True when this turn carries something to process — text OR a structured attachment (media/location/contact).
-
#instrument(event_name, payload = {}, &block) ⇒ Object
Instrument a block of code with the given event name and payload.
-
#platform_message_id_from(result) ⇒ Object
What the platform called the message it just accepted.
-
#report_delivery_failure(context, **payload) ⇒ Object
Wraps a delivery so a reply the platform would not take is reported.
-
#report_delivery_success(context, result) ⇒ Object
The success half.
- #report_to_app(context, error) ⇒ Object
-
#report_to_subscribers(error, payload) ⇒ Object
Neither reader may replace the delivery error with one of its own, which would hide the failure they are being told about.
Class Method Details
.instrument(event_name, payload = {}, &block) ⇒ Object
Module-level method for direct calls like FlowChat::Instrumentation.instrument
158 159 160 161 162 163 164 165 166 |
# File 'lib/flow_chat/instrumentation.rb', line 158 def self.instrument(event_name, payload = {}, &block) full_event_name = "#{event_name}.flow_chat" enriched_payload = { timestamp: Time.current }.merge(payload || {}).compact ActiveSupport::Notifications.instrument(full_event_name, enriched_payload, &block) end |
.report_api_error(message, error: nil, **context) ⇒ Object
Shared helper for reporting API errors with instrumentation and Rails.error
message is prose for a human reading logs. Subscribers deciding what to
do about an error should read the structured keys instead, so that
rewording a message never changes behaviour somewhere else:
error_class the exception's class, filled in here from `error`
error_type what kind of failure it is, named by the adapter
error_code the platform's own code, where it gives one
181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/flow_chat/instrumentation.rb', line 181 def self.report_api_error(, error: nil, **context) error_context = context.compact # An exception's class is a classification the caller already made. Carry # it so a subscriber can branch on it rather than parsing the message. error_context[:error_class] ||= error.class.name if error # Instrument for custom subscribers instrument(Events::API_ERROR, error_context.merge(message: )) # Report to Rails.error if available if defined?(Rails) && Rails.respond_to?(:error) && Rails.error.respond_to?(:report) exception = error || StandardError.new() Rails.error.report(exception, handled: true, context: error_context) end end |
Instance Method Details
#elapsed_ms_since(started_at) ⇒ Object
89 90 91 |
# File 'lib/flow_chat/instrumentation.rb', line 89 def elapsed_ms_since(started_at) ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000).round(2) end |
#inbound_message?(context) ⇒ Boolean
True when this turn carries something to process — text OR a structured
attachment (media/location/contact). Gateways gate MESSAGE_RECEIVED on this
so caption-less media, locations, and contacts are still instrumented: they
set a blank input string (not the old "$media$"-style sentinel), so a plain
context.input.present? check would silently drop them.
142 143 144 145 146 147 148 149 |
# File 'lib/flow_chat/instrumentation.rb', line 142 def (context) return false unless context context.input.present? || !context["request.media"].nil? || !context["request.location"].nil? || !context["request.contact"].nil? end |
#instrument(event_name, payload = {}, &block) ⇒ Object
Instrument a block of code with the given event name and payload
22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/flow_chat/instrumentation.rb', line 22 def instrument(event_name, payload = {}, &block) enriched_payload = payload&.dup || {} if respond_to?(:context) && context enriched_payload[:request_id] = context["request.id"] if context["request.id"] enriched_payload[:session_id] = context["session.id"] if context["session.id"] enriched_payload[:flow_name] = context["flow.name"] if context["flow.name"] enriched_payload[:gateway] = context["request.gateway"] if context["request.gateway"] enriched_payload[:platform] = context["request.platform"] if context["request.platform"] end self.class.instrument(event_name, enriched_payload, &block) end |
#platform_message_id_from(result) ⇒ Object
What the platform called the message it just accepted.
Overridden by every gateway that delivers out of band, because each one is the only thing that knows the shape of its own client's answer. Naming it here rather than in each app is the point: an app stamping the id onto its own record should not have to carry a case statement over platforms.
110 111 112 |
# File 'lib/flow_chat/instrumentation.rb', line 110 def (result) nil end |
#report_delivery_failure(context, **payload) ⇒ Object
Wraps a delivery so a reply the platform would not take is reported.
A gateway sends after the middleware stack has returned. An app that records what the flow said has therefore already recorded it, and recorded it as having gone out, before anything knows whether it did. The send is the only place that learns otherwise, and it is downstream of everything that could act on it.
Reported two ways, because two different kinds of reader want it.
The event is a broadcast, and takes the same shape its gateway gives MESSAGE_SENT: what was being sent and where, and nothing else. Anyone may subscribe, including tools that write whatever they are handed straight into a log, so it carries no more than the send itself already announces.
The callback is the app that owns this turn, acting on records only it knows about. It gets the whole context because it is the app's own code, configured by the app, and reading what the app put there. That is not true of a subscriber, and the context holds the gateway client and the raw inbound body.
Re-raises whatever the send raised: this reports a failure, it does not handle one. A send fails two ways and only one of them raises. Every client here answers with the platform's parsed response when the message was accepted and nil once it has already logged an API error, so a nil result is a failure that arrived quietly. Treating it as success fired on_delivery_success for a message that was never delivered, and stamped a nil id onto the context as though the platform had named one.
It reports rather than raises, because the client already decided not to: turning a swallowed API error into an exception here would fail the webhook for a reply the platform merely declined.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/flow_chat/instrumentation.rb', line 68 def report_delivery_failure(context, **payload) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) result = yield context[DELIVERY_DURATION_KEY] = elapsed_ms_since(started_at) if result.nil? error = FlowChat::DeliveryError.new("#{payload[:platform] || "the platform"} did not accept the message") report_to_subscribers(error, payload) report_to_app(context, error) return nil end report_delivery_success(context, result) result rescue => error context[DELIVERY_DURATION_KEY] ||= elapsed_ms_since(started_at) if started_at report_to_subscribers(error, payload) report_to_app(context, error) raise error end |
#report_delivery_success(context, result) ⇒ Object
The success half. Runs where the send happened, which is the only place that knows what the platform called the message.
95 96 97 98 99 100 101 102 |
# File 'lib/flow_chat/instrumentation.rb', line 95 def report_delivery_success(context, result) context[DELIVERED_MESSAGE_ID_KEY] = (result) FlowChat::Config.on_delivery_success&.call(context, result) rescue => callback_error FlowChat.logger.error do "Instrumentation: on_delivery_success raised #{callback_error.class}: #{callback_error.}" end end |
#report_to_app(context, error) ⇒ Object
129 130 131 132 133 134 135 |
# File 'lib/flow_chat/instrumentation.rb', line 129 def report_to_app(context, error) FlowChat::Config.on_delivery_failure&.call(context, error) rescue => callback_error FlowChat.logger.error do "Instrumentation: on_delivery_failure raised #{callback_error.class}: #{callback_error.}" end end |
#report_to_subscribers(error, payload) ⇒ Object
Neither reader may replace the delivery error with one of its own, which would hide the failure they are being told about. Notifications gather subscriber errors and re-raise them, so both are reachable.
117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/flow_chat/instrumentation.rb', line 117 def report_to_subscribers(error, payload) instrument(Events::MESSAGE_DELIVERY_FAILED, payload.merge( error_class: error.class.name, message: error. )) rescue => subscriber_error FlowChat.logger.error do "Instrumentation: a #{Events::MESSAGE_DELIVERY_FAILED} subscriber raised " \ "#{subscriber_error.class}: #{subscriber_error.}" end end |