Skip to content

feat: Elixir upgrade to 1.19.5 (OTP 26) + PostHog analytics#17

Merged
timrodz merged 20 commits into
mainfrom
feat/elixir-upgrade-1.19.5-otp-26
Feb 21, 2026
Merged

feat: Elixir upgrade to 1.19.5 (OTP 26) + PostHog analytics#17
timrodz merged 20 commits into
mainfrom
feat/elixir-upgrade-1.19.5-otp-26

Conversation

@timrodz

@timrodz timrodz commented Feb 21, 2026

Copy link
Copy Markdown
Owner

Also patches issues from mix credo and cleans up service utilities that are placed outside services.

Removes dead code too.

Summary by CodeRabbit

  • New Features

    • Added analytics event tracking for user signups, tournament creation, tournament deletion, and round completion.
  • Chores

    • Updated runtime dependencies and Elixir to version 1.19.
    • Optimized Docker image builds with improved layer structure and Debian variant.
    • Removed legacy rate limiting infrastructure.
    • Increased test coverage threshold and improved code organization.

@coderabbitai

coderabbitai Bot commented Feb 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR upgrades the Elixir/OTP toolchain to 1.19.5, integrates PostHog analytics throughout the application, refactors tournament rendering components into a localized module, removes the rate-limiting infrastructure, updates multiple core dependencies, and optimizes the Docker build configuration.

Changes

