Hex.pm Hex Docs Downloads License

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 setupmix fyi.install handles migrations, config, and routes

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

  • --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,
  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:

config :fyi, emoji: "📣"

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

Routing

Routes use simple glob matching:

  • purchase.* matches purchase.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
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 1. Go to [api.slack.com/apps](https://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](https://t.me/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/getUpdates` - Look for `"chat":{"id":-1001234567890}` in the response - Group IDs are negative numbers

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-After response 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