test/openagents_web/live/admin_analytics_live_test.exs

58e6347eeb72 · 6 KB

defmodule OpenAgentsWeb.AdminAnalyticsLiveTest do
  @moduledoc """
  `/admin/analytics` gates like every operator surface, and its data path is
  honest about the states it can be in: unconfigured credentials, a PostHog
  that did not answer, and loaded aggregates.

  The assertions hold the same line as `/admin`: aggregate operational facts
  only. Nothing rendered here names conversation or memory content.
  """

  use OpenAgentsWeb.ConnCase, async: false
  import Phoenix.LiveViewTest

  setup do
    original = Application.get_env(:openagents, :posthog_analytics)
    Application.put_env(:openagents, :posthog_analytics, personal_api_key: nil, project_id: nil)

    on_exit(fn ->
      if original == nil,
        do: Application.delete_env(:openagents, :posthog_analytics),
        else: Application.put_env(:openagents, :posthog_analytics, original)
    end)

    :ok
  end

  describe "access" do
    test "the operator reaches the surface", %{conn: conn} do
      conn = log_in_admin_user(conn, "analytics-operator")

      {:ok, _view, html} = live(conn, ~p"/admin/analytics")

      assert html =~ "Product analytics"
    end

    test "an ordinary authenticated account is redirected and told nothing", %{conn: conn} do
      conn = log_in_github_user(conn, "analytics-ordinary")

      assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/admin/analytics")

      response = get(conn, ~p"/admin/analytics")
      assert redirected_to(response) == ~p"/"
    end

    test "an unauthenticated visitor is redirected", %{conn: conn} do
      assert {:error, {:redirect, %{to: "/"}}} = live(conn, ~p"/admin/analytics")
    end
  end

  describe "states" do
    test "without credentials the page says so instead of showing stale numbers",
         %{conn: conn} do
      Application.put_env(:openagents, :posthog_analytics,
        personal_api_key: nil,
        project_id: nil
      )

      conn = log_in_admin_user(conn, "analytics-unconfigured")

      {:ok, view, _html} = live(conn, ~p"/admin/analytics")
      render_async(view)

      assert has_element?(view, "#analytics-not-configured")
      refute has_element?(view, "#analytics-event-volume")
    end

    test "a failed pull offers retry and renders no numbers", %{conn: conn} do
      configure_posthog(fn conn ->
        Plug.Conn.send_resp(conn, 500, "boom")
      end)

      conn = log_in_admin_user(conn, "analytics-failed")

      {:ok, view, _html} = live(conn, ~p"/admin/analytics")
      render_async(view)

      assert has_element?(view, "#analytics-unavailable")
      refute has_element?(view, "#analytics-event-volume")
    end

    test "a successful pull renders bounded aggregates and refreshes", %{conn: conn} do
      Application.put_env(:openagents, :posthog_analytics,
        personal_api_key: "phx_test_key",
        project_id: 303_178,
        app_host: "https://posthog-api.internal",
        request_options: [plug: {Req.Test, __MODULE__}]
      )

      # One pull is eight questions.
      Req.Test.expect(__MODULE__, 8, fn conn -> respond_by_query(conn) end)

      conn = log_in_admin_user(conn, "analytics-loaded")

      {:ok, view, _html} = live(conn, ~p"/admin/analytics")
      render_async(view)

      assert has_element?(view, "#analytics-generated-at")
      assert has_element?(view, "#analytics-triage-health")
      assert has_element?(view, "#analytics-weekly-issue-flow")
      assert has_element?(view, "#analytics-funnel")
      assert has_element?(view, "#analytics-chat-turns")
      assert has_element?(view, "#analytics-chat-lifecycle")
      assert has_element?(view, "#analytics-chat-tokens-table")
      assert html = render(view)
      assert html =~ "anthropic/claude-sonnet-4.5"
      assert html =~ "4.2%"
      assert html =~ "12.5h"
      assert html =~ "15.0%"
      assert html =~ "$pageview"
      assert html =~ "https://openagents.com/"

      # A second full pull backs the refresh click.
      Req.Test.expect(__MODULE__, 8, fn conn -> respond_by_query(conn) end)

      assert view |> element("#analytics-refresh") |> render_click() =~ "LIVE POSTHOG"
      render_async(view)
      assert has_element?(view, "#analytics-generated-at")
    end
  end

  defp configure_posthog(handler) when is_function(handler, 1) do
    Application.put_env(:openagents, :posthog_analytics,
      personal_api_key: "phx_test_key",
      project_id: 303_178,
      app_host: "https://posthog-api.internal",
      request_options: [plug: {Req.Test, __MODULE__}]
    )

    Req.Test.expect(__MODULE__, handler)
  end

  # The client asks eight questions in a fixed order; each stub answers by
  # matching the HogQL in the request body rather than relying on call order.
  defp respond_by_query(conn) do
    {:ok, body, conn} = Plug.Conn.read_body(conn)

    cond do
      body =~ "uniq(person_id)" ->
        Req.Test.json(conn, %{
          "columns" => ["event", "count", "people"],
          "results" => [["$pageview", 118, 8], ["chat_opened", 17, 3]]
        })

      body =~ "'auth_started'" ->
        Req.Test.json(conn, %{
          "columns" => [
            "auth_started",
            "user_signed_up",
            "user_signed_in",
            "identified_signups",
            "chat_message_sent"
          ],
          "results" => [[3, 1, 2, 1, 6]]
        })

      body =~ "turn_failure_percent" ->
        Req.Test.json(conn, %{
          "columns" => [
            "messages_received",
            "messages_queued",
            "turns_failed",
            "turns_finished",
            "turn_failure_percent"
          ],
          "results" => [[24, 2, 1, 24, 4.2]]
        })

      body =~ "'chat_tokens_used'" ->
        Req.Test.json(conn, %{
          "columns" => [
            "model",
            "provider",
            "turns",
            "input_tokens",
            "output_tokens",
            "total_tokens"
          ],
          "results" => [["anthropic/claude-sonnet-4.5", "anthropic", 6, 4200, 1800, 6000]]
        })

      body =~ "'chat_turn_completed'" ->
        Req.Test.json(conn, %{
          "columns" => [
            "turns",
            "completed",
            "failed",
            "cancelled",
            "avg_duration_ms",
            "max_duration_ms"
          ],
          "results" => [[6, 6, 0, 0, 4525.0, 7414]]
        })

      body =~ "unlabeled_after_24h_percent" ->
        Req.Test.json(conn, %{
          "columns" => [
            "median_first_maintainer_response_hours",
            "eligible_issues",
            "unlabeled_issues",
            "unlabeled_after_24h_percent"
          ],
          "results" => [[12.5, 20, 3, 15.0]]
        })

      body =~ "toStartOfWeek" ->
        Req.Test.json(conn, %{
          "columns" => ["week", "created", "closed"],
          "results" => [["2026-08-10", 8, 5], ["2026-08-17", 11, 9]]
        })

      true ->
        Req.Test.json(conn, %{
          "columns" => ["url", "views"],
          "results" => [["https://openagents.com/", 20]]
        })
    end
  end
end