FYI

Hex.pmHex DocsDownloadsLicense

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:

FYI Admin Inbox

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:

  1. Add FYI.Application to your supervision tree
  2. Create a migration for the fyi_events table
  3. Print instructions to add the /fyi route to your router
  4. Add configuration stubs to your config files

Installer Options

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, :boop]},
%{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:

config :fyi, emoji: "📣"

Messages will show as: 💰 [MyApp] *purchase.created* by user_123

Routing

Routes use simple glob matching:

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:

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:

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
  1. Go to api.slack.com/apps and click Create New App
  2. Choose From scratch, name it (e.g., "FYI"), and select your workspace
  3. Click Incoming Webhooks in the sidebar, then toggle it On
  4. Click Add New Webhook to Workspace and select the channel
  5. 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
  1. Message @BotFather on Telegram
  2. Send /newbot and follow the prompts to name your bot
  3. Copy the token (looks like 123456789:ABCdefGHI...)
  4. Add the bot to your group/channel and send a message
  5. 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

Boop (push to your phone)

Boop is a tiny self-hosted notification inbox with an iOS app. Point the sink at your server with a project API key and every routed event becomes a push:

{FYI.Sink.Boop, %{
url: System.get_env("BOOP_URL"), # https://boop.example.com
api_key: System.get_env("BOOP_API_KEY"), # boop_proj_... from the Boop web UI
source: "my_app", # optional
levels: %{"user.*" => "success", "purchase.*" => "success", "*.failed" => "error"}, # optional
dashboard_url: "https://my-app.com/admin/fyi" # optional: "Open in FYI" button on the push
}}

The event name becomes the Boop fingerprint, so repeats group into one inbox row with a count; the payload is the notification body; actor, tags and the full payload are attached as data. Add actions: fn event -> [%{label: "Open user", url: ...}] end for extra buttons (three at most, including the dashboard one).

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:

Think "Oban Pro install experience", but for events + feedback.

HTTP Retries

FYI automatically retries failed HTTP requests to sinks using exponential backoff:

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