Working metrics for all forms of interactions and updated .env.example

This commit is contained in:
2026-04-07 00:09:10 -04:00
parent 76a8acc731
commit 3c9910a723
4 changed files with 89 additions and 24 deletions

View File

@@ -27,3 +27,14 @@ S3_VIRTUAL_HOST=false
# Email (Brevo) # Email (Brevo)
BREVO_API_KEY=your-brevo-api-key BREVO_API_KEY=your-brevo-api-key
# ClickHouse (analytics / metrics)
# single connection URL (overrides all individual vars below)
CLICKHOUSE_URL=http://default:password@localhost:8123/mixer_metrics
# individual vars (used when CLICKHOUSE_URL is not set)
CLICKHOUSE_HOST=localhost
CLICKHOUSE_PORT=8123
CLICKHOUSE_DATABASE=mixer_metrics
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=
CLICKHOUSE_SCHEME=http

View File

@@ -33,7 +33,8 @@ defmodule Mixer.Metrics do
# Event types # Event types
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@type event_type :: :view | :like | :unlike | :comment | :share @type event_type ::
:view | :post | :comment | :like | :unlike | :share | :delete_post | :delete_comment
@type track_opt :: @type track_opt ::
{:user_id, binary() | nil} {:user_id, binary() | nil}
@@ -70,6 +71,33 @@ defmodule Mixer.Metrics do
@spec track_share(binary(), [track_opt()]) :: :ok @spec track_share(binary(), [track_opt()]) :: :ok
def track_share(tweet_id, opts \\ []), do: enqueue("share", tweet_id, opts) def track_share(tweet_id, opts \\ []), do: enqueue("share", tweet_id, opts)
@doc """
Track a new top-level tweet being published.
The event is recorded against the new tweet's own ID.
"""
@spec track_post(binary(), [track_opt()]) :: :ok
def track_post(tweet_id, opts \\ []), do: enqueue("post", tweet_id, opts)
@doc """
Track a top-level tweet being deleted.
The event is recorded against the deleted tweet's ID.
Note: cascade-deleted comments are not individually tracked — only the
explicit user-initiated destroy action emits this event.
"""
@spec track_delete_post(binary(), [track_opt()]) :: :ok
def track_delete_post(tweet_id, opts \\ []), do: enqueue("delete_post", tweet_id, opts)
@doc """
Track a comment (reply) being deleted.
The event is recorded against the *parent* tweet's ID so that
`get_summary/1` can reflect net comment activity on a tweet.
"""
@spec track_delete_comment(binary(), [track_opt()]) :: :ok
def track_delete_comment(tweet_id, opts \\ []), do: enqueue("delete_comment", tweet_id, opts)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Query helpers # Query helpers
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@@ -8,13 +8,16 @@ defmodule Mixer.Metrics.PostEvent do
## Event types ## Event types
| event_type | Description | | event_type | `tweet_id` refers to | Description |
|-------------|------------------------------------------| |--------------------|-----------------------|-------------------------------------------------|
| `"view"` | A tweet was displayed to a user | | `"view"` | the viewed tweet | Tweet detail page was loaded |
| `"like"` | A user liked a tweet | | `"post"` | the new tweet | A new top-level tweet was published |
| `"unlike"` | A user removed their like from a tweet | | `"comment"` | the parent tweet | A reply was posted; count against the parent |
| `"comment"` | A user replied to a tweet | | `"like"` | the liked tweet | A user liked a tweet |
| `"share"` | A user shared / reposted a tweet | | `"unlike"` | the unliked tweet | A user removed their like |
| `"share"` | the shared tweet | A user shared / reposted a tweet |
| `"delete_post"` | the deleted tweet | A top-level tweet was deleted by its author |
| `"delete_comment"` | the parent tweet | A reply was deleted; count against the parent |
""" """
use Ecto.Schema use Ecto.Schema

View File

@@ -32,7 +32,7 @@ defmodule Mixer.Posts.Tweet do
end end
actions do actions do
defaults [:read, :destroy] defaults [:read]
read :following_feed do read :following_feed do
filter expr( filter expr(
@@ -67,25 +67,48 @@ defmodule Mixer.Posts.Tweet do
end end
end end
# Track a "comment" metric event whenever a reply is posted. We record # Track post / comment creation metrics.
# the event against the *parent* tweet so that `get_summary/1` and # Root tweets emit a "post" event recorded against their own ID.
# `get_bulk_summaries/1` can count how many comments each tweet received. # Replies emit a "comment" event recorded against the parent tweet ID so
# that `get_summary/1` can count how many replies a tweet has received.
change fn changeset, context -> change fn changeset, context ->
case Ash.Changeset.get_attribute(changeset, :parent_tweet_id) do parent_tweet_id = Ash.Changeset.get_attribute(changeset, :parent_tweet_id)
nil -> user_id = context.actor && context.actor.id
changeset
parent_tweet_id ->
Ash.Changeset.after_action(changeset, fn _changeset, tweet -> Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
Mixer.Metrics.track_comment( if parent_tweet_id do
parent_tweet_id, Mixer.Metrics.track_comment(parent_tweet_id, user_id: user_id)
user_id: context.actor && context.actor.id else
) Mixer.Metrics.track_post(tweet.id, user_id: user_id)
end
{:ok, tweet} {:ok, tweet}
end) end)
end end
end end
# Explicit destroy so we can attach a metrics hook. The policy and cascade
# behaviour are identical to the previous default :destroy action.
destroy :destroy do
require_atomic? false
change fn changeset, context ->
# Capture the record's identity *before* deletion — after the action
# completes the row no longer exists.
tweet_id = changeset.data.id
parent_tweet_id = changeset.data.parent_tweet_id
user_id = context.actor && context.actor.id
Ash.Changeset.after_action(changeset, fn _changeset, result ->
if parent_tweet_id do
Mixer.Metrics.track_delete_comment(parent_tweet_id, user_id: user_id)
else
Mixer.Metrics.track_delete_post(tweet_id, user_id: user_id)
end
{:ok, result}
end)
end
end end
update :update do update :update do