diff --git a/lib/mixer/metrics/buffer.ex b/lib/mixer/metrics/buffer.ex index 23366e0..3078cff 100644 --- a/lib/mixer/metrics/buffer.ex +++ b/lib/mixer/metrics/buffer.ex @@ -128,14 +128,18 @@ defmodule Mixer.Metrics.Buffer do defp do_flush(events) do rows = Enum.reverse(events) + count = length(rows) try do - {count, _} = Mixer.ClickhouseRepo.insert_all(PostEvent, rows) + # ClickHouse async inserts acknowledge writes immediately and always + # return num_rows: 0 — the data is queued for background commitment. + # We use our own row count for the log so it is always accurate. + Mixer.ClickhouseRepo.insert_all(PostEvent, rows) Logger.debug("[Mixer.Metrics.Buffer] Flushed #{count} event(s) to ClickHouse") rescue error -> Logger.error( - "[Mixer.Metrics.Buffer] Failed to flush #{length(rows)} event(s) to ClickHouse: " <> + "[Mixer.Metrics.Buffer] Failed to flush #{count} event(s) to ClickHouse: " <> Exception.message(error) ) end diff --git a/lib/mixer/metrics/post_event.ex b/lib/mixer/metrics/post_event.ex index 191d6d0..71cb189 100644 --- a/lib/mixer/metrics/post_event.ex +++ b/lib/mixer/metrics/post_event.ex @@ -22,19 +22,23 @@ defmodule Mixer.Metrics.PostEvent do @primary_key false schema "post_events" do - # LowCardinality(String) in ClickHouse — keep values in the set above - field :event_type, :string + # Must be Ch-typed so ecto_ch emits LowCardinality(String) in the RowBinary + # header, matching the ClickHouse table DDL exactly. + field :event_type, Ch, type: "LowCardinality(String)" # The tweet that the event relates to field :tweet_id, Ecto.UUID - # The acting user; may be nil for anonymous views - field :user_id, Ecto.UUID + # The acting user; may be nil for anonymous views. + # Must be Ch-typed so ecto_ch emits Nullable(UUID) in the RowBinary header, + # matching the ClickHouse table DDL exactly. + field :user_id, Ch, type: "Nullable(UUID)" # Wall-clock time of the event (UTC, second precision) field :occurred_at, :utc_datetime - # Optional originating IP, useful for deduplicating anonymous views - field :ip_address, :string + # Optional originating IP, useful for deduplicating anonymous views. + # Nullable(String) for the same reason as user_id above. + field :ip_address, Ch, type: "Nullable(String)" end end diff --git a/lib/mixer/posts/tweet.ex b/lib/mixer/posts/tweet.ex index 3b0f984..5bd840e 100644 --- a/lib/mixer/posts/tweet.ex +++ b/lib/mixer/posts/tweet.ex @@ -66,6 +66,26 @@ defmodule Mixer.Posts.Tweet do end) end end + + # Track a "comment" metric event whenever a reply is posted. We record + # the event against the *parent* tweet so that `get_summary/1` and + # `get_bulk_summaries/1` can count how many comments each tweet received. + change fn changeset, context -> + case Ash.Changeset.get_attribute(changeset, :parent_tweet_id) do + nil -> + changeset + + parent_tweet_id -> + Ash.Changeset.after_action(changeset, fn _changeset, tweet -> + Mixer.Metrics.track_comment( + parent_tweet_id, + user_id: context.actor && context.actor.id + ) + + {:ok, tweet} + end) + end + end end update :update do @@ -80,6 +100,7 @@ defmodule Mixer.Posts.Tweet do Ash.Changeset.after_action(changeset, fn _changeset, tweet -> case ensure_like(tweet, context.actor) do {:created, _like} -> + Mixer.Metrics.track_like(tweet.id, user_id: context.actor && context.actor.id) increment_likes(tweet, context.actor) {:noop, _like} -> @@ -100,6 +121,7 @@ defmodule Mixer.Posts.Tweet do Ash.Changeset.after_action(changeset, fn _changeset, tweet -> case remove_like(tweet, context.actor) do {:deleted, _like} -> + Mixer.Metrics.track_unlike(tweet.id, user_id: context.actor && context.actor.id) decrement_likes(tweet, context.actor) {:noop, _like} -> diff --git a/lib/mixer_web/controllers/page_controller.ex b/lib/mixer_web/controllers/page_controller.ex index ac6467e..f2349f0 100644 --- a/lib/mixer_web/controllers/page_controller.ex +++ b/lib/mixer_web/controllers/page_controller.ex @@ -16,6 +16,8 @@ defmodule MixerWeb.PageController do end def show(conn, %{"tweet_id" => tweet_id}) do + user_id = conn.assigns[:current_user] && conn.assigns[:current_user].id + Mixer.Metrics.track_view(tweet_id, user_id: user_id, ip_address: conn.remote_ip) render_spa(conn, %{page: "tweet", tweet_id: tweet_id, user_id: nil}) end