Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
139 changes: 139 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: E2E Tracing Tests

on:
push:
branches:
- master
- release/**
pull_request:

jobs:
tracing-e2e:
name: Tracing E2E Tests
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
with:
elixir-version: "1.18"
otp-version: "27.2"

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Cache Elixir dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
test_integrations/phoenix_app/deps
test_integrations/phoenix_app/_build
key: ${{ runner.os }}-elixir-1.18-otp-27.2-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: |
${{ runner.os }}-elixir-1.18-otp-27.2-mix-

- name: Cache Node.js dependencies
uses: actions/cache@v4
with:
path: |
test_integrations/tracing/node_modules
test_integrations/tracing/svelte_mini/node_modules
key: ${{ runner.os }}-node-20-${{ hashFiles('test_integrations/tracing/package-lock.json', 'test_integrations/tracing/svelte_mini/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-20-

- name: Install main project dependencies
run: mix deps.get

- name: Install Phoenix app dependencies
working-directory: test_integrations/phoenix_app
run: mix deps.get

- name: Compile Phoenix app
working-directory: test_integrations/phoenix_app
run: mix compile

- name: Install tracing test npm dependencies
working-directory: test_integrations/tracing
run: npm install

- name: Install Svelte app dependencies
working-directory: test_integrations/tracing/svelte_mini
run: npm install

- name: Install Playwright browsers
working-directory: test_integrations/tracing
run: npx playwright install --with-deps --only-shell chromium

- name: Start Phoenix server
working-directory: test_integrations/phoenix_app
run: |
rm -f tmp/sentry_debug_events.log
SENTRY_E2E_TEST_MODE=true mix phx.server &
echo $! > /tmp/phoenix.pid
echo "Phoenix server started with PID $(cat /tmp/phoenix.pid)"

- name: Start Svelte server
working-directory: test_integrations/tracing/svelte_mini
run: |
SENTRY_E2E_SVELTE_APP_PORT=4001 npm run dev &
echo $! > /tmp/svelte.pid
echo "Svelte server started with PID $(cat /tmp/svelte.pid)"

- name: Wait for Phoenix server
run: |
echo "Waiting for Phoenix server at http://localhost:4000/health..."
timeout 60 bash -c 'until curl -s http://localhost:4000/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
echo "Phoenix server is ready!"
curl -s http://localhost:4000/health

- name: Wait for Svelte server
run: |
echo "Waiting for Svelte server at http://localhost:4001/health..."
timeout 60 bash -c 'until curl -s http://localhost:4001/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
echo "Svelte server is ready!"
curl -s http://localhost:4001/health

- name: Run Playwright tests
working-directory: test_integrations/tracing
env:
SENTRY_E2E_PHOENIX_APP_URL: http://localhost:4000
SENTRY_E2E_SVELTE_APP_URL: http://localhost:4001
SENTRY_E2E_SERVERS_RUNNING: "true"
run: npx playwright test --reporter=list

- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: test_integrations/tracing/playwright-report/
retention-days: 7

- name: Upload test screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: test-screenshots
path: test_integrations/tracing/test-results/
retention-days: 7

- name: Show Phoenix server logs
if: failure()
run: |
echo "=== Sentry debug events log ==="
cat test_integrations/phoenix_app/tmp/sentry_debug_events.log 2>/dev/null || echo "No events logged"

- name: Cleanup servers
if: always()
run: |
[ -f /tmp/phoenix.pid ] && kill $(cat /tmp/phoenix.pid) 2>/dev/null || true
[ -f /tmp/svelte.pid ] && kill $(cat /tmp/svelte.pid) 2>/dev/null || true
4 changes: 3 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ jobs:
uses: actions/checkout@v4

- name: Setup Elixir and Erlang
uses: erlef/setup-beam@v1
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
with:
elixir-version: ${{ matrix.elixir }}
otp-version: ${{ matrix.otp }}
Expand Down Expand Up @@ -117,6 +117,8 @@ jobs:
run: mix test

- name: Run integration tests
env:
SKIP_TRACING_E2E: "true"
run: mix test.integrations

- name: Cache Dialyzer PLT
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ test_integrations/phoenix_app/db

test_integrations/*/_build
test_integrations/*/deps
test_integrations/*/test-results/
8 changes: 8 additions & 0 deletions test_integrations/phoenix_app/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
config :opentelemetry,
sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]}

# Configure OpenTelemetry to use Sentry propagator for distributed tracing
config :opentelemetry,
text_map_propagators: [
:trace_context,
:baggage,
Sentry.OpenTelemetry.Propagator
]

# 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"
15 changes: 15 additions & 0 deletions test_integrations/phoenix_app/config/runtime.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ if System.get_env("PHX_SERVER") do
config :phoenix_app, PhoenixAppWeb.Endpoint, server: true
end

# For e2e tracing tests, use the TestClient to log events to a file
# This must be in runtime.exs because the env var is set at runtime, not compile time
if System.get_env("SENTRY_E2E_TEST_MODE") == "true" do
config :sentry,
dsn: "https://public@sentry.example.com/1",
client: PhoenixApp.TestClient
else
# Allow runtime configuration of Sentry DSN and environment
if dsn = System.get_env("SENTRY_DSN") do
config :sentry,
dsn: dsn,
environment_name: System.get_env("SENTRY_ENVIRONMENT") || config_env()
end
end

