Admin Events

A product analytics system built on top of the admin panel. Track user events from your core application, then view them in a real-time feed and dashboard with bar charts, stacked charts, pie charts, and funnels. Each user's show page gets a per-user analytics view with an activity heatmap. Events are stored in the database and can be extended with custom handlers.

v1.8.3
Level: Advanced
23
2335
12
2 downloads

Related

Admin JS

The Bar , Funnel , and Pie hooks are registered alongside the existing CopyToClipboard hook in the admin LiveSocket.

Bar chart hook

Handles tooltip show/hide/position for bar and stacked bar charts. Formats date labels into human-readable strings (e.g. "Mon, 3 Mar 2026" for days, "March 2026" for months). Reads data-bar-label , data-bar-count , and data-bar-dataset attributes from each bar section. The activity heatmap reuses this hook unchanged — its day cells carry the same data-bar-* attributes.

Funnel chart hook

Similar tooltip handling for funnel steps. Reads data-funnel-label , data-funnel-count , and data-funnel-rate attributes.

Pie chart hook

Tooltip handling for pie and donut slices. Reads data-pie-label , data-pie-count , and data-pie-pct attributes so the tooltip can show both the absolute count and the slice's share.

Tracking events in the app

Two events are wired into the registration flow:

register_user/1 tracks "user.signed_up" after a successful insert, using with to preserve the original {:ok, user} return value.

login_user_by_magic_link/1 tracks "user.confirmed" in the branch where a previously unconfirmed user clicks their magic link. Already-confirmed users skip the tracking.

Events context

The public API is intentionally small: track/2 , subscribe/0 , and broadcast/1 . All query and analytics functions live in the admin layer.

track/2 inserts the event, broadcasts it over PubSub for the real-time feed, and notifies any registered handlers. Failures are logged rather than raised, so tracking never breaks the calling code path.

Handlers are notified synchronously after each event insert. Register them in config:

Event catalog

Maps event names to display properties: a human-readable title, an emoji icon, and a description function that receives the event struct. The catalog is used by the event feed, the dashboard charts, and the per-user analytics. Unknown events get a nil fallback so the UI always renders gracefully.

Event schema

A minimal schema with a @name_format regex that enforces lowercase dotted names like user.signed_up . The metadata field defaults to an empty map and stores arbitrary key-value data about the event. The belongs_to :user association links events to the user who triggered them.

Handler behaviour

A simple behaviour with a single handle_event/1 callback. Implement this to react to events, for example sending a welcome email when a user signs up. Handlers run synchronously after insert, keeping the architecture simple while allowing extension.

Admin event queries

All read queries and analytics live in the admin layer since they're only used by admin views.

list_latest/1 powers the event feed and the per-user event list with optional name, user, and single-day date filters. Events are preloaded with their user association and ordered newest-first. The date filter compares against ?::date so a heatmap day maps directly onto a UTC day of events.

aggregate/3 computes a count for a date range and its previous period of the same length, returning the current count, previous count, and percentage change. This drives the stat cards on the dashboard.

bar_chart/4 wraps aggregate for daily data and adds a monthly mode using date_trunc('month', ...) . Returns data shaped for the <.bar_chart> component.

stacked_bar_chart/3 runs a single grouped query for multiple event names and zero-fills each series. Uses catalog titles as dataset labels.

pie_chart/3 groups counts by event name, optionally within a date range, shaped for the <.pie_chart> component.

funnel_chart/3 counts distinct users per event and intersects the sets at each step, so each step shows users who completed that step and all previous steps.

activity_by_day/3 returns a %{Date => count} map of one user's events over a date range, powering the activity heatmap.

counts_by_name/2 returns a user's all-time event counts by name, descending — the source for the Top Events card.

Chart components

A set of function components for rendering analytics charts, imported into all admin views through html_helpers in my_app_admin_web.ex .

stat/1 renders a card with a label, value, and color-coded percentage change indicator.

