FYI
Know what's happening in your app.
In-app events, user feedback, and instant Slack/Telegram notifications for Phoenix.
Stop refreshing your database to see if users are signing up. FYI gives you:
- 📤 Event tracking — Emit events from anywhere in your app with one line of code
- 📊 Live dashboard — Beautiful admin UI with search, filtering, and activity histograms
- 💬 Feedback widget — Drop-in component to collect user feedback (installs into your codebase)
- 🔔 Instant notifications — Get pinged in Slack or Telegram when important things happen
- 🎯 Smart routing — Send specific events to specific channels with glob patterns
- 🚀 One-command setup —
mix fyi.installhandles migrations, config, and routes
Installation
Add fyi to your list of dependencies in mix.exs:
def deps do [ {:fyi, "~> 1.0.0"} ] end
Then run the installer:
mix deps.get mix fyi.install
This will:
- Add
FYI.Applicationto your supervision tree - Create a migration for the
fyi_eventstable - Print instructions to add the
/fyiroute to your router - Add configuration stubs to your config files
Installer Options
--no-ui— Skip installing the admin inbox UI--no-persist— Skip the database migration (events won't be persisted)--no-feedback— Skip installing the feedback component
Configuration
# config/config.exs config :fyi, app_name: "MyApp", persist_events: true, repo: MyApp.Repo, prefix: "fyi", # optional sinks: [ {FYI.Sink.SlackWebhook, %{url: System.get_env("SLACK_WEBHOOK_URL")}}, {FYI.Sink.Telegram, %{ token: System.get_env("TELEGRAM_BOT_TOKEN"), chat_id: System.get_env("TELEGRAM_CHAT_ID") }} ], routes: [ %{match: "waitlist.*", sinks: [:slack]}, %{match: "purchase.*", sinks: [:slack, :telegram]}, %{match: "feedback.*", sinks: [:slack]} ]
App Name
Set app_name to identify events when multiple apps share the same Slack channel or Telegram chat:
config :fyi, app_name: "MyApp"
Messages will include the app name: [MyApp] *purchase.created* by user_123
Emojis
Add emojis to your notifications in three ways (in priority order):
1. Per-event override:
FYI.emit("error.critical", %{message: "DB down"}, emoji: "🚨")
2. Pattern-based mapping:
config :fyi, emojis: %{ "purchase.*" => "💰", "user.signup" => "👋", "feedback.*" => "💬", "error.*" => "🚨" }
3. Default fallback:
Messages will show as: 💰 [MyApp] *purchase.created* by user_123
Routing
Routes use simple glob matching:
purchase.*matchespurchase.created,purchase.updated, etc.*at the end matches any suffix
If no routes are configured, all events go to all sinks.
Usage
Emit an Event
FYI.emit("purchase.created", %{amount: 4900, currency: "GBP"}, actor: user_id) FYI.emit("user.signup", %{email: "user@example.com"}, source: "landing_page") FYI.emit("error.critical", %{message: "DB connection failed"}, emoji: "🚨", tags: %{env: "prod"})
Options:
:actor- who triggered the event (user_id, email, etc.):source- where the event originated (e.g., "api", "web", "worker"):tags- additional metadata map for filtering:emoji- override emoji for this specific event
Emit from Ecto.Multi (Recommended)
Ecto.Multi.new() |> Ecto.Multi.insert(:purchase, changeset) |> FYI.Multi.emit("purchase.created", fn %{purchase: p} -> %{payload: %{amount: p.amount, currency: p.currency}, actor: p.user_id} end) |> Repo.transaction()
This ensures events are only emitted after the transaction commits successfully.
Feedback Component
The installer creates a customizable feedback component in your codebase at lib/your_app_web/components/fyi/feedback_component.ex.
Use it in any LiveView:
import MyAppWeb.FYI.FeedbackComponent # In your template <.feedback_button />
Customize as needed:
<.feedback_button title="Report an Issue" button_label="Report" button_icon="🐛" categories={[{"bug", "Bug"}, {"ux", "UX Problem"}, {"other", "Other"}]} />
Since the component lives in your codebase, you can freely modify the Tailwind classes, add fields, or change the behavior.
Skip installing with mix fyi.install --no-feedback.
Admin Inbox
Add the route to your router (the installer prints this):
# In router.ex scope "/fyi", FYI.Web do pipe_through [:browser] live "/", InboxLive, :index live "/events/:id", InboxLive, :show end
Visit /fyi to see the event inbox with:
- Activity histogram with time-based tooltips
- Real-time event updates (requires PubSub config)
- Time range filtering (5 minutes to all time)
- Event type filtering
- Search by event name or actor
- Event detail panel with full payload
Real-time Updates
To enable real-time updates in the admin inbox, add your PubSub module:
config :fyi, pubsub: MyApp.PubSub
New events will appear instantly without refreshing the page.
Built-in Sinks
Slack Webhook
{FYI.Sink.SlackWebhook, %{ url: "https://hooks.slack.com/services/...", username: "FYI Bot", # optional icon_emoji: ":bell:" # optional }}
How to create a Slack webhook
- Go to api.slack.com/apps and click Create New App
- Choose From scratch, name it (e.g., "FYI"), and select your workspace
- Click Incoming Webhooks in the sidebar, then toggle it On
- Click Add New Webhook to Workspace and select the channel
- Copy the webhook URL — it looks like
https://hooks.slack.com/services/T00/B00/xxxx
Telegram Bot
{FYI.Sink.Telegram, %{ token: "123456:ABC-DEF...", chat_id: "-1001234567890", parse_mode: "HTML" # optional, default: "HTML" }}
How to create a Telegram bot
- Message @BotFather on Telegram
- Send
/newbotand follow the prompts to name your bot - Copy the token (looks like
123456789:ABCdefGHI...) - Add the bot to your group/channel and send a message
- Get your chat_id by visiting:
https://api.telegram.org/bot<TOKEN>/getUpdates- Look for
"chat":{"id":-1001234567890}in the response - Group IDs are negative numbers
- Look for
Custom Sinks
Implement the FYI.Sink behaviour:
defmodule MyApp.DiscordSink do @behaviour FYI.Sink @impl true def id, do: :discord @impl true def init(config) do {:ok, %{webhook_url: config.url}} end @impl true def deliver(event, state) do # POST to Discord webhook using FYI.Client for automatic retries case FYI.Client.post(state.webhook_url, json: %{content: event.name}) do {:ok, %{status: s}} when s in 200..299 -> :ok {:ok, resp} -> {:error, resp} {:error, err} -> {:error, err} end end end
Then add it to your config:
sinks: [ {MyApp.DiscordSink, %{url: "https://discord.com/api/webhooks/..."}} ]
Design Philosophy
FYI is intentionally simple:
- ❌ No Oban
- ❌ No durable queues or persistent job storage
- ✅ Fire-and-forget async delivery with automatic retries
- ✅ Phoenix + Ecto assumed
- ✅ Failures are logged, never block your application
Think "Oban Pro install experience", but for events + feedback.
HTTP Retries
FYI automatically retries failed HTTP requests to sinks using exponential backoff:
- Default: 3 retry attempts with delays of 1s, 2s, 4s
- Retry conditions: Network errors, 500-599 status codes
- Respects:
Retry-Afterresponse headers
Configure retry behavior:
# config/config.exs config :fyi, http_client: [ max_retries: 5, # default: 3 retry_delay: fn attempt -> attempt * 2000 end # custom delay function ]
Set max_retries: 0 to disable retries entirely.
Development
To use FYI locally without publishing to Hex:
# In your app's mix.exs {:fyi, path: "../fyi"}
License
MIT