Cohort / File(s) Summary
Toolchain & Build Configuration
.tool-versions, .github/workflows/ci.yml, Dockerfile, .dockerignore, rel/overlays/bin/migrate, rel/overlays/bin/server
Updated Elixir from 1.15.x to 1.19.5 and Erlang from 26.0.2 to 26 across all configuration files. Dockerfile multi-stage build refactored with explicit docker.io image references, bookworm-20260202-slim Debian variant, consolidated apt-get invocations, and stricter cleanup. Shell scripts now enforce error handling via set -eu.
PostHog Analytics Integration
config/config.exs, config/dev.exs, config/prod.exs, config/test.exs, lib/mtg_friends/analytics.ex
Added PostHog configuration blocks across all environments with enable/test_mode/api_key settings. Introduced new MtgFriends.Analytics module with public functions for capturing user signup, tournament creation/deletion, and round completion events, gated by posthog_ready? check.
Analytics Event Capture
lib/mtg_friends/tournaments.ex, lib/mtg_friends/rounds.ex, lib/mtg_friends_web/live/user_confirmation_live.ex
Added analytics capture calls on tournament creation/deletion and round completion. User confirmation now captures signup event post-validation. Rounds module tracks status transitions to finished state.
Tournament Rendering Refactor
lib/mtg_friends_web/components/tournament_components.ex, lib/mtg_friends_web/components/extended_core_components.ex, lib/mtg_friends/tournament_renderer.ex, lib/mtg_friends_web/controllers/landing_html.ex
Deleted MtgFriends.TournamentRenderer module (119 lines). Removed 6 public functions from ExtendedCoreComponents (215 lines). Relocated rendering helpers to TournamentComponents with new render_status/render_round_status/render_format/render_subformat functions. Removed "Open Source" feature from landing page.
Rate Limiting Removal
lib/mtg_friends_web/rate_limit.ex, lib/mtg_friends/application.ex
Deleted MtgFriendsWeb.RateLimit module. Removed RateLimit child from supervision tree in Application startup.
Dependency Updates & Configuration
mix.exs, lib/mtg_friends/release.ex, lib/mtg_friends_web/components/core_components.ex, lib/mtg_friends_web/live/tournament_live/*.ex, test/support/data_case.ex
Bumped Elixir requirement to > 1.19. Updated ecto_sql (3.6→3.13), postgrex (0.0.0→0.22.0), finch (0.13→0.21), jason (1.2→1.4.4), plug_cowboy (2.5→2.8), poison (5.0→6.0). Removed hammer dependency. Added posthog (> 2.4.0). Added Phoenix.CodeReloader listeners. Increased test coverage threshold to 78.20. Release module now ensures SSL startup before loading app. Minor refactorings: aliased Form module in CoreComponents, added Sandbox alias in DataCase, refactored round/tournament LiveView logic to use Tournaments helper functions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • PR #9 — Modifies render_* functions in the TournamentRenderer module, which this PR deletes entirely, creating a direct code-level conflict that must be reconciled.
  • PR #15 — Updates authorization helpers and gates tournament write actions in LiveView/controllers, complementing the tournament management logic refactored in this PR.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 68.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the two main objectives of the changeset: upgrading Elixir to version 1.19.5 with OTP 26 and adding PostHog analytics integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/mtg_friends_web/live/tournament_live/round.ex (2)

28-58: ⚠️ Potential issue | 🟠 Major

Redundant DB query — round is fetched twice on :edit action

get_pairing_id_from_number/3 (line 41→206) fetches the round via Rounds.get_round_from_round_number_str!, and then generate_socket/4 (line 44→62) fetches the same round again. On every edit-pairing navigation, this doubles the DB load for that round (including preloaded pairings).

Consider restructuring so the round is fetched once and passed to both the pairing lookup and socket generation:

Sketch
   defp apply_action(socket, :edit, %{...}) do
     case UserAuth.ensure_can_manage_tournament_id(...) do
       {:ok, socket} ->
-        with {pairing_number, ""} when pairing_number > 0 <- Integer.parse(pairing_number_str),
-             {:ok, pairing_id} <-
-               get_pairing_id_from_number(tournament_id, round_number, pairing_number) do
+        round = Rounds.get_round_from_round_number_str!(tournament_id, round_number)
+        with {pairing_number, ""} when pairing_number > 0 <- Integer.parse(pairing_number_str),
+             {:ok, pairing_id} <- get_pairing_id(round, pairing_number) do
           socket
           |> assign(:selected_pairing_id, pairing_id)
-          |> generate_socket(tournament_id, round_number, :edit)
+          |> generate_socket_from_round(round, :edit)

92-96: 🧹 Nitpick | 🔵 Trivial

Silent no-op when timer with doesn't match — consider adding an else or simplifying

The with block on lines 92–96 has no else clause, so if timer_reference is nil, it silently falls through returning nil (the result of Map.get). This works but the intent is obscured. A simple if would be clearer:

Suggested simplification
-    with timer_reference <- Map.get(socket.assigns, :timer_reference),
-         false <- is_nil(timer_reference) do
-      {:ok, ref} = timer_reference
-      :timer.cancel(ref)
-    end
+    case Map.get(socket.assigns, :timer_reference) do
+      {:ok, ref} -> :timer.cancel(ref)
+      _ -> :ok
+    end

Comment thread config/config.exs Outdated
Comment thread config/prod.exs Outdated
Comment thread config/test.exs
Comment thread lib/mtg_friends_web/components/tournament_components.ex Outdated
Comment thread lib/mtg_friends/analytics.ex
Comment thread lib/mtg_friends/analytics.ex Outdated
Comment thread lib/mtg_friends/rounds.ex
Comment on lines +311 to +326
defp maybe_capture_round_finished(
%Round{status: previous_status},
attrs,
%Round{} = updated_round
) do
with :finished <- normalize_status(Map.get(attrs, :status) || Map.get(attrs, "status")),
true <- previous_status != :finished do
Analytics.capture_round_finished(
updated_round.tournament_id,
updated_round.id,
updated_round.number
)
end

:ok
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider reading updated_round.status instead of re-inspecting attrs.

The current implementation reads the new status from attrs rather than the canonical updated_round.status. For the only call site today (%{status: :finished}), this is equivalent, but it's more fragile: a changeset that coerces or normalises the value would cause a mismatch.

♻️ Suggested refactor
 defp maybe_capture_round_finished(
        %Round{status: previous_status},
-       attrs,
+       _attrs,
        %Round{} = updated_round
      ) do
-  with :finished <- normalize_status(Map.get(attrs, :status) || Map.get(attrs, "status")),
-       true <- previous_status != :finished do
+  with :finished <- normalize_status(updated_round.status),
+       true <- previous_status != :finished do

Comment thread lib/mtg_friends/tournaments.ex

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Comment thread config/runtime.exs Outdated
Comment thread lib/mtg_friends/tournaments.ex Outdated
@timrodz
timrodz force-pushed the feat/elixir-upgrade-1.19.5-otp-26 branch from f65fdb5 to 66c783b Compare February 21, 2026 08:53

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/mtg_friends_web/live/tournament_live/tournament_edit_form_component.ex (2)

134-136: ⚠️ Potential issue | 🟠 Major

Atom table exhaustion risk with String.to_atom/1 on user input.

Using String.to_atom/1 on tournament_params["game_code"] from user input can cause atom table exhaustion (atoms are never garbage collected). This is a potential denial-of-service vector.

Use String.to_existing_atom/1 instead, which only converts to atoms that already exist in the runtime.

🛡️ Proposed fix
   defp handle_event("validate", %{"tournament" => tournament_params}, socket) do
     selected_game_code =
-      tournament_params["game_code"] |> String.to_atom()
+      tournament_params["game_code"] |> String.to_existing_atom()

As per coding guidelines: "Don't use String.to_atom/1 on user input due to memory leak risk"


145-147: ⚠️ Potential issue | 🟠 Major

Same atom exhaustion risk here.

String.to_atom/1 on user-provided format string poses the same DoS risk.

🛡️ Proposed fix
         false ->
-          tournament_params["format"] |> String.to_atom()
+          tournament_params["format"] |> String.to_existing_atom()

Comment thread Dockerfile
Comment thread lib/mtg_friends/tournaments.ex Outdated
Comment on lines +270 to +274
# 5. Verify the updated points appear correctly on the new page
{:ok, round_view, _html} = live(conn, ~p"/tournaments/#{tournament.id}/rounds/1")

assert render(round_view) =~ "3 pts"
assert render(round_view) =~ "1 pts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace raw HTML assertions with element-based checks.

Line 273-274 asserts on raw HTML strings, which makes LiveView tests brittle and violates the project’s test guidelines. Prefer has_element?/2 (ideally with stable IDs or data-testid attributes) instead of render/1 string matching.

✅ Example adjustment (update selector to match your markup)
-      assert render(round_view) =~ "3 pts"
-      assert render(round_view) =~ "1 pts"
+      assert has_element?(round_view, "span", "3 pts")
+      assert has_element?(round_view, "span", "1 pts")

As per coding guidelines: “Never test against raw HTML. Always use element/2, has_element/2…” and “Instead of relying on testing text content, which can change, favor testing for the presence of key elements.”

@timrodz
timrodz merged commit 94301a6 into main Feb 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant