Skip to content

Commit bc06ffc

Browse files
authored
feat: distributed tracing - e2e tests (#964)
* chore(tests): add svelte app for e2e testing * chore(tests): add playwright for e2e testing * chore(tests): add `TestClient` for e2e tests * chore(tests): add e2e tests for tracing * chore(tests): add e2e github workflow
1 parent ad782bf commit bc06ffc

File tree

21 files changed

+2478
-1
lines changed

21 files changed

+2478
-1
lines changed

.github/workflows/e2e.yml

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
name: E2E Tracing Tests
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- release/**
8+
pull_request:
9+
10+
jobs:
11+
tracing-e2e:
12+
name: Tracing E2E Tests
13+
runs-on: ubuntu-latest
14+
timeout-minutes: 15
15+
16+
steps:
17+
- name: Check out repository
18+
uses: actions/checkout@v4
19+
20+
- name: Setup Elixir and Erlang
21+
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
22+
with:
23+
elixir-version: "1.18"
24+
otp-version: "27.2"
25+
26+
- name: Setup Node.js
27+
uses: actions/setup-node@v4
28+
with:
29+
node-version: "20"
30+
31+
- name: Cache Elixir dependencies
32+
uses: actions/cache@v4
33+
with:
34+
path: |
35+
deps
36+
_build
37+
test_integrations/phoenix_app/deps
38+
test_integrations/phoenix_app/_build
39+
key: ${{ runner.os }}-elixir-1.18-otp-27.2-mix-${{ hashFiles('**/mix.lock') }}
40+
restore-keys: |
41+
${{ runner.os }}-elixir-1.18-otp-27.2-mix-
42+
43+
- name: Cache Node.js dependencies
44+
uses: actions/cache@v4
45+
with:
46+
path: |
47+
test_integrations/tracing/node_modules
48+
test_integrations/tracing/svelte_mini/node_modules
49+
key: ${{ runner.os }}-node-20-${{ hashFiles('test_integrations/tracing/package-lock.json', 'test_integrations/tracing/svelte_mini/package-lock.json') }}
50+
restore-keys: |
51+
${{ runner.os }}-node-20-
52+
53+
- name: Install main project dependencies
54+
run: mix deps.get
55+
56+
- name: Install Phoenix app dependencies
57+
working-directory: test_integrations/phoenix_app
58+
run: mix deps.get
59+
60+
- name: Compile Phoenix app
61+
working-directory: test_integrations/phoenix_app
62+
run: mix compile
63+
64+
- name: Install tracing test npm dependencies
65+
working-directory: test_integrations/tracing
66+
run: npm install
67+
68+
- name: Install Svelte app dependencies
69+
working-directory: test_integrations/tracing/svelte_mini
70+
run: npm install
71+
72+
- name: Install Playwright browsers
73+
working-directory: test_integrations/tracing
74+
run: npx playwright install --with-deps --only-shell chromium
75+
76+
- name: Start Phoenix server
77+
working-directory: test_integrations/phoenix_app
78+
run: |
79+
rm -f tmp/sentry_debug_events.log
80+
SENTRY_E2E_TEST_MODE=true mix phx.server &
81+
echo $! > /tmp/phoenix.pid
82+
echo "Phoenix server started with PID $(cat /tmp/phoenix.pid)"
83+
84+
- name: Start Svelte server
85+
working-directory: test_integrations/tracing/svelte_mini
86+
run: |
87+
SENTRY_E2E_SVELTE_APP_PORT=4001 npm run dev &
88+
echo $! > /tmp/svelte.pid
89+
echo "Svelte server started with PID $(cat /tmp/svelte.pid)"
90+
91+
- name: Wait for Phoenix server
92+
run: |
93+
echo "Waiting for Phoenix server at http://localhost:4000/health..."
94+
timeout 60 bash -c 'until curl -s http://localhost:4000/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
95+
echo "Phoenix server is ready!"
96+
curl -s http://localhost:4000/health
97+
98+
- name: Wait for Svelte server
99+
run: |
100+
echo "Waiting for Svelte server at http://localhost:4001/health..."
101+
timeout 60 bash -c 'until curl -s http://localhost:4001/health > /dev/null 2>&1; do echo "Waiting..."; sleep 2; done'
102+
echo "Svelte server is ready!"
103+
curl -s http://localhost:4001/health
104+
105+
- name: Run Playwright tests
106+
working-directory: test_integrations/tracing
107+
env:
108+
SENTRY_E2E_PHOENIX_APP_URL: http://localhost:4000
109+
SENTRY_E2E_SVELTE_APP_URL: http://localhost:4001
110+
SENTRY_E2E_SERVERS_RUNNING: "true"
111+
run: npx playwright test --reporter=list
112+
113+
- name: Upload Playwright report
114+
uses: actions/upload-artifact@v4
115+
if: failure()
116+
with:
117+
name: playwright-report
118+
path: test_integrations/tracing/playwright-report/
119+
retention-days: 7
120+
121+
- name: Upload test screenshots
122+
uses: actions/upload-artifact@v4
123+
if: failure()
124+
with:
125+
name: test-screenshots
126+
path: test_integrations/tracing/test-results/
127+
retention-days: 7
128+
129+
- name: Show Phoenix server logs
130+
if: failure()
131+
run: |
132+
echo "=== Sentry debug events log ==="
133+
cat test_integrations/phoenix_app/tmp/sentry_debug_events.log 2>/dev/null || echo "No events logged"
134+
135+
- name: Cleanup servers
136+
if: always()
137+
run: |
138+
[ -f /tmp/phoenix.pid ] && kill $(cat /tmp/phoenix.pid) 2>/dev/null || true
139+
[ -f /tmp/svelte.pid ] && kill $(cat /tmp/svelte.pid) 2>/dev/null || true

.github/workflows/main.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
uses: actions/checkout@v4
5555

5656
- name: Setup Elixir and Erlang
57-
uses: erlef/setup-beam@v1
57+
uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9
5858
with:
5959
elixir-version: ${{ matrix.elixir }}
6060
otp-version: ${{ matrix.otp }}
@@ -117,6 +117,8 @@ jobs:
117117
run: mix test
118118

119119
- name: Run integration tests
120+
env:
121+
SKIP_TRACING_E2E: "true"
120122
run: mix test.integrations
121123

122124
- name: Cache Dialyzer PLT

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ test_integrations/phoenix_app/db
1717

1818
test_integrations/*/_build
1919
test_integrations/*/deps
20+
test_integrations/*/test-results/

test_integrations/phoenix_app/config/config.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ config :opentelemetry, span_processor: {Sentry.OpenTelemetry.SpanProcessor, []}
6565
config :opentelemetry,
6666
sampler: {Sentry.OpenTelemetry.Sampler, [drop: ["Elixir.Oban.Stager process"]]}
6767

68+
# Configure OpenTelemetry to use Sentry propagator for distributed tracing
69+
config :opentelemetry,
70+
text_map_propagators: [
71+
:trace_context,
72+
:baggage,
73+
Sentry.OpenTelemetry.Propagator
74+
]
75+
6876
# Import environment specific config. This must remain at the bottom
6977
# of this file so it overrides the configuration defined above.
7078
import_config "#{config_env()}.exs"

test_integrations/phoenix_app/config/runtime.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ if System.get_env("PHX_SERVER") do
2020
config :phoenix_app, PhoenixAppWeb.Endpoint, server: true
2121
end
2222

23+
# For e2e tracing tests, use the TestClient to log events to a file
24+
# This must be in runtime.exs because the env var is set at runtime, not compile time
25+
if System.get_env("SENTRY_E2E_TEST_MODE") == "true" do
26+
config :sentry,
27+
dsn: "https://public@sentry.example.com/1",
28+
client: PhoenixApp.TestClient
29+
else
30+
# Allow runtime configuration of Sentry DSN and environment
31+
if dsn = System.get_env("SENTRY_DSN") do
32+
config :sentry,
33+
dsn: dsn,
34+
environment_name: System.get_env("SENTRY_ENVIRONMENT") || config_env()
35+
end
36+
end
37+
2338
if config_env() == :prod do
2439
# database_url =
2540
# System.get_env("DATABASE_URL") ||
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
defmodule PhoenixApp.TestClient do
2+
@moduledoc """
3+
A test Sentry client that logs envelopes to a file for e2e test validation.
4+
5+
This client mimics the behavior of Sentry::DebugTransport in sentry-ruby,
6+
logging all envelopes to a file that can be read by Playwright tests.
7+
"""
8+
9+
require Logger
10+
11+
@behaviour Sentry.HTTPClient
12+
13+
@impl true
14+
def post(_url, _headers, body) do
15+
log_envelope(body)
16+
17+
# Return success response
18+
{:ok, 200, [], ~s({"id":"test-event-id"})}
19+
end
20+
21+
defp log_envelope(body) when is_binary(body) do
22+
log_file = Path.join([File.cwd!(), "tmp", "sentry_debug_events.log"])
23+
24+
# Ensure the tmp directory exists
25+
log_dir = Path.dirname(log_file)
26+
File.mkdir_p!(log_dir)
27+
28+
# Parse the envelope binary to extract events and headers
29+
case parse_envelope(body) do
30+
{:ok, envelope_data} ->
31+
# Write the envelope data as JSON
32+
json = Jason.encode!(envelope_data)
33+
File.write!(log_file, json <> "\n", [:append])
34+
35+
{:error, reason} ->
36+
Logger.warning("Failed to parse envelope for logging: #{inspect(reason)}")
37+
end
38+
rescue
39+
error ->
40+
Logger.warning("Failed to log envelope: #{inspect(error)}")
41+
end
42+
43+
defp parse_envelope(body) when is_binary(body) do
44+
# Envelope format: header\nitem_header\nitem_payload[\nitem_header\nitem_payload...]
45+
# See: https://develop.sentry.dev/sdk/envelopes/
46+
47+
lines = String.split(body, "\n")
48+
49+
with {:ok, header_line, rest} <- get_first_line(lines),
50+
{:ok, envelope_headers} <- Jason.decode(header_line),
51+
{:ok, items} <- parse_items(rest) do
52+
53+
envelope = %{
54+
headers: envelope_headers,
55+
items: items
56+
}
57+
58+
{:ok, envelope}
59+
else
60+
error -> {:error, error}
61+
end
62+
end
63+
64+
defp get_first_line([first | rest]), do: {:ok, first, rest}
65+
defp get_first_line([]), do: {:error, :empty_envelope}
66+
67+
defp parse_items(lines), do: parse_items(lines, [])
68+
69+
defp parse_items([], acc), do: {:ok, Enum.reverse(acc)}
70+
71+
defp parse_items([item_header_line, payload_line | rest], acc) do
72+
with {:ok, _item_header} <- Jason.decode(item_header_line),
73+
{:ok, payload} <- Jason.decode(payload_line) do
74+
parse_items(rest, [payload | acc])
75+
else
76+
_error ->
77+
# Skip malformed items
78+
parse_items(rest, acc)
79+
end
80+
end
81+
82+
defp parse_items([_single_line], acc) do
83+
# Handle trailing empty line
84+
{:ok, Enum.reverse(acc)}
85+
end
86+
end

test_integrations/phoenix_app/lib/phoenix_app_web/controllers/page_controller.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,35 @@ defmodule PhoenixAppWeb.PageController do
4848

4949
render(conn, :home, layout: false)
5050
end
51+
52+
# E2E tracing test endpoints
53+
54+
def api_error(_conn, _params) do
55+
raise ArithmeticError, "bad argument in arithmetic expression"
56+
end
57+
58+
def health(conn, _params) do
59+
json(conn, %{status: "ok"})
60+
end
61+
62+
def api_data(conn, _params) do
63+
Tracer.with_span "fetch_data" do
64+
users = Repo.all(User)
65+
66+
Tracer.with_span "process_data" do
67+
user_count = length(users)
68+
69+
first_user = Repo.get(User, 1)
70+
71+
json(conn, %{
72+
message: "Data fetched successfully",
73+
data: %{
74+
user_count: user_count,
75+
first_user: if(first_user, do: first_user.name, else: nil),
76+
timestamp: DateTime.utc_now() |> DateTime.to_iso8601()
77+
}
78+
})
79+
end
80+
end
81+
end
5182
end

test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ defmodule PhoenixAppWeb.Endpoint do
4444
plug Plug.RequestId
4545
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
4646

47+
plug :cors
48+
4749
plug Plug.Parsers,
4850
parsers: [:urlencoded, :multipart, :json],
4951
pass: ["*/*"],
@@ -53,4 +55,22 @@ defmodule PhoenixAppWeb.Endpoint do
5355
plug Plug.Head
5456
plug Plug.Session, @session_options
5557
plug PhoenixAppWeb.Router
58+
59+
# CORS plug for e2e tests - allows the Svelte frontend to make
60+
# cross-origin requests to the Phoenix backend
61+
defp cors(conn, _opts) do
62+
conn
63+
|> put_resp_header("access-control-allow-origin", "*")
64+
|> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
65+
|> put_resp_header("access-control-allow-headers", "content-type, sentry-trace, baggage")
66+
|> handle_preflight()
67+
end
68+
69+
defp handle_preflight(%{method: "OPTIONS"} = conn) do
70+
conn
71+
|> send_resp(200, "")
72+
|> halt()
73+
end
74+
75+
defp handle_preflight(conn), do: conn
5676
end

test_integrations/phoenix_app/lib/phoenix_app_web/router.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ defmodule PhoenixAppWeb.Router do
1212

1313
pipeline :api do
1414
plug :accepts, ["json"]
15+
plug :put_cors_headers
16+
end
17+
18+
defp put_cors_headers(conn, _opts) do
19+
conn
20+
|> put_resp_header("access-control-allow-origin", "*")
21+
|> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
22+
|> put_resp_header("access-control-allow-headers", "content-type, authorization, sentry-trace, baggage")
1523
end
1624

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

43+
# For e2e DT tests with a front-end app
44+
scope "/", PhoenixAppWeb do
45+
pipe_through :api
46+
47+
get "/error", PageController, :api_error
48+
get "/health", PageController, :health
49+
get "/api/data", PageController, :api_data
50+
end
51+
3552
# Other scopes may use custom stacks.
3653
# scope "/api", PhoenixAppWeb do
3754
# pipe_through :api

0 commit comments

Comments
 (0)