Skip to content

Commit d994013

Browse files
committed
wip - e2e tests for distributed tracing
1 parent 22dfc60 commit d994013

File tree

23 files changed

+1127
-1
lines changed

23 files changed

+1127
-1
lines changed

.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/

mix.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ defmodule Sentry.Mixfile do
154154
run_integration_tests("umbrella", args)
155155
run_integration_tests("phoenix_app", args)
156156
run_integration_tests("legacy_otel", args)
157+
run_integration_tests("tracing", args)
157158
else
158159
Mix.shell().info("Skipping integration tests for Elixir versions < 1.16")
159160
end

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/dev.exs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,19 @@ dsn =
8484
do: System.get_env("SENTRY_DSN_LOCAL"),
8585
else: System.get_env("SENTRY_DSN")
8686

87+
# For e2e tracing tests, use the TestClient to log events to a file
88+
client =
89+
if System.get_env("SENTRY_E2E_TEST_MODE") == "true",
90+
do: PhoenixApp.TestClient,
91+
else: Sentry.HackneyClient
92+
8793
config :sentry,
8894
dsn: dsn,
8995
environment_name: :dev,
9096
enable_source_code_context: true,
9197
send_result: :sync,
92-
traces_sample_rate: 1.0
98+
traces_sample_rate: 1.0,
99+
client: client
93100

94101
config :phoenix_app, Oban,
95102
repo: PhoenixApp.Repo,
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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,37 @@ defmodule PhoenixAppWeb.PageController do
4848

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

test_integrations/phoenix_app/lib/phoenix_app_web/endpoint.ex

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

47+
# Simple CORS handler for e2e tests
48+
plug :cors
49+
50+
defp cors(conn, _opts) do
51+
conn
52+
|> put_resp_header("access-control-allow-origin", "*")
53+
|> put_resp_header("access-control-allow-methods", "GET, POST, PUT, DELETE, OPTIONS")
54+
|> put_resp_header("access-control-allow-headers", "content-type, sentry-trace, baggage")
55+
|> then(fn conn ->
56+
if conn.method == "OPTIONS" do
57+
conn
58+
|> send_resp(200, "")
59+
|> halt()
60+
else
61+
conn
62+
end
63+
end)
64+
end
65+
4766
plug Plug.Parsers,
4867
parsers: [:urlencoded, :multipart, :json],
4968
pass: ["*/*"],

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+
# API endpoints for e2e tracing tests
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

test_integrations/phoenix_app/test/phoenix_app_web/controllers/transaction_test.exs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,29 @@ defmodule Sentry.Integrations.Phoenix.TransactionTest do
129129

130130
refute mount_transaction.contexts.trace.trace_id == handle_params_transaction.contexts.trace.trace_id
131131
end
132+
133+
test "GET /users with distributed tracing headers includes child spans with details", %{conn: conn} do
134+
trace_id = "1234567890abcdef1234567890abcdef"
135+
span_id = "1234567890abcdef"
136+
sentry_trace = "#{trace_id}-#{span_id}-1"
137+
138+
conn = put_req_header(conn, "sentry-trace", sentry_trace)
139+
get(conn, ~p"/users")
140+
141+
transactions = Sentry.Test.pop_sentry_transactions()
142+
143+
assert length(transactions) == 2
144+
145+
assert [mount_transaction, _handle_params_transaction] = transactions
146+
147+
assert mount_transaction.contexts.trace.trace_id == trace_id
148+
assert mount_transaction.contexts.trace.parent_span_id != nil
149+
150+
assert [span_ecto] = mount_transaction.spans
151+
assert span_ecto.op == "db"
152+
153+
assert span_ecto.description == "SELECT u0.\"id\", u0.\"name\", u0.\"age\", u0.\"inserted_at\", u0.\"updated_at\" FROM \"users\" AS u0"
154+
155+
assert span_ecto.data["db.system"] != nil
156+
end
132157
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
*.beam
2+
*.ez
3+
_build/
4+
deps/
5+
doc/
6+
.fetch
7+
erl_crash.dump
8+
*.plt
9+
*.plt.hash
10+
esbuild
11+
.elixir_ls/
12+
.lexical/
13+
14+
# Phoenix mini
15+
phoenix_mini/tmp/
16+
phoenix_mini/_build/
17+
phoenix_mini/deps/
18+
19+
# Svelte mini
20+
svelte_mini/node_modules/
21+
svelte_mini/dist/
22+
svelte_mini/.vite/
23+
svelte_mini/package-lock.json
24+
25+
# Test artifacts
26+
tmp/
27+
.DS_Store

0 commit comments

Comments
 (0)