{"id":35145858,"url":"https://github.com/chrisgreg/fyi","last_synced_at":"2026-01-01T23:03:33.610Z","repository":{"id":330606121,"uuid":"1123342182","full_name":"chrisgreg/fyi","owner":"chrisgreg","description":"In-app events \u0026 feedback with Slack/Telegram notifications for Phoenix","archived":false,"fork":false,"pushed_at":"2025-12-28T16:49:26.000Z","size":5199,"stargazers_count":55,"open_issues_count":0,"forks_count":2,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-12-30T22:49:44.910Z","etag":null,"topics":["analytics","analytics-product","elixir","feedback-systems","igniter","phoenix-framework","phoenix-liveview"],"latest_commit_sha":null,"homepage":"https://hexdocs.pm/fyi/readme.html","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/chrisgreg.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-26T17:09:52.000Z","updated_at":"2025-12-30T13:41:40.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/chrisgreg/fyi","commit_stats":null,"previous_names":["chrisgreg/fyi"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/chrisgreg/fyi","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgreg%2Ffyi","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgreg%2Ffyi/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgreg%2Ffyi/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgreg%2Ffyi/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/chrisgreg","download_url":"https://codeload.github.com/chrisgreg/fyi/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/chrisgreg%2Ffyi/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28146400,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-12-31T02:00:06.200Z","response_time":55,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analytics","analytics-product","elixir","feedback-systems","igniter","phoenix-framework","phoenix-liveview"],"created_at":"2025-12-28T13:59:11.446Z","updated_at":"2026-01-01T23:03:33.585Z","avatar_url":"https://github.com/chrisgreg.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eFYI\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://hex.pm/packages/fyi\"\u003e\u003cimg src=\"https://img.shields.io/hexpm/v/fyi.svg\" alt=\"Hex.pm\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://hexdocs.pm/fyi\"\u003e\u003cimg src=\"https://img.shields.io/badge/hex-docs-blue.svg\" alt=\"Hex Docs\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://hex.pm/packages/fyi\"\u003e\u003cimg src=\"https://img.shields.io/hexpm/dt/fyi.svg\" alt=\"Downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/chrisgreg/fyi/blob/main/LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/hexpm/l/fyi.svg\" alt=\"License\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003eKnow what's happening in your app.\u003c/strong\u003e\u003c/p\u003e\n\n---\n\nIn-app events, user feedback, and instant Slack/Telegram notifications for Phoenix.\n\nStop refreshing your database to see if users are signing up. FYI gives you:\n\n- 📤 **Event tracking** — Emit events from anywhere in your app with one line of code\n- 📊 **Live dashboard** — Beautiful admin UI with search, filtering, and activity histograms\n- 💬 **Feedback widget** — Drop-in component to collect user feedback (installs into your codebase)\n- 🔔 **Instant notifications** — Get pinged in Slack or Telegram when important things happen\n- 🎯 **Smart routing** — Send specific events to specific channels with glob patterns\n- 🚀 **One-command setup** — `mix fyi.install` handles migrations, config, and routes\n\n![FYI Admin Inbox](screenshot.png)\n\n## Installation\n\nAdd `fyi` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:fyi, \"~\u003e 1.0.0\"}\n  ]\nend\n```\n\nThen run the installer:\n\n```bash\nmix deps.get\nmix fyi.install\n```\n\nThis will:\n1. Add `FYI.Application` to your supervision tree\n2. Create a migration for the `fyi_events` table\n3. Print instructions to add the `/fyi` route to your router\n4. Add configuration stubs to your config files\n\n### Installer Options\n\n- `--no-ui` — Skip installing the admin inbox UI\n- `--no-persist` — Skip the database migration (events won't be persisted)\n- `--no-feedback` — Skip installing the feedback component\n\n## Configuration\n\n```elixir\n# config/config.exs\nconfig :fyi,\n  app_name: \"MyApp\",\n  persist_events: true,\n  repo: MyApp.Repo,\n  sinks: [\n    {FYI.Sink.SlackWebhook, %{url: System.get_env(\"SLACK_WEBHOOK_URL\")}},\n    {FYI.Sink.Telegram, %{\n      token: System.get_env(\"TELEGRAM_BOT_TOKEN\"),\n      chat_id: System.get_env(\"TELEGRAM_CHAT_ID\")\n    }}\n  ],\n  routes: [\n    %{match: \"waitlist.*\", sinks: [:slack]},\n    %{match: \"purchase.*\", sinks: [:slack, :telegram]},\n    %{match: \"feedback.*\", sinks: [:slack]}\n  ]\n```\n\n### App Name\n\nSet `app_name` to identify events when multiple apps share the same Slack channel or Telegram chat:\n\n```elixir\nconfig :fyi, app_name: \"MyApp\"\n```\n\nMessages will include the app name: `[MyApp] *purchase.created* by user_123`\n\n### Emojis\n\nAdd emojis to your notifications in three ways (in priority order):\n\n**1. Per-event override:**\n```elixir\nFYI.emit(\"error.critical\", %{message: \"DB down\"}, emoji: \"🚨\")\n```\n\n**2. Pattern-based mapping:**\n```elixir\nconfig :fyi,\n  emojis: %{\n    \"purchase.*\" =\u003e \"💰\",\n    \"user.signup\" =\u003e \"👋\",\n    \"feedback.*\" =\u003e \"💬\",\n    \"error.*\" =\u003e \"🚨\"\n  }\n```\n\n**3. Default fallback:**\n```elixir\nconfig :fyi, emoji: \"📣\"\n```\n\nMessages will show as: `💰 [MyApp] *purchase.created* by user_123`\n\n### Routing\n\nRoutes use simple glob matching:\n- `purchase.*` matches `purchase.created`, `purchase.updated`, etc.\n- `*` at the end matches any suffix\n\nIf no routes are configured, all events go to all sinks.\n\n## Usage\n\n### Emit an Event\n\n```elixir\nFYI.emit(\"purchase.created\", %{amount: 4900, currency: \"GBP\"}, actor: user_id)\n\nFYI.emit(\"user.signup\", %{email: \"user@example.com\"}, source: \"landing_page\")\n\nFYI.emit(\"error.critical\", %{message: \"DB connection failed\"}, emoji: \"🚨\", tags: %{env: \"prod\"})\n```\n\nOptions:\n- `:actor` - who triggered the event (user_id, email, etc.)\n- `:source` - where the event originated (e.g., \"api\", \"web\", \"worker\")\n- `:tags` - additional metadata map for filtering\n- `:emoji` - override emoji for this specific event\n\n### Emit from Ecto.Multi (Recommended)\n\n```elixir\nEcto.Multi.new()\n|\u003e Ecto.Multi.insert(:purchase, changeset)\n|\u003e FYI.Multi.emit(\"purchase.created\", fn %{purchase: p} -\u003e\n  %{payload: %{amount: p.amount, currency: p.currency}, actor: p.user_id}\nend)\n|\u003e Repo.transaction()\n```\n\nThis ensures events are only emitted after the transaction commits successfully.\n\n### Feedback Component\n\nThe installer creates a customizable feedback component in your codebase at `lib/your_app_web/components/fyi/feedback_component.ex`.\n\nUse it in any LiveView:\n\n```elixir\nimport MyAppWeb.FYI.FeedbackComponent\n\n# In your template\n\u003c.feedback_button /\u003e\n```\n\nCustomize as needed:\n\n```heex\n\u003c.feedback_button\n  title=\"Report an Issue\"\n  button_label=\"Report\"\n  button_icon=\"🐛\"\n  categories={[{\"bug\", \"Bug\"}, {\"ux\", \"UX Problem\"}, {\"other\", \"Other\"}]}\n/\u003e\n```\n\nSince the component lives in your codebase, you can freely modify the Tailwind classes, add fields, or change the behavior.\n\nSkip installing with `mix fyi.install --no-feedback`.\n\n### Admin Inbox\n\nAdd the route to your router (the installer prints this):\n\n```elixir\n# In router.ex\nscope \"/fyi\", FYI.Web do\n  pipe_through [:browser]\n  live \"/\", InboxLive, :index\n  live \"/events/:id\", InboxLive, :show\nend\n```\n\nVisit `/fyi` to see the event inbox with:\n- Activity histogram with time-based tooltips\n- Real-time event updates (requires PubSub config)\n- Time range filtering (5 minutes to all time)\n- Event type filtering\n- Search by event name or actor\n- Event detail panel with full payload\n\n### Real-time Updates\n\nTo enable real-time updates in the admin inbox, add your PubSub module:\n\n```elixir\nconfig :fyi, pubsub: MyApp.PubSub\n```\n\nNew events will appear instantly without refreshing the page.\n\n## Built-in Sinks\n\n### Slack Webhook\n\n```elixir\n{FYI.Sink.SlackWebhook, %{\n  url: \"https://hooks.slack.com/services/...\",\n  username: \"FYI Bot\",      # optional\n  icon_emoji: \":bell:\"      # optional\n}}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eHow to create a Slack webhook\u003c/summary\u003e\n\n1. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App**\n2. Choose **From scratch**, name it (e.g., \"FYI\"), and select your workspace\n3. Click **Incoming Webhooks** in the sidebar, then toggle it **On**\n4. Click **Add New Webhook to Workspace** and select the channel\n5. Copy the webhook URL — it looks like `https://hooks.slack.com/services/T00/B00/xxxx`\n\n\u003c/details\u003e\n\n### Telegram Bot\n\n```elixir\n{FYI.Sink.Telegram, %{\n  token: \"123456:ABC-DEF...\",\n  chat_id: \"-1001234567890\",\n  parse_mode: \"HTML\"         # optional, default: \"HTML\"\n}}\n```\n\n\u003cdetails\u003e\n\u003csummary\u003eHow to create a Telegram bot\u003c/summary\u003e\n\n1. Message [@BotFather](https://t.me/botfather) on Telegram\n2. Send `/newbot` and follow the prompts to name your bot\n3. Copy the **token** (looks like `123456789:ABCdefGHI...`)\n4. Add the bot to your group/channel and send a message\n5. Get your **chat_id** by visiting: `https://api.telegram.org/bot\u003cTOKEN\u003e/getUpdates`\n   - Look for `\"chat\":{\"id\":-1001234567890}` in the response\n   - Group IDs are negative numbers\n\n\u003c/details\u003e\n\n## Custom Sinks\n\nImplement the `FYI.Sink` behaviour:\n\n```elixir\ndefmodule MyApp.DiscordSink do\n  @behaviour FYI.Sink\n\n  @impl true\n  def id, do: :discord\n\n  @impl true\n  def init(config) do\n    {:ok, %{webhook_url: config.url}}\n  end\n\n  @impl true\n  def deliver(event, state) do\n    # POST to Discord webhook using FYI.Client for automatic retries\n    case FYI.Client.post(state.webhook_url, json: %{content: event.name}) do\n      {:ok, %{status: s}} when s in 200..299 -\u003e :ok\n      {:ok, resp} -\u003e {:error, resp}\n      {:error, err} -\u003e {:error, err}\n    end\n  end\nend\n```\n\nThen add it to your config:\n\n```elixir\nsinks: [\n  {MyApp.DiscordSink, %{url: \"https://discord.com/api/webhooks/...\"}}\n]\n```\n\n## Design Philosophy\n\nFYI is intentionally simple:\n\n- ❌ No Oban\n- ❌ No durable queues or persistent job storage\n- ✅ Fire-and-forget async delivery with automatic retries\n- ✅ Phoenix + Ecto assumed\n- ✅ Failures are logged, never block your application\n\nThink \"Oban Pro install experience\", but for events + feedback.\n\n### HTTP Retries\n\nFYI automatically retries failed HTTP requests to sinks using exponential backoff:\n\n- **Default**: 3 retry attempts with delays of 1s, 2s, 4s\n- **Retry conditions**: Network errors, 500-599 status codes\n- **Respects**: `Retry-After` response headers\n\nConfigure retry behavior:\n\n```elixir\n# config/config.exs\nconfig :fyi,\n  http_client: [\n    max_retries: 5,  # default: 3\n    retry_delay: fn attempt -\u003e attempt * 2000 end  # custom delay function\n  ]\n```\n\nSet `max_retries: 0` to disable retries entirely.\n\n## Development\n\nTo use FYI locally without publishing to Hex:\n\n```elixir\n# In your app's mix.exs\n{:fyi, path: \"../fyi\"}\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisgreg%2Ffyi","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fchrisgreg%2Ffyi","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fchrisgreg%2Ffyi/lists"}