Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenTelemetry Support #853

Draft
wants to merge 7 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Integration test setup for OpenTelemetry support
solnic committed Jan 27, 2025
commit 9c02f49f334aa227216d2865fb8f7f81e01fa544
6 changes: 6 additions & 0 deletions test_integrations/phoenix_app/config/config.exs
Original file line number Diff line number Diff line change
@@ -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"
21 changes: 21 additions & 0 deletions test_integrations/phoenix_app/config/dev.exs
Original file line number Diff line number Diff line change
@@ -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,19 @@ 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

config :phoenix_app, Oban,
repo: PhoenixApp.Repo,
engine: Oban.Engines.Lite,
queues: [default: 10, background: 5]
20 changes: 15 additions & 5 deletions test_integrations/phoenix_app/config/test.exs
Original file line number Diff line number Diff line change
@@ -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,9 +30,13 @@ 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,
test_mode: true

config :phoenix_app, Oban,
repo: PhoenixApp.Repo,
engine: Oban.Engines.Lite,
queues: [default: 10, background: 5]
104 changes: 104 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app/accounts.ex
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app/accounts/user.ex
Original file line number Diff line number Diff line change
@@ -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
33 changes: 25 additions & 8 deletions test_integrations/phoenix_app/lib/phoenix_app/application.ex
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app/repo.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
defmodule PhoenixApp.Repo do
use Ecto.Repo,
otp_app: :phoenix_app,
adapter: Ecto.Adapters.SQLite3
end
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<div class="mx-auto max-w-2xl">
<div class="bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Schedule Test Worker</h3>

<div class="mt-5">
<.form for={@form} phx-submit="schedule" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700">Sleep Time (ms)</label>
<div class="mt-1">
<input type="number" name="test_job[sleep_time]" value="1000" min="0"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
</div>
</div>

<div>
<label class="block text-sm font-medium text-gray-700">Queue</label>
<select name="test_job[queue]" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm">
<option value="default">default</option>
<option value="background">background</option>
</select>
</div>

<div class="relative flex items-start">
<div class="flex h-6 items-center">
<input type="checkbox" name="test_job[should_fail]" value="true"
class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600" />
</div>
<div class="ml-3 text-sm leading-6">
<label class="font-medium text-gray-900">Should Fail</label>
</div>
</div>

<div>
<button type="submit" class="inline-flex justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Schedule Job
</button>
</div>
</.form>
</div>
</div>
</div>

<div class="mt-8 bg-white shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900">Auto Schedule Multiple Jobs</h3>

<div class="mt-5">
<.form for={@auto_form} phx-submit="auto_schedule" class="space-y-6">
<div>
<label class="block text-sm font-medium text-gray-700">Number of Jobs</label>
<div class="mt-1">
<input type="number"
name="auto[job_count]"
value="5"
min="1"
max="100"
class="block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" />
</div>
<p class="mt-2 text-sm text-gray-500">
Jobs will be created with random sleep times (500-5000ms), random queues, and random failure states.
</p>
</div>

<div>
<button type="submit" class="inline-flex justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">
Auto Schedule Jobs
</button>
</div>
</.form>
</div>
</div>
</div>

<div class="mt-8">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Recent Jobs</h3>

<div class="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">ID</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Queue</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">State</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Attempt</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Args</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
<%= for job <- @jobs do %>
<tr>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"><%= job.id %></td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"><%= job.queue %></td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"><%= job.state %></td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"><%= job.attempt %></td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"><%= inspect(job.args) %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule PhoenixAppWeb.UserLive.FormComponent do
use PhoenixAppWeb, :live_component

alias PhoenixApp.Accounts

@impl true
def render(assigns) do
~H"""
<div>
<.header>
<%= @title %>
<:subtitle>Use this form to manage user records in your database.</:subtitle>
</.header>
<.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</.button>
</:actions>
</.simple_form>
</div>
"""
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<.header>
Listing Users
<:actions>
<.link patch={~p"/users/new"}>
<.button>New User</.button>
</.link>
</:actions>
</.header>