if config_env() == :prod do
# database_url =
# System.get_env("DATABASE_URL") ||
Expand Down
86 changes: 86 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app/test_client.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
defmodule PhoenixApp.TestClient do
@moduledoc """
A test Sentry client that logs envelopes to a file for e2e test validation.

This client mimics the behavior of Sentry::DebugTransport in sentry-ruby,
logging all envelopes to a file that can be read by Playwright tests.
"""

require Logger

@behaviour Sentry.HTTPClient

@impl true
def post(_url, _headers, body) do
log_envelope(body)

# Return success response
{:ok, 200, [], ~s({"id":"test-event-id"})}
end

defp log_envelope(body) when is_binary(body) do
log_file = Path.join([File.cwd!(), "tmp", "sentry_debug_events.log"])

# Ensure the tmp directory exists
log_dir = Path.dirname(log_file)
File.mkdir_p!(log_dir)

# Parse the envelope binary to extract events and headers
case parse_envelope(body) do
{:ok, envelope_data} ->
# Write the envelope data as JSON
json = Jason.encode!(envelope_data)
File.write!(log_file, json <> "\n", [:append])

{:error, reason} ->
Logger.warning("Failed to parse envelope for logging: #{inspect(reason)}")
end
rescue
error ->
Logger.warning("Failed to log envelope: #{inspect(error)}")
end

defp parse_envelope(body) when is_binary(body) do
# Envelope format: header\nitem_header\nitem_payload[\nitem_header\nitem_payload...]
# See: https://develop.sentry.dev/sdk/envelopes/

lines = String.split(body, "\n")

with {:ok, header_line, rest} <- get_first_line(lines),
{:ok, envelope_headers} <- Jason.decode(header_line),
{:ok, items} <- parse_items(rest) do

envelope = %{
headers: envelope_headers,
items: items
}

{:ok, envelope}
else
error -> {:error, error}
end
end

defp get_first_line([first | rest]), do: {:ok, first, rest}
defp get_first_line([]), do: {:error, :empty_envelope}

defp parse_items(lines), do: parse_items(lines, [])

defp parse_items([], acc), do: {:ok, Enum.reverse(acc)}

defp parse_items([item_header_line, payload_line | rest], acc) do
with {:ok, _item_header} <- Jason.decode(item_header_line),
{:ok, payload} <- Jason.decode(payload_line) do
parse_items(rest, [payload | acc])
else
_error ->
# Skip malformed items
parse_items(rest, acc)
end
end

defp parse_items([_single_line], acc) do
# Handle trailing empty line
{:ok, Enum.reverse(acc)}
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,35 @@ defmodule PhoenixAppWeb.PageController do

render(conn, :home, layout: false)
end

# E2E tracing test endpoints

def api_error(_conn, _params) do
raise ArithmeticError, "bad argument in arithmetic expression"
end

def health(conn, _params) do
json(conn, %{status: "ok"})
end

def api_data(conn, _params) do
Tracer.with_span "fetch_data" do
users = Repo.all(User)

Tracer.with_span "process_data" do
user_count = length(users)

first_user = Repo.get(User, 1)

json(conn, %{
message: "Data fetched successfully",
data: %{
user_count: user_count,
first_user: if(first_user, do: first_user.name, else: nil),
timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
}
})
end
end
end
end
20 changes: 20 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ defmodule PhoenixAppWeb.Endpoint do
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

plug :cors

plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
Expand All @@ -53,4 +55,22 @@ defmodule PhoenixAppWeb.Endpoint do
plug Plug.Head
plug Plug.Session, @session_options
plug PhoenixAppWeb.Router

# CORS plug for e2e tests - allows the Svelte frontend to make
# cross-origin requests to the Phoenix backend
defp cors(conn, _opts) do
conn
|> put_resp_header("access-control-allow-origin", "*")
|> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
|> put_resp_header("access-control-allow-headers", "content-type, sentry-trace, baggage")
|> handle_preflight()
end

defp handle_preflight(%{method: "OPTIONS"} = conn) do
conn
|> send_resp(200, "")
|> halt()
end

defp handle_preflight(conn), do: conn
end
17 changes: 17 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ defmodule PhoenixAppWeb.Router do

pipeline :api do
plug :accepts, ["json"]
plug :put_cors_headers
end

defp put_cors_headers(conn, _opts) do
conn
|> put_resp_header("access-control-allow-origin", "*")
|> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
|> put_resp_header("access-control-allow-headers", "content-type, authorization, sentry-trace, baggage")
end

scope "/", PhoenixAppWeb do
Expand All @@ -32,6 +40,15 @@ defmodule PhoenixAppWeb.Router do
live "/users/:id/show/edit", UserLive.Show, :edit
end

# For e2e DT tests with a front-end app
scope "/", PhoenixAppWeb do
pipe_through :api

get "/error", PageController, :api_error
get "/health", PageController, :health
get "/api/data", PageController, :api_data
end

# Other scopes may use custom stacks.
# scope "/api", PhoenixAppWeb do
# pipe_through :api
Expand Down
Loading