Is there any way to intercept incoming events (before they are being sent to handle_in/3 callback) in Channels. From the docs. I can see one can only intercept outgoing events.
If not, is there any specific reason why is this not supported?
Specifically, my use case: I’m trying to set the Sentry context when handling input messages, so I get additional information on the errors (event name, last message) if such error occurs in the handle_in/3 callback. Currently, I’m forced to write some prologue code in all handle_in/3 callbacks, which I would like to escape if I can.
because it doesn’t make sense to have a step before the handle in. the handle_in is the moment you gonna process it comming from the client. intercepting the outgoing events happens because phoenix abstract away the need to handle all outgoing messages.
handle_in - messages from client(FE) to the channel(BE).
handle_out - messages that gets to the channel(BE) through the system(other processes in the BE, usually through the pubsub) and needs to be sent to the client(FE).
phoenix automate that any message sent to the topic of a channel(the name of the channel is the topic) through the phoenix pubsub is something that should go to the client. so you only needs to implement the handle_out callback if the channel subscribe to other topics, or if you wanna do something with the message before sending it to the client(like filtering messages).
See my use-case below, I think it makes sense to have a one place (whatever it may be) that executes within same process and before handle_in/3 callback.
defmodule SomeChannel do
# ...
def handle_in("message-1", payload, socket) do
SentryContext.within_channel_event("message-1", payload)
# ...
end
def handle_in("message-2", payload, socket) do
SentryContext.within_channel_event("message-2", payload)
# ...
end
def handle_in("message-3", payload, socket) do
SentryContext.within_channel_event("message-3", payload)
# ...
end
def handle_in("message-4", payload, socket) do
SentryContext.within_channel_event("message-4", payload)
# ...
end
# Some more handle_in/3 callbacks...
end
defmodule SomeAlternativeChannel do
# ...
# NOTE: This does not exist, it's just an example
def before_each_event(event, payload, socket) do
SentryContext.within_channel_event(event, payload)
{:ok, socket}
end
def handle_in("message-1", payload, socket) do
# ...
end
def handle_in("message-2", payload, socket) do
# ...
end
# Some more handle_in/3 callbacks...
end
Another use-case would be setting up request_id metadata in logger:
# NOTE: This does not exist, it's just an example
def before_each_event(event, payload, socket) do
Logger.metadata(request_id: reuse_or_generate_new_request_id(payload))
{:ok, socket}
end
why don’t you just make the handle_in generic and create another function to deal with the message…
def handle_in(event, payload, socket) do
SentryContext.withing_chanel9event(event, payload)
process_incoming(event, payload, socket)
end
and then in the process_incoming/3 you do the specific stuff. you see that you can compose with the handle_in without the need to introduce more steps to the abstraction. if that is something that you want to use on all your channels you can even abstract it away.
my point on making sense is not that “it doesn’t have usage”, but that it doesn’t fit the way the framework designed the channels to work.
Channels / LiveView don’t have an equivalent to plug style middleware, where you can easily go to a channel or a live view and just say “oh also do X when you get a message”. LiveView handled this by adding hooks since it was a pretty common ask to deal with auth / telemetry / etc, but it doesn’t look like Channels have an equivalent yet. Probably the hooks design could also work here? That said, @kklisura’s solution is definitely the way to go for the moment.