bar_chart/1 renders a horizontal bar chart with CSS-based bars (no JS charting library). Each bar's height is computed as a percentage of the max value. The Bar phx-hook handles tooltip positioning on hover.

stacked_bar_chart/1 extends the bar chart for multiple series stacked per day. Colors are auto-assigned from a shared @series_colors palette with a legend.

pie_chart/1 renders a pie or donut ( style={:donut} ) with an HTML side legend showing counts and percentages. Arc paths are precomputed in Elixir, with a full-circle special case when a single slice makes up the whole total (an SVG arc from 0 to 2π is degenerate). Donuts show the total in the center.

funnel_chart/1 renders horizontal bars that taper from 100% down based on conversion rates. Shows counts and percentages at each step.

activity_heatmap/1 renders the contribution-style day grid: one column per week, cells colored in five intensity steps by event count. Cells scale fluidly with the card width up to a 16px cap, so the graph fills wide cards and shrinks to fit phones without scrolling. An optional day_click attribute makes cells clickable — the event is pushed with the cell's ISO date — and selected highlights the active day, letting views use the heatmap as a date filter.

All chart types share the public chart_card wrapper for consistent titles, icons, and subtitle bars — views can also use it directly for non-chart panels, as the Top Events and User Events cards do — plus a bar_tooltip component for hover states.

Responsive page headers

The shared header component stacks its title and actions vertically on small screens instead of forcing them into one row. Together with this, the search inputs on the Users and Admin Users index pages go full-width on mobile ( w-full sm:w-64 ), keeping the admin usable on phones.

Sidebar entry

An Events item is added to the App section of the admin sidebar, using the same signal icon the feed uses for uncataloged events.

Dashboard

The admin landing page renders four stat cards (30d, 90d, 6mo, 1yr registrations), two bar charts (daily and quarterly), a stacked bar chart comparing sign-ups vs confirmations, a registration funnel, and a pair of pie/donut charts breaking events down by type. All data is computed on mount from the query module.

Event feed

A real-time event feed using LiveView streams. New events arrive via PubSub subscription and are prepended to the stream. URL-based filters for event name and user ID are applied both to the initial query and to incoming broadcasts via matches_filter?/2 . Filter pills with clear buttons make it easy to drill into specific events or users.

Event detail

Displays the event's catalog title, icon, description, user email, IDs, and metadata key-value pairs. Each property is a click-to-copy button using the CopyToClipboard phx-hook with a data-copied attribute for visual feedback.

User analytics

The user show page becomes a per-user analytics view with three cards built from the event stream, all sharing the chart_card frame:

Activity — a GitHub-style contribution heatmap of the user's last 26 weeks. Clicking a day cell pushes select_day , which re-queries the event list for that UTC day and highlights the cell; clicking the same cell again (or the Clear control) resets the filter.

Top Events — the user's five most frequent events of all time, each with a bar scaled proportionally against the most frequent one.

User Events — the latest 20 events scoped to this user, or every event on the selected heatmap day. The subtitle swaps between "N most recent events" and "N events on {date}" so the active filter is always visible. Each row is a link to the event detail page, and a View all link jumps to the global feed pre-filtered to this user.

The private event_icon component renders the catalog emoji when one exists and falls back to a generic signal icon, mirroring the global feed's treatment of uncataloged events.

Routes

Two routes join the existing authenticated admin live_session : the event feed at /admin/events and event detail at /admin/events/:id .

Migration

Creates the events table with name , metadata (JSON map), user_id , and organization_id . The updated_at timestamp is omitted since events are append-only. Indexes on name , inserted_at , user_id , organization_id , and a composite (name, inserted_at) index support the admin query patterns.

Tests

Covers track/2 : event creation with name, metadata, and user, name format validation, and the metadata default. test/my_app_admin/query/events_test.exs exercises the analytics queries — list_latest/1 filters and ordering, aggregate/3 period-over-period math, chart shaping, and funnel intersection — using test/support/fixtures/events_fixtures.ex , which creates events through the real track/2 path.