<.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>
<:col :let={{_id, user}} label="Age"><%= user.age %></:col>
<:action :let={{_id, user}}>
<div class="sr-only">
<.link navigate={~p"/users/#{user}"}>Show</.link>
</div>
<.link patch={~p"/users/#{user}/edit"}>Edit</.link>
</:action>
<:action :let={{id, user}}>
<.link
phx-click={JS.push("delete", value: %{id: user.id}) |> hide("##{id}")}
data-confirm="Are you sure?"
>
Delete
</.link>
</:action>
</.table>

<.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"}
/>
</.modal>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<.header>
User <%= @user.id %>
<:subtitle>This is a user record from your database.</:subtitle>
<:actions>
<.link patch={~p"/users/#{@user}/show/edit"} phx-click={JS.push_focus()}>
<.button>Edit user</.button>
</.link>
</:actions>
</.header>

<.list>
<:item title="Name"><%= @user.name %></:item>
<:item title="Age"><%= @user.age %></:item>
</.list>

<.back navigate={~p"/users"}>Back to users</.back>

<.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}"}
/>
</.modal>
10 changes: 10 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 24 additions & 3 deletions test_integrations/phoenix_app/mix.exs
Original file line number Diff line number Diff line change
@@ -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,9 +64,19 @@ 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_phoenix, "~> 2.0"},
{:opentelemetry_semantic_conventions, "~> 1.27"},
# 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"},
{:sentry, path: "../.."},
{:hackney, "~> 1.18"},
{:oban, "~> 2.10"}
]
end

23 changes: 21 additions & 2 deletions test_integrations/phoenix_app/mix.lock

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
50 changes: 50 additions & 0 deletions test_integrations/phoenix_app/test/phoenix_app/oban_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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")
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 [span] = transaction.spans

assert span.op == "queue.process"
assert span.description == "Sentry.Integrations.Phoenix.ObanTest.TestWorker"
assert span.data["oban.job.job_id"]
assert span.data["messaging.destination"] == "default"
assert span.data["oban.job.attempt"] == 1
end
end
28 changes: 28 additions & 0 deletions test_integrations/phoenix_app/test/phoenix_app/repo_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule PhoenixApp.RepoTest do
use PhoenixApp.DataCase

alias PhoenixApp.{Repo, Accounts.User}

import Sentry.TestHelpers

setup do
put_test_config(dsn: "http://public:secret@localhost:8080/1")

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
Original file line number Diff line number Diff line change
@@ -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")

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
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule Sentry.Integrations.Phoenix.TransactionTest do
use PhoenixAppWeb.ConnCase, async: true

import Sentry.TestHelpers

setup do
put_test_config(dsn: "http://public:secret@localhost:8080/1")

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 == %{}

assert [span] = transaction.spans

assert span.op == "test_span"
assert span.trace_id == trace.trace_id
refute span.parent_span_id
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_mount, span_ecto] = mount_transaction.spans

assert span_mount.op == "PhoenixAppWeb.UserLive.Index.mount"
assert span_mount.description == "PhoenixAppWeb.UserLive.Index.mount"

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 == %{}

assert [span_handle_params] = handle_params_transaction.spans

assert span_handle_params.op == "PhoenixAppWeb.UserLive.Index.handle_params"
assert span_handle_params.description == "PhoenixAppWeb.UserLive.Index.handle_params"
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
defmodule PhoenixAppWeb.UserLiveTest do
use PhoenixAppWeb.ConnCase

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")

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&#39;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) == 2
assert [_span_1, span_2] = transaction_save.spans
assert span_2.op == "db"
assert span_2.description =~ "INSERT INTO \"users\""
assert span_2.data["db.system"] == :sqlite
assert span_2.data["db.type"] == :sql
assert span_2.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&#39;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&#39;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
26 changes: 13 additions & 13 deletions test_integrations/phoenix_app/test/support/data_case.ex
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion test_integrations/phoenix_app/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
ExUnit.start()
# Ecto.Adapters.SQL.Sandbox.mode(PhoenixApp.Repo, :manual)
Ecto.Adapters.SQL.Sandbox.mode(PhoenixApp.Repo, :manual)