diff --git a/test_integrations/phoenix_app/config/config.exs b/test_integrations/phoenix_app/config/config.exs
index a0ce0afe..68901111 100644
--- a/test_integrations/phoenix_app/config/config.exs
+++ b/test_integrations/phoenix_app/config/config.exs
@@ -8,6 +8,7 @@
import Config
config :phoenix_app,
+ ecto_repos: [PhoenixApp.Repo],
generators: [timestamp_type: :utc_datetime]
# Configures the endpoint
@@ -59,6 +60,11 @@ config :logger, :console,
config :phoenix, :json_library, if(Code.ensure_loaded?(JSON), do: JSON, else: Jason)
+config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
+
+config :opentelemetry,
+ sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]}
+
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
diff --git a/test_integrations/phoenix_app/config/dev.exs b/test_integrations/phoenix_app/config/dev.exs
index 9506d05c..8dc26871 100644
--- a/test_integrations/phoenix_app/config/dev.exs
+++ b/test_integrations/phoenix_app/config/dev.exs
@@ -1,5 +1,10 @@
import Config
+# Configure your database
+config :phoenix_app, PhoenixApp.Repo,
+ adapter: Ecto.Adapters.SQLite3,
+ database: "db/dev.sqlite3"
+
# For development, we disable any cache and enable
# debugging and code reloading.
#
@@ -73,3 +78,20 @@ config :phoenix_live_view,
# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false
+
+dsn =
+ if System.get_env("SENTRY_LOCAL"),
+ do: System.get_env("SENTRY_DSN_LOCAL"),
+ else: System.get_env("SENTRY_DSN")
+
+config :sentry,
+ dsn: dsn,
+ environment_name: :dev,
+ enable_source_code_context: true,
+ send_result: :sync,
+ traces_sample_rate: 1.0
+
+config :phoenix_app, Oban,
+ repo: PhoenixApp.Repo,
+ engine: Oban.Engines.Lite,
+ queues: [default: 10, background: 5]
diff --git a/test_integrations/phoenix_app/config/test.exs b/test_integrations/phoenix_app/config/test.exs
index f845417b..39f25e84 100644
--- a/test_integrations/phoenix_app/config/test.exs
+++ b/test_integrations/phoenix_app/config/test.exs
@@ -1,5 +1,11 @@
import Config
+# Configure your database
+config :phoenix_app, PhoenixApp.Repo,
+ adapter: Ecto.Adapters.SQLite3,
+ pool: Ecto.Adapters.SQL.Sandbox,
+ database: "db/test.sqlite3"
+
# We don't run a server during test. If one is required,
# you can enable the server option below.
config :phoenix_app, PhoenixAppWeb.Endpoint,
@@ -24,11 +30,17 @@ config :phoenix_live_view,
enable_expensive_runtime_checks: true
config :sentry,
- dsn: "http://public:secret@localhost:8080/1",
- environment_name: Mix.env(),
+ dsn: nil,
+ environment_name: :dev,
enable_source_code_context: true,
root_source_code_paths: [File.cwd!()],
test_mode: true,
- send_result: :sync
+ send_result: :sync,
+ traces_sample_rate: 1.0
config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
+
+config :phoenix_app, Oban,
+ repo: PhoenixApp.Repo,
+ engine: Oban.Engines.Lite,
+ queues: [default: 10, background: 5]
diff --git a/test_integrations/phoenix_app/lib/phoenix_app/accounts.ex b/test_integrations/phoenix_app/lib/phoenix_app/accounts.ex
new file mode 100644
index 00000000..2b626dad
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app/accounts.ex
@@ -0,0 +1,104 @@
+defmodule PhoenixApp.Accounts do
+ @moduledoc """
+ The Accounts context.
+ """
+
+ import Ecto.Query, warn: false
+ alias PhoenixApp.Repo
+
+ alias PhoenixApp.Accounts.User
+
+ @doc """
+ Returns the list of users.
+
+ ## Examples
+
+ iex> list_users()
+ [%User{}, ...]
+
+ """
+ def list_users do
+ Repo.all(User)
+ end
+
+ @doc """
+ Gets a single user.
+
+ Raises `Ecto.NoResultsError` if the User does not exist.
+
+ ## Examples
+
+ iex> get_user!(123)
+ %User{}
+
+ iex> get_user!(456)
+ ** (Ecto.NoResultsError)
+
+ """
+ def get_user!(id), do: Repo.get!(User, id)
+
+ @doc """
+ Creates a user.
+
+ ## Examples
+
+ iex> create_user(%{field: value})
+ {:ok, %User{}}
+
+ iex> create_user(%{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def create_user(attrs \\ %{}) do
+ %User{}
+ |> User.changeset(attrs)
+ |> Repo.insert()
+ end
+
+ @doc """
+ Updates a user.
+
+ ## Examples
+
+ iex> update_user(user, %{field: new_value})
+ {:ok, %User{}}
+
+ iex> update_user(user, %{field: bad_value})
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def update_user(%User{} = user, attrs) do
+ user
+ |> User.changeset(attrs)
+ |> Repo.update()
+ end
+
+ @doc """
+ Deletes a user.
+
+ ## Examples
+
+ iex> delete_user(user)
+ {:ok, %User{}}
+
+ iex> delete_user(user)
+ {:error, %Ecto.Changeset{}}
+
+ """
+ def delete_user(%User{} = user) do
+ Repo.delete(user)
+ end
+
+ @doc """
+ Returns an `%Ecto.Changeset{}` for tracking user changes.
+
+ ## Examples
+
+ iex> change_user(user)
+ %Ecto.Changeset{data: %User{}}
+
+ """
+ def change_user(%User{} = user, attrs \\ %{}) do
+ User.changeset(user, attrs)
+ end
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app/accounts/user.ex b/test_integrations/phoenix_app/lib/phoenix_app/accounts/user.ex
new file mode 100644
index 00000000..21fc3552
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app/accounts/user.ex
@@ -0,0 +1,18 @@
+defmodule PhoenixApp.Accounts.User do
+ use Ecto.Schema
+ import Ecto.Changeset
+
+ schema "users" do
+ field :name, :string
+ field :age, :integer
+
+ timestamps(type: :utc_datetime)
+ end
+
+ @doc false
+ def changeset(user, attrs) do
+ user
+ |> cast(attrs, [:name, :age])
+ |> validate_required([:name, :age])
+ end
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app/application.ex b/test_integrations/phoenix_app/lib/phoenix_app/application.ex
index b97f81ba..ff132cb1 100644
--- a/test_integrations/phoenix_app/lib/phoenix_app/application.ex
+++ b/test_integrations/phoenix_app/lib/phoenix_app/application.ex
@@ -7,14 +7,28 @@ defmodule PhoenixApp.Application do
@impl true
def start(_type, _args) do
+ :ok = Application.ensure_started(:inets)
+
+ :logger.add_handler(:my_sentry_handler, Sentry.LoggerHandler, %{
+ config: %{metadata: [:file, :line]}
+ })
+
+ OpentelemetryBandit.setup()
+ OpentelemetryPhoenix.setup(adapter: :bandit)
+ OpentelemetryOban.setup()
+ OpentelemetryEcto.setup([:phoenix_app, :repo], db_statement: :enabled)
+
children = [
PhoenixAppWeb.Telemetry,
+ PhoenixApp.Repo,
+ {Ecto.Migrator,
+ repos: Application.fetch_env!(:phoenix_app, :ecto_repos), skip: skip_migrations?()},
{DNSCluster, query: Application.get_env(:phoenix_app, :dns_cluster_query) || :ignore},
{Phoenix.PubSub, name: PhoenixApp.PubSub},
# Start the Finch HTTP client for sending emails
{Finch, name: PhoenixApp.Finch},
- # Start a worker by calling: PhoenixApp.Worker.start_link(arg)
- # {PhoenixApp.Worker, arg},
+ # Start Oban
+ {Oban, Application.fetch_env!(:phoenix_app, Oban)},
# Start to serve requests, typically the last entry
PhoenixAppWeb.Endpoint
]
@@ -25,12 +39,15 @@ defmodule PhoenixApp.Application do
Supervisor.start_link(children, opts)
end
- # TODO: Uncomment if we ever move the endpoint from test/support to the phoenix_app dir
# Tell Phoenix to update the endpoint configuration
# whenever the application is updated.
- # @impl true
- # def config_change(changed, _new, removed) do
- # PhoenixAppWeb.Endpoint.config_change(changed, removed)
- # :ok
- # end
+ @impl true
+ def config_change(changed, _new, removed) do
+ PhoenixAppWeb.Endpoint.config_change(changed, removed)
+ :ok
+ end
+
+ defp skip_migrations?() do
+ System.get_env("RELEASE_NAME") != nil
+ end
end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app/repo.ex b/test_integrations/phoenix_app/lib/phoenix_app/repo.ex
new file mode 100644
index 00000000..3976eb3b
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app/repo.ex
@@ -0,0 +1,5 @@
+defmodule PhoenixApp.Repo do
+ use Ecto.Repo,
+ otp_app: :phoenix_app,
+ adapter: Ecto.Adapters.SQLite3
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app/workers/test_worker.ex b/test_integrations/phoenix_app/lib/phoenix_app/workers/test_worker.ex
new file mode 100644
index 00000000..be57ffaf
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app/workers/test_worker.ex
@@ -0,0 +1,21 @@
+defmodule PhoenixApp.Workers.TestWorker do
+ use Oban.Worker
+
+ @impl Oban.Worker
+ def perform(%Oban.Job{args: %{"sleep_time" => sleep_time, "should_fail" => should_fail}}) do
+ # Simulate some work
+ Process.sleep(sleep_time)
+
+ if should_fail do
+ raise "Simulated failure in test worker"
+ else
+ :ok
+ end
+ end
+
+ def perform(%Oban.Job{args: %{"sleep_time" => sleep_time}}) do
+ # Simulate some work
+ Process.sleep(sleep_time)
+ :ok
+ end
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/controllers/page_controller.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/controllers/page_controller.ex
index b51d6b3c..dbc7812b 100644
--- a/test_integrations/phoenix_app/lib/phoenix_app_web/controllers/page_controller.ex
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/controllers/page_controller.ex
@@ -1,13 +1,29 @@
defmodule PhoenixAppWeb.PageController do
use PhoenixAppWeb, :controller
+ require OpenTelemetry.Tracer, as: Tracer
+
+ alias PhoenixApp.{Repo, User}
+
def home(conn, _params) do
- # The home page is often custom made,
- # so skip the default app layout.
render(conn, :home, layout: false)
end
def exception(_conn, _params) do
raise "Test exception"
end
+
+ def transaction(conn, _params) do
+ Tracer.with_span "test_span" do
+ :timer.sleep(100)
+ end
+
+ render(conn, :home, layout: false)
+ end
+
+ def users(conn, _params) do
+ Repo.all(User) |> Enum.map(& &1.name)
+
+ render(conn, :home, layout: false)
+ end
end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex
index c1817a4e..cbc6c40a 100644
--- a/test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex
@@ -35,7 +35,6 @@ defmodule PhoenixAppWeb.Endpoint do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
- plug Phoenix.Ecto.CheckRepoStatus, otp_app: :phoenix_app
end
plug Phoenix.LiveDashboard.RequestLogger,
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.ex
new file mode 100644
index 00000000..0ba8562a
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.ex
@@ -0,0 +1,94 @@
+defmodule PhoenixAppWeb.TestWorkerLive do
+ use PhoenixAppWeb, :live_view
+
+ alias PhoenixApp.Workers.TestWorker
+
+ @impl true
+ def mount(_params, _session, socket) do
+ socket =
+ assign(socket,
+ form: to_form(%{"sleep_time" => 1000, "should_fail" => false, "queue" => "default"}),
+ auto_form: to_form(%{"job_count" => 5}),
+ jobs: list_jobs()
+ )
+
+ if connected?(socket) do
+ # Poll for job updates every second
+ :timer.send_interval(1000, self(), :update_jobs)
+ end
+
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_event("schedule", %{"test_job" => params}, socket) do
+ sleep_time = String.to_integer(params["sleep_time"])
+ should_fail = params["should_fail"] == "true"
+ queue = params["queue"]
+
+ case schedule_job(sleep_time, should_fail, queue) do
+ {:ok, _job} ->
+ {:noreply,
+ socket
+ |> put_flash(:info, "Job scheduled successfully!")
+ |> assign(jobs: list_jobs())}
+
+ {:error, changeset} ->
+ {:noreply,
+ socket
+ |> put_flash(:error, "Error scheduling job: #{inspect(changeset.errors)}")}
+ end
+ end
+
+ @impl true
+ def handle_event("auto_schedule", %{"auto" => %{"job_count" => count}}, socket) do
+ job_count = String.to_integer(count)
+
+ results =
+ Enum.map(1..job_count, fn _ ->
+ sleep_time = Enum.random(500..5000)
+ should_fail = Enum.random([true, false])
+ queue = Enum.random(["default", "background"])
+
+ schedule_job(sleep_time, should_fail, queue)
+ end)
+
+ failed_count = Enum.count(results, &match?({:error, _}, &1))
+ success_count = job_count - failed_count
+
+ socket =
+ socket
+ |> put_flash(:info, "Scheduled #{success_count} jobs successfully!")
+ |> assign(jobs: list_jobs())
+
+ if failed_count > 0 do
+ socket = put_flash(socket, :error, "Failed to schedule #{failed_count} jobs")
+ {:noreply, socket}
+ else
+ {:noreply, socket}
+ end
+ end
+
+ @impl true
+ def handle_info(:update_jobs, socket) do
+ {:noreply, assign(socket, jobs: list_jobs())}
+ end
+
+ defp schedule_job(sleep_time, should_fail, queue) do
+ TestWorker.new(
+ %{"sleep_time" => sleep_time, "should_fail" => should_fail},
+ queue: queue
+ )
+ |> Oban.insert()
+ end
+
+ defp list_jobs do
+ import Ecto.Query
+
+ Oban.Job
+ |> where([j], j.worker == "PhoenixApp.Workers.TestWorker")
+ |> order_by([j], desc: j.inserted_at)
+ |> limit(10)
+ |> PhoenixApp.Repo.all()
+ end
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.html.heex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.html.heex
new file mode 100644
index 00000000..d4f75595
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/test_worker_live.html.heex
@@ -0,0 +1,103 @@
+
+
+
+
Schedule Test Worker
+
+
+ <.form for={@form} phx-submit="schedule" class="space-y-6">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Auto Schedule Multiple Jobs
+
+
+ <.form for={@auto_form} phx-submit="auto_schedule" class="space-y-6">
+
+
+
+
+
+
+ Jobs will be created with random sleep times (500-5000ms), random queues, and random failure states.
+
+
+
+
+
+
+
+
+
+
+
+
+
Recent Jobs
+
+
+
+
+
+ | ID |
+ Queue |
+ State |
+ Attempt |
+ Args |
+
+
+
+ <%= for job <- @jobs do %>
+
+ | <%= job.id %> |
+ <%= job.queue %> |
+ <%= job.state %> |
+ <%= job.attempt %> |
+ <%= inspect(job.args) %> |
+
+ <% end %>
+
+
+
+
+
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/form_component.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/form_component.ex
new file mode 100644
index 00000000..622a6b05
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/form_component.ex
@@ -0,0 +1,83 @@
+defmodule PhoenixAppWeb.UserLive.FormComponent do
+ use PhoenixAppWeb, :live_component
+
+ alias PhoenixApp.Accounts
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.header>
+ <%= @title %>
+ <:subtitle>Use this form to manage user records in your database.
+
+
+ <.simple_form
+ for={@form}
+ id="user-form"
+ phx-target={@myself}
+ phx-change="validate"
+ phx-submit="save"
+ >
+ <.input field={@form[:name]} type="text" label="Name" />
+ <.input field={@form[:age]} type="number" label="Age" />
+ <:actions>
+ <.button phx-disable-with="Saving...">Save User
+
+
+
+ """
+ end
+
+ @impl true
+ def update(%{user: user} = assigns, socket) do
+ {:ok,
+ socket
+ |> assign(assigns)
+ |> assign_new(:form, fn ->
+ to_form(Accounts.change_user(user))
+ end)}
+ end
+
+ @impl true
+ def handle_event("validate", %{"user" => user_params}, socket) do
+ changeset = Accounts.change_user(socket.assigns.user, user_params)
+ {:noreply, assign(socket, form: to_form(changeset, action: :validate))}
+ end
+
+ def handle_event("save", %{"user" => user_params}, socket) do
+ save_user(socket, socket.assigns.action, user_params)
+ end
+
+ defp save_user(socket, :edit, user_params) do
+ case Accounts.update_user(socket.assigns.user, user_params) do
+ {:ok, user} ->
+ notify_parent({:saved, user})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "User updated successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp save_user(socket, :new, user_params) do
+ case Accounts.create_user(user_params) do
+ {:ok, user} ->
+ notify_parent({:saved, user})
+
+ {:noreply,
+ socket
+ |> put_flash(:info, "User created successfully")
+ |> push_patch(to: socket.assigns.patch)}
+
+ {:error, %Ecto.Changeset{} = changeset} ->
+ {:noreply, assign(socket, form: to_form(changeset))}
+ end
+ end
+
+ defp notify_parent(msg), do: send(self(), {__MODULE__, msg})
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.ex
new file mode 100644
index 00000000..4cbf8962
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.ex
@@ -0,0 +1,47 @@
+defmodule PhoenixAppWeb.UserLive.Index do
+ use PhoenixAppWeb, :live_view
+
+ alias PhoenixApp.Accounts
+ alias PhoenixApp.Accounts.User
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, stream(socket, :users, Accounts.list_users())}
+ end
+
+ @impl true
+ def handle_params(params, _url, socket) do
+ {:noreply, apply_action(socket, socket.assigns.live_action, params)}
+ end
+
+ defp apply_action(socket, :edit, %{"id" => id}) do
+ socket
+ |> assign(:page_title, "Edit User")
+ |> assign(:user, Accounts.get_user!(id))
+ end
+
+ defp apply_action(socket, :new, _params) do
+ socket
+ |> assign(:page_title, "New User")
+ |> assign(:user, %User{})
+ end
+
+ defp apply_action(socket, :index, _params) do
+ socket
+ |> assign(:page_title, "Listing Users")
+ |> assign(:user, nil)
+ end
+
+ @impl true
+ def handle_info({PhoenixAppWeb.UserLive.FormComponent, {:saved, user}}, socket) do
+ {:noreply, stream_insert(socket, :users, user)}
+ end
+
+ @impl true
+ def handle_event("delete", %{"id" => id}, socket) do
+ user = Accounts.get_user!(id)
+ {:ok, _} = Accounts.delete_user(user)
+
+ {:noreply, stream_delete(socket, :users, user)}
+ end
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.html.heex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.html.heex
new file mode 100644
index 00000000..33a964df
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/index.html.heex
@@ -0,0 +1,42 @@
+<.header>
+ Listing Users
+ <:actions>
+ <.link patch={~p"/users/new"}>
+ <.button>New User
+
+
+
+
+<.table
+ id="users"
+ rows={@streams.users}
+ row_click={fn {_id, user} -> JS.navigate(~p"/users/#{user}") end}
+>
+ <:col :let={{_id, user}} label="Name"><%= user.name %>
+ <:col :let={{_id, user}} label="Age"><%= user.age %>
+ <:action :let={{_id, user}}>
+
+ <.link navigate={~p"/users/#{user}"}>Show
+
+ <.link patch={~p"/users/#{user}/edit"}>Edit
+
+ <:action :let={{id, user}}>
+ <.link
+ phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")}
+ data-confirm="Are you sure?"
+ >
+ Delete
+
+
+
+
+<.modal :if={@live_action in [:new, :edit]} id="user-modal" show on_cancel={JS.patch(~p"/users")}>
+ <.live_component
+ module={PhoenixAppWeb.UserLive.FormComponent}
+ id={@user.id || :new}
+ title={@page_title}
+ action={@live_action}
+ user={@user}
+ patch={~p"/users"}
+ />
+
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.ex
new file mode 100644
index 00000000..eaa24470
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.ex
@@ -0,0 +1,21 @@
+defmodule PhoenixAppWeb.UserLive.Show do
+ use PhoenixAppWeb, :live_view
+
+ alias PhoenixApp.Accounts
+
+ @impl true
+ def mount(_params, _session, socket) do
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(%{"id" => id}, _, socket) do
+ {:noreply,
+ socket
+ |> assign(:page_title, page_title(socket.assigns.live_action))
+ |> assign(:user, Accounts.get_user!(id))}
+ end
+
+ defp page_title(:show), do: "Show User"
+ defp page_title(:edit), do: "Edit User"
+end
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.html.heex b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.html.heex
new file mode 100644
index 00000000..35b90bb2
--- /dev/null
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/live/user_live/show.html.heex
@@ -0,0 +1,27 @@
+<.header>
+ User <%= @user.id %>
+ <:subtitle>This is a user record from your database.
+ <:actions>
+ <.link patch={~p"/users/#{@user}/show/edit"} phx-click={JS.push_focus()}>
+ <.button>Edit user
+
+
+
+
+<.list>
+ <:item title="Name"><%= @user.name %>
+ <:item title="Age"><%= @user.age %>
+
+
+<.back navigate={~p"/users"}>Back to users
+
+<.modal :if={@live_action == :edit} id="user-modal" show on_cancel={JS.patch(~p"/users/#{@user}")}>
+ <.live_component
+ module={PhoenixAppWeb.UserLive.FormComponent}
+ id={@user.id}
+ title={@page_title}
+ action={@live_action}
+ user={@user}
+ patch={~p"/users/#{@user}"}
+ />
+
diff --git a/test_integrations/phoenix_app/lib/phoenix_app_web/router.ex b/test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
index 409aeb27..ddf33edf 100644
--- a/test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
+++ b/test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
@@ -19,6 +19,16 @@ defmodule PhoenixAppWeb.Router do
get "/", PageController, :home
get "/exception", PageController, :exception
+ get "/transaction", PageController, :transaction
+
+ live "/test-worker", TestWorkerLive
+
+ live "/users", UserLive.Index, :index
+ live "/users/new", UserLive.Index, :new
+ live "/users/:id/edit", UserLive.Index, :edit
+
+ live "/users/:id", UserLive.Show, :show
+ live "/users/:id/show/edit", UserLive.Show, :edit
end
# Other scopes may use custom stacks.
diff --git a/test_integrations/phoenix_app/mix.exs b/test_integrations/phoenix_app/mix.exs
index ad10a3da..52e6a1b4 100644
--- a/test_integrations/phoenix_app/mix.exs
+++ b/test_integrations/phoenix_app/mix.exs
@@ -36,10 +36,21 @@ defmodule PhoenixApp.MixProject do
{:nimble_ownership, "~> 0.3.0 or ~> 1.0"},
{:postgrex, ">= 0.0.0"},
+ {:ecto, "~> 3.12"},
+ {:ecto_sql, "~> 3.12"},
+ {:ecto_sqlite3, "~> 0.16"},
{:phoenix, "~> 1.7.14"},
{:phoenix_html, "~> 4.1"},
{:phoenix_live_view, "~> 1.0"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
+ {:phoenix_ecto, "~> 4.6", optional: true},
+ {:heroicons,
+ github: "tailwindlabs/heroicons",
+ tag: "v2.1.1",
+ sparse: "optimized",
+ app: false,
+ compile: false,
+ depth: 1},
{:floki, ">= 0.30.0", only: :test},
{:phoenix_live_dashboard, "~> 0.8.3"},
{:esbuild, "~> 0.8", runtime: Mix.env() == :dev},
@@ -53,14 +64,23 @@ defmodule PhoenixApp.MixProject do
{:dns_cluster, "~> 0.1.1"},
{:bandit, "~> 1.5"},
{:bypass, "~> 2.1", only: :test},
- {:hackney, "~> 1.18", only: :test},
{:sentry, path: "../.."},
{:opentelemetry, "~> 1.5"},
{:opentelemetry_api, "~> 1.4"},
{:opentelemetry_exporter, "~> 1.0"},
- {:opentelemetry_semantic_conventions, "~> 1.27"}
+ {:opentelemetry_semantic_conventions, "~> 1.27"},
+ {:opentelemetry_bandit, "~> 0.1"},
+ {:opentelemetry_phoenix, "~> 2.0"},
+ # TODO: Update once merged
+ {:opentelemetry_oban, "~> 1.1",
+ github: "danschultzer/opentelemetry-erlang-contrib",
+ branch: "oban-v1.27-semantics",
+ sparse: "instrumentation/opentelemetry_oban"},
+ {:opentelemetry_ecto, "~> 1.2"},
+ {:hackney, "~> 1.18"},
+ {:oban, "~> 2.10"}
]
end
diff --git a/test_integrations/phoenix_app/mix.lock b/test_integrations/phoenix_app/mix.lock
index 11d417cf..982392e2 100644
--- a/test_integrations/phoenix_app/mix.lock
+++ b/test_integrations/phoenix_app/mix.lock
@@ -3,6 +3,7 @@
"bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"},
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"},
+ "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"chatterbox": {:hex, :ts_chatterbox, "0.15.1", "5cac4d15dd7ad61fc3c4415ce4826fc563d4643dee897a558ec4ea0b1c835c9c", [:rebar3], [{:hpack, "~> 0.3.0", [hex: :hpack_erl, repo: "hexpm", optional: false]}], "hexpm", "4f75b91451338bc0da5f52f3480fa6ef6e3a2aeecfc33686d6b3d0a0948f31aa"},
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
@@ -12,8 +13,13 @@
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
"decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"},
"dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
+ "ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"},
+ "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"},
+ "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.19.0", "00030bbaba150369ff3754bbc0d2c28858e8f528ae406bf6997d1772d3a03203", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "297b16750fe229f3056fe32afd3247de308094e8b0298aef0d73a8493ce97c81"},
+ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
"esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
"expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
+ "exqlite": {:hex, :exqlite, "0.31.0", "bdf87c618861147382cee29eb8bd91d8cfb0949f89238b353d24fa331527a33a", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "df352de99ba4ce1bac2ad4943d09dbe9ad59e0e7ace55917b493ae289c78fc75"},
"file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"},
"finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"},
"floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"},
@@ -21,6 +27,7 @@
"gproc": {:hex, :gproc, "0.9.1", "f1df0364423539cf0b80e8201c8b1839e229e5f9b3ccb944c5834626998f5b8c", [:rebar3], [], "hexpm", "905088e32e72127ed9466f0bac0d8e65704ca5e73ee5a62cb073c3117916d507"},
"grpcbox": {:hex, :grpcbox, "0.17.1", "6e040ab3ef16fe699ffb513b0ef8e2e896da7b18931a1ef817143037c454bcce", [:rebar3], [{:acceptor_pool, "~> 1.0.0", [hex: :acceptor_pool, repo: "hexpm", optional: false]}, {:chatterbox, "~> 0.15.1", [hex: :ts_chatterbox, repo: "hexpm", optional: false]}, {:ctx, "~> 0.6.0", [hex: :ctx, repo: "hexpm", optional: false]}, {:gproc, "~> 0.9.1", [hex: :gproc, repo: "hexpm", optional: false]}], "hexpm", "4a3b5d7111daabc569dc9cbd9b202a3237d81c80bf97212fbc676832cb0ceb17"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
+ "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
"hpack": {:hex, :hpack_erl, "0.3.0", "2461899cc4ab6a0ef8e970c1661c5fc6a52d3c25580bc6dd204f84ce94669926", [:rebar3], [], "hexpm", "d6137d7079169d8c485c6962dfe261af5b9ef60fbc557344511c1e65e3d95fb0"},
"hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@@ -32,12 +39,21 @@
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_ownership": {:hex, :nimble_ownership, "1.0.0", "3f87744d42c21b2042a0aa1d48c83c77e6dd9dd357e425a038dd4b49ba8b79a1", [:mix], [], "hexpm", "7c16cc74f4e952464220a73055b557a273e8b1b7ace8489ec9d86e9ad56cb2cc"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
+ "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"},
"opentelemetry": {:hex, :opentelemetry, "1.5.0", "7dda6551edfc3050ea4b0b40c0d2570423d6372b97e9c60793263ef62c53c3c2", [:rebar3], [{:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "cdf4f51d17b592fc592b9a75f86a6f808c23044ba7cf7b9534debbcc5c23b0ee"},
"opentelemetry_api": {:hex, :opentelemetry_api, "1.4.0", "63ca1742f92f00059298f478048dfb826f4b20d49534493d6919a0db39b6db04", [:mix, :rebar3], [], "hexpm", "3dfbbfaa2c2ed3121c5c483162836c4f9027def469c41578af5ef32589fcfc58"},
+ "opentelemetry_bandit": {:hex, :opentelemetry_bandit, "0.2.0", "60ee4789994d4532ec1b4c05cb8fad333c60ba2c248eb908918369fde045bbda", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:plug, ">= 1.15.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57e31355a860250c9203ae34f0bf0290a14b72ab02b154535e1b2512a0767bca"},
+ "opentelemetry_ecto": {:hex, :opentelemetry_ecto, "1.2.0", "2382cb47ddc231f953d3b8263ed029d87fbf217915a1da82f49159d122b64865", [:mix], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "70dfa2e79932e86f209df00e36c980b17a32f82d175f0068bf7ef9a96cf080cf"},
"opentelemetry_exporter": {:hex, :opentelemetry_exporter, "1.8.0", "5d546123230771ef4174e37bedfd77e3374913304cd6ea3ca82a2add49cd5d56", [:rebar3], [{:grpcbox, ">= 0.0.0", [hex: :grpcbox, repo: "hexpm", optional: false]}, {:opentelemetry, "~> 1.5.0", [hex: :opentelemetry, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:tls_certificate_check, "~> 1.18", [hex: :tls_certificate_check, repo: "hexpm", optional: false]}], "hexpm", "a1f9f271f8d3b02b81462a6bfef7075fd8457fdb06adff5d2537df5e2264d9af"},
+ "opentelemetry_oban": {:git, "https://github.com/danschultzer/opentelemetry-erlang-contrib.git", "fda7ab9acde6d845393f8bb4a9876ebb98aedd75", [branch: "oban-v1.27-semantics", sparse: "instrumentation/opentelemetry_oban"]},
+ "opentelemetry_phoenix": {:hex, :opentelemetry_phoenix, "2.0.1", "c664cdef205738cffcd409b33599439a4ffb2035ef6e21a77927ac1da90463cb", [:mix], [{:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:opentelemetry_api, "~> 1.4", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: false]}, {:opentelemetry_semantic_conventions, "~> 1.27", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: false]}, {:opentelemetry_telemetry, "~> 1.1", [hex: :opentelemetry_telemetry, repo: "hexpm", optional: false]}, {:otel_http, "~> 0.2", [hex: :otel_http, repo: "hexpm", optional: false]}, {:plug, ">= 1.11.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a24fdccdfa6b890c8892c6366beab4a15a27ec0c692b0f77ec2a862e7b235f6e"},
+ "opentelemetry_process_propagator": {:hex, :opentelemetry_process_propagator, "0.3.0", "ef5b2059403a1e2b2d2c65914e6962e56371570b8c3ab5323d7a8d3444fb7f84", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.0", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}], "hexpm", "7243cb6de1523c473cba5b1aefa3f85e1ff8cc75d08f367104c1e11919c8c029"},
"opentelemetry_semantic_conventions": {:hex, :opentelemetry_semantic_conventions, "1.27.0", "acd0194a94a1e57d63da982ee9f4a9f88834ae0b31b0bd850815fe9be4bbb45f", [:mix, :rebar3], [], "hexpm", "9681ccaa24fd3d810b4461581717661fd85ff7019b082c2dff89c7d5b1fc2864"},
+ "opentelemetry_telemetry": {:hex, :opentelemetry_telemetry, "1.1.2", "410ab4d76b0921f42dbccbe5a7c831b8125282850be649ee1f70050d3961118a", [:mix, :rebar3], [{:opentelemetry_api, "~> 1.3", [hex: :opentelemetry_api, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "641ab469deb181957ac6d59bce6e1321d5fe2a56df444fc9c19afcad623ab253"},
+ "otel_http": {:hex, :otel_http, "0.2.0", "b17385986c7f1b862f5d577f72614ecaa29de40392b7618869999326b9a61d8a", [:rebar3], [], "hexpm", "f2beadf922c8cfeb0965488dd736c95cc6ea8b9efce89466b3904d317d7cc717"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.17", "2fcdceecc6fb90bec26fab008f96abbd0fd93bc9956ec7985e5892cf545152ca", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "50e8ad537f3f7b0efb1509b2f75b5c918f697be6a45d48e49a30d3b7c0e464c9"},
+ "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"},
"phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.4", "4508e481f791ce62ec6a096e13b061387158cbeefacca68c6c1928e1305e23ed", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "2984aae96994fbc5c61795a73b8fb58153b41ff934019cfb522343d2d3817d59"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"},
diff --git a/test_integrations/phoenix_app/priv/repo/migrations/20240926155911_create_users.exs b/test_integrations/phoenix_app/priv/repo/migrations/20240926155911_create_users.exs
new file mode 100644
index 00000000..21f4a335
--- /dev/null
+++ b/test_integrations/phoenix_app/priv/repo/migrations/20240926155911_create_users.exs
@@ -0,0 +1,12 @@
+defmodule PhoenixApp.Repo.Migrations.CreateUsers do
+ use Ecto.Migration
+
+ def change do
+ create table(:users) do
+ add :name, :string
+ add :age, :integer
+
+ timestamps(type: :utc_datetime)
+ end
+ end
+end
diff --git a/test_integrations/phoenix_app/priv/repo/migrations/20241213222834_add_oban.exs b/test_integrations/phoenix_app/priv/repo/migrations/20241213222834_add_oban.exs
new file mode 100644
index 00000000..f7aa7789
--- /dev/null
+++ b/test_integrations/phoenix_app/priv/repo/migrations/20241213222834_add_oban.exs
@@ -0,0 +1,11 @@
+defmodule PhoenixApp.Repo.Migrations.AddOban do
+ use Ecto.Migration
+
+ def up do
+ Oban.Migration.up()
+ end
+
+ def down do
+ Oban.Migration.down()
+ end
+end
diff --git a/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs b/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs
new file mode 100644
index 00000000..0377ed29
--- /dev/null
+++ b/test_integrations/phoenix_app/test/phoenix_app/oban_test.exs
@@ -0,0 +1,45 @@
+defmodule Sentry.Integrations.Phoenix.ObanTest do
+ use PhoenixAppWeb.ConnCase, async: false
+ use Oban.Testing, repo: PhoenixApp.Repo
+
+ import Sentry.TestHelpers
+
+ setup do
+ put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
+
+ Sentry.Test.start_collecting_sentry_reports()
+
+ :ok
+ end
+
+ defmodule TestWorker do
+ use Oban.Worker
+
+ @impl Oban.Worker
+ def perform(_args) do
+ :timer.sleep(100)
+ end
+ end
+
+ test "captures Oban worker execution as transaction" do
+ :ok = perform_job(TestWorker, %{test: "args"})
+
+ transactions = Sentry.Test.pop_sentry_transactions()
+ assert length(transactions) == 1
+
+ [transaction] = transactions
+
+ assert transaction.transaction == "Sentry.Integrations.Phoenix.ObanTest.TestWorker"
+ assert transaction.transaction_info == %{source: :custom}
+
+ trace = transaction.contexts.trace
+ assert trace.origin == "opentelemetry_oban"
+ assert trace.op == "queue.process"
+ assert trace.description == "Sentry.Integrations.Phoenix.ObanTest.TestWorker"
+ assert trace.data["oban.job.job_id"]
+ assert trace.data["messaging.destination"] == "default"
+ assert trace.data["oban.job.attempt"] == 1
+
+ assert [] = transaction.spans
+ end
+end
diff --git a/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs b/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs
new file mode 100644
index 00000000..095fbbe0
--- /dev/null
+++ b/test_integrations/phoenix_app/test/phoenix_app/repo_test.exs
@@ -0,0 +1,28 @@
+defmodule PhoenixApp.RepoTest do
+ use PhoenixApp.DataCase, async: false
+
+ alias PhoenixApp.{Repo, Accounts.User}
+
+ import Sentry.TestHelpers
+
+ setup do
+ put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
+
+ Sentry.Test.start_collecting_sentry_reports()
+ end
+
+ test "instrumented top-level ecto transaction span" do
+ Repo.all(User) |> Enum.map(& &1.id)
+
+ transactions = Sentry.Test.pop_sentry_transactions()
+
+ assert length(transactions) == 1
+
+ assert [transaction] = transactions
+
+ assert transaction.transaction_info == %{source: :custom}
+ assert transaction.contexts.trace.op == "db"
+ assert String.starts_with?(transaction.contexts.trace.description, "SELECT")
+ assert transaction.contexts.trace.data["db.system"] == :sqlite
+ end
+end
diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs
index b1e81b86..7f597c73 100644
--- a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs
+++ b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/exception_test.exs
@@ -4,21 +4,12 @@ defmodule Sentry.Integrations.Phoenix.ExceptionTest do
import Sentry.TestHelpers
setup do
- bypass = Bypass.open()
- put_test_config(dsn: "http://public:secret@localhost:#{bypass.port}/1")
- %{bypass: bypass}
- end
+ put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
- test "GET /exception sends exception to Sentry", %{conn: conn, bypass: bypass} do
- Bypass.expect(bypass, fn conn ->
- {:ok, body, conn} = Plug.Conn.read_body(conn)
- assert body =~ "RuntimeError"
- assert body =~ "Test exception"
- assert conn.request_path == "/api/1/envelope/"
- assert conn.method == "POST"
- Plug.Conn.resp(conn, 200, ~s<{"id": "340"}>)
- end)
+ Sentry.Test.start_collecting_sentry_reports()
+ end
+ test "GET /exception sends exception to Sentry", %{conn: conn} do
assert_raise RuntimeError, "Test exception", fn ->
get(conn, ~p"/exception")
end
diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs
new file mode 100644
index 00000000..2a821d4e
--- /dev/null
+++ b/test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs
@@ -0,0 +1,64 @@
+defmodule Sentry.Integrations.Phoenix.TransactionTest do
+ use PhoenixAppWeb.ConnCase, async: false
+
+ import Sentry.TestHelpers
+
+ setup do
+ put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
+
+ Sentry.Test.start_collecting_sentry_reports()
+ end
+
+ test "GET /transaction", %{conn: conn} do
+ # TODO: Wrap this in a transaction that the web server usually
+ # would wrap it in.
+ get(conn, ~p"/transaction")
+
+ transactions = Sentry.Test.pop_sentry_transactions()
+
+ assert length(transactions) == 1
+
+ assert [transaction] = transactions
+
+ assert transaction.transaction == "test_span"
+ assert transaction.transaction_info == %{source: :custom}
+
+ trace = transaction.contexts.trace
+ assert trace.origin == "phoenix_app"
+ assert trace.op == "test_span"
+ assert trace.data == %{}
+ end
+
+ test "GET /users", %{conn: conn} do
+ get(conn, ~p"/users")
+
+ transactions = Sentry.Test.pop_sentry_transactions()
+
+ assert length(transactions) == 2
+
+ assert [mount_transaction, handle_params_transaction] = transactions
+
+ assert mount_transaction.transaction == "PhoenixAppWeb.UserLive.Index.mount"
+ assert mount_transaction.transaction_info == %{source: :custom}
+
+ trace = mount_transaction.contexts.trace
+ assert trace.origin == "opentelemetry_phoenix"
+ assert trace.op == "PhoenixAppWeb.UserLive.Index.mount"
+ assert trace.data == %{}
+
+ assert [span_ecto] = mount_transaction.spans
+
+ assert span_ecto.op == "db"
+ assert span_ecto.description == "SELECT u0.\"id\", u0.\"name\", u0.\"age\", u0.\"inserted_at\", u0.\"updated_at\" FROM \"users\" AS u0"
+
+ assert handle_params_transaction.transaction ==
+ "PhoenixAppWeb.UserLive.Index.handle_params"
+
+ assert handle_params_transaction.transaction_info == %{source: :custom}
+
+ trace = handle_params_transaction.contexts.trace
+ assert trace.origin == "opentelemetry_phoenix"
+ assert trace.op == "PhoenixAppWeb.UserLive.Index.handle_params"
+ assert trace.data == %{}
+ end
+end
diff --git a/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs b/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs
new file mode 100644
index 00000000..46a45142
--- /dev/null
+++ b/test_integrations/phoenix_app/test/phoenix_app_web/live/user_live_test.exs
@@ -0,0 +1,140 @@
+defmodule PhoenixAppWeb.UserLiveTest do
+ use PhoenixAppWeb.ConnCase, async: false
+
+ import Sentry.TestHelpers
+ import Phoenix.LiveViewTest
+ import PhoenixApp.AccountsFixtures
+
+ @create_attrs %{name: "some name", age: 42}
+ @update_attrs %{name: "some updated name", age: 43}
+ @invalid_attrs %{name: nil, age: nil}
+
+ setup do
+ put_test_config(dsn: "http://public:secret@localhost:8080/1", traces_sample_rate: 1.0)
+
+ Sentry.Test.start_collecting_sentry_reports()
+ end
+
+ defp create_user(_) do
+ user = user_fixture()
+ %{user: user}
+ end
+
+ describe "Index" do
+ setup [:create_user]
+
+ test "lists all users", %{conn: conn, user: user} do
+ {:ok, _index_live, html} = live(conn, ~p"/users")
+
+ assert html =~ "Listing Users"
+ assert html =~ user.name
+ end
+
+ test "saves new user", %{conn: conn} do
+ {:ok, index_live, _html} = live(conn, ~p"/users")
+
+ assert index_live |> element("a", "New User") |> render_click() =~
+ "New User"
+
+ assert_patch(index_live, ~p"/users/new")
+
+ assert index_live
+ |> form("#user-form", user: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#user-form", user: @create_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/users")
+
+ html = render(index_live)
+ assert html =~ "User created successfully"
+ assert html =~ "some name"
+
+ transactions = Sentry.Test.pop_sentry_transactions()
+
+ transaction_save =
+ Enum.find(transactions, fn transaction ->
+ transaction.transaction == "PhoenixAppWeb.UserLive.Index.handle_event#save"
+ end)
+
+ assert transaction_save.transaction == "PhoenixAppWeb.UserLive.Index.handle_event#save"
+ assert transaction_save.transaction_info.source == :custom
+ assert transaction_save.contexts.trace.op == "PhoenixAppWeb.UserLive.Index.handle_event#save"
+ assert transaction_save.contexts.trace.origin == "opentelemetry_phoenix"
+
+ assert length(transaction_save.spans) == 1
+ assert [span] = transaction_save.spans
+ assert span.op == "db"
+ assert span.description =~ "INSERT INTO \"users\""
+ assert span.data["db.system"] == :sqlite
+ assert span.data["db.type"] == :sql
+ assert span.origin == "opentelemetry_ecto"
+ end
+
+ test "updates user in listing", %{conn: conn, user: user} do
+ {:ok, index_live, _html} = live(conn, ~p"/users")
+
+ assert index_live |> element("#users-#{user.id} a", "Edit") |> render_click() =~
+ "Edit User"
+
+ assert_patch(index_live, ~p"/users/#{user}/edit")
+
+ assert index_live
+ |> form("#user-form", user: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert index_live
+ |> form("#user-form", user: @update_attrs)
+ |> render_submit()
+
+ assert_patch(index_live, ~p"/users")
+
+ html = render(index_live)
+ assert html =~ "User updated successfully"
+ assert html =~ "some updated name"
+ end
+
+ test "deletes user in listing", %{conn: conn, user: user} do
+ {:ok, index_live, _html} = live(conn, ~p"/users")
+
+ assert index_live |> element("#users-#{user.id} a", "Delete") |> render_click()
+ refute has_element?(index_live, "#users-#{user.id}")
+ end
+ end
+
+ describe "Show" do
+ setup [:create_user]
+
+ test "displays user", %{conn: conn, user: user} do
+ {:ok, _show_live, html} = live(conn, ~p"/users/#{user}")
+
+ assert html =~ "Show User"
+ assert html =~ user.name
+ end
+
+ test "updates user within modal", %{conn: conn, user: user} do
+ {:ok, show_live, _html} = live(conn, ~p"/users/#{user}")
+
+ assert show_live |> element("a", "Edit") |> render_click() =~
+ "Edit User"
+
+ assert_patch(show_live, ~p"/users/#{user}/show/edit")
+
+ assert show_live
+ |> form("#user-form", user: @invalid_attrs)
+ |> render_change() =~ "can't be blank"
+
+ assert show_live
+ |> form("#user-form", user: @update_attrs)
+ |> render_submit()
+
+ assert_patch(show_live, ~p"/users/#{user}")
+
+ html = render(show_live)
+ assert html =~ "User updated successfully"
+ assert html =~ "some updated name"
+ end
+ end
+end
diff --git a/test_integrations/phoenix_app/test/support/data_case.ex b/test_integrations/phoenix_app/test/support/data_case.ex
index 648de1de..d58f0fe0 100644
--- a/test_integrations/phoenix_app/test/support/data_case.ex
+++ b/test_integrations/phoenix_app/test/support/data_case.ex
@@ -20,9 +20,9 @@ defmodule PhoenixApp.DataCase do
quote do
alias PhoenixApp.Repo
- # import Ecto
- # import Ecto.Changeset
- # import Ecto.Query
+ import Ecto
+ import Ecto.Changeset
+ import Ecto.Query
import PhoenixApp.DataCase
end
end
@@ -35,9 +35,9 @@ defmodule PhoenixApp.DataCase do
@doc """
Sets up the sandbox based on the test tags.
"""
- def setup_sandbox(_tags) do
- # pid = Ecto.Adapters.SQL.Sandbox.start_owner!(PhoenixApp.Repo, shared: not tags[:async])
- # on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
+ def setup_sandbox(tags) do
+ pid = Ecto.Adapters.SQL.Sandbox.start_owner!(PhoenixApp.Repo, shared: not tags[:async])
+ on_exit(fn -> Ecto.Adapters.SQL.Sandbox.stop_owner(pid) end)
end
@doc """
@@ -48,11 +48,11 @@ defmodule PhoenixApp.DataCase do
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
- # def errors_on(changeset) do
- # Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
- # Regex.replace(~r"%{(\w+)}", message, fn _, key ->
- # opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
- # end)
- # end)
- # end
+ def errors_on(changeset) do
+ Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
+ Regex.replace(~r"%{(\w+)}", message, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ end
end
diff --git a/test_integrations/phoenix_app/test/support/fixtures/accounts_fixtures.ex b/test_integrations/phoenix_app/test/support/fixtures/accounts_fixtures.ex
new file mode 100644
index 00000000..eb0799e2
--- /dev/null
+++ b/test_integrations/phoenix_app/test/support/fixtures/accounts_fixtures.ex
@@ -0,0 +1,21 @@
+defmodule PhoenixApp.AccountsFixtures do
+ @moduledoc """
+ This module defines test helpers for creating
+ entities via the `PhoenixApp.Accounts` context.
+ """
+
+ @doc """
+ Generate a user.
+ """
+ def user_fixture(attrs \\ %{}) do
+ {:ok, user} =
+ attrs
+ |> Enum.into(%{
+ age: 42,
+ name: "some name"
+ })
+ |> PhoenixApp.Accounts.create_user()
+
+ user
+ end
+end
diff --git a/test_integrations/phoenix_app/test/test_helper.exs b/test_integrations/phoenix_app/test/test_helper.exs
index 97b7531c..8b917f93 100644
--- a/test_integrations/phoenix_app/test/test_helper.exs
+++ b/test_integrations/phoenix_app/test/test_helper.exs
@@ -1,2 +1,2 @@
ExUnit.start()
-# Ecto.Adapters.SQL.Sandbox.mode(PhoenixApp.Repo, :manual)
+Ecto.Adapters.SQL.Sandbox.mode(PhoenixApp.Repo, :manual)