Skip to content

Commit 3c486b6

Browse files
committed
wip - add E2E distributed tracing tests
1 parent fa5bf68 commit 3c486b6

File tree

28 files changed

+1725
-32
lines changed

28 files changed

+1725
-32
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/

lib/mix/tasks/test.apps.start.ex

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
defmodule Mix.Tasks.Test.Apps.Start do
2+
use Mix.Task
3+
4+
@shortdoc "Start an integration test app for manual testing"
5+
6+
@moduledoc """
7+
Starts an integration test application for manual testing with a custom DSN.
8+
9+
## Usage
10+
11+
$ mix test.apps.start --app phoenix_app --dsn YOUR_DSN
12+
13+
## Options
14+
15+
* `--app` - The integration app to start. Available apps:
16+
* `phoenix_app` (default) - Phoenix LiveView application with Oban and OpenTelemetry
17+
18+
* `--dsn` - The Sentry DSN to use (required). Can be a full DSN URL or
19+
omitted to use the DSN from environment variables.
20+
21+
* `--environment` - The environment name to report to Sentry (default: "manual-test")
22+
23+
## Examples
24+
25+
# Start phoenix_app with a custom DSN
26+
$ mix test.apps.start --dsn https://[email protected]/123
27+
28+
# Use DSN from SENTRY_DSN environment variable
29+
$ export SENTRY_DSN=https://[email protected]/123
30+
$ mix test.apps.start
31+
32+
"""
33+
34+
@switches [
35+
app: :string,
36+
dsn: :string,
37+
environment: :string
38+
]
39+
40+
@available_apps ["phoenix_app"]
41+
42+
@impl true
43+
def run(args) when is_list(args) do
44+
{opts, _args} = OptionParser.parse!(args, strict: @switches)
45+
46+
app = Keyword.get(opts, :app, "phoenix_app")
47+
dsn = Keyword.get(opts, :dsn) || System.get_env("SENTRY_DSN")
48+
environment = Keyword.get(opts, :environment, "manual-test")
49+
50+
unless app in @available_apps do
51+
Mix.raise("""
52+
Invalid app: #{app}
53+
54+
Available apps:
55+
#{Enum.map_join(@available_apps, "\n", &" - #{&1}")}
56+
""")
57+
end
58+
59+
unless dsn do
60+
Mix.raise("""
61+
No DSN provided. Please provide a DSN via:
62+
--dsn flag: mix test.apps.start --dsn YOUR_DSN
63+
Or set SENTRY_DSN environment variable
64+
""")
65+
end
66+
67+
app_path = Path.join("test_integrations", app)
68+
69+
unless File.dir?(app_path) do
70+
Mix.raise("Integration app not found: #{app_path}")
71+
end
72+
73+
Mix.shell().info([
74+
:cyan,
75+
:bright,
76+
"\n==> Starting integration app: #{app}",
77+
:reset
78+
])
79+
80+
Mix.shell().info("DSN: #{mask_dsn(dsn)}")
81+
Mix.shell().info("Environment: #{environment}\n")
82+
83+
# Set up dependencies
84+
Mix.shell().info("Installing dependencies...")
85+
86+
case System.cmd("mix", ["deps.get"], cd: app_path, into: IO.stream(:stdio, :line)) do
87+
{_, 0} -> :ok
88+
{_, status} -> Mix.raise("Failed to install dependencies (exit status: #{status})")
89+
end
90+
91+
# Check if overmind is available
92+
case System.cmd("which", ["overmind"], stderr_to_stdout: true) do
93+
{_, 0} ->
94+
# Set environment variables
95+
env = [
96+
{"SENTRY_DSN", dsn},
97+
{"SENTRY_ENVIRONMENT", environment}
98+
]
99+
100+
# Start the application
101+
Mix.shell().info([
102+
:green,
103+
:bright,
104+
"\n==> Starting #{app} with Overmind...",
105+
:reset,
106+
"\n"
107+
])
108+
109+
System.cmd("overmind", ["start"],
110+
cd: app_path,
111+
into: IO.stream(:stdio, :line),
112+
env: env
113+
)
114+
115+
_ ->
116+
Mix.raise("""
117+
Overmind is not installed. Please install it:
118+
119+
macOS: brew install overmind tmux
120+
Linux: go install github.com/DarthSim/overmind/v2@latest
121+
122+
Then add to PATH: export PATH=$PATH:$(go env GOPATH)/bin
123+
""")
124+
end
125+
end
126+
127+
defp mask_dsn(dsn) do
128+
case URI.parse(dsn) do
129+
%URI{userinfo: userinfo} when is_binary(userinfo) ->
130+
String.replace(dsn, userinfo, "***")
131+
132+
_ ->
133+
dsn
134+
end
135+
end
136+
end

mix.exs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,8 @@ defmodule Sentry.Mixfile do
2222
plt_add_apps: [:mix, :ex_unit]
2323
],
2424
test_coverage: [tool: ExCoveralls],
25-
preferred_cli_env: [
26-
"coveralls.html": :test,
27-
"test.integrations": :test
28-
],
2925
name: "Sentry",
26+
cli: cli(),
3027
docs: [
3128
extra_section: "Guides",
3229
extras: [
@@ -93,6 +90,16 @@ defmodule Sentry.Mixfile do
9390
defp test_paths(nil), do: ["test"]
9491
defp test_paths(integration), do: ["test_integrations/#{integration}/test"]
9592

93+
defp cli do
94+
[
95+
preferred_envs: [
96+
"coveralls.html": :test,
97+
"test.integrations": :test,
98+
"test.apps.start": :dev
99+
]
100+
]
101+
end
102+
96103
defp deps do
97104
[
98105
{:nimble_options, "~> 1.0"},
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
phoenix: mix phx.server

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: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ config :phoenix_app, PhoenixApp.Repo,
1111
# The watchers configuration can be used to run external
1212
# watchers to your application. For example, we can use it
1313
# to bundle .js and .css sources.
14+
#
15+
# For e2e tests, disable debug_errors so exceptions propagate properly and
16+
# create transactions. In regular dev mode, enable debug_errors for a better
17+
# debugging experience.
18+
debug_errors_setting = System.get_env("SENTRY_E2E_TEST_MODE") != "true"
19+
1420
config :phoenix_app, PhoenixAppWeb.Endpoint,
1521
# Binding to loopback ipv4 address prevents access from other machines.
1622
# Change to `ip: {0, 0, 0, 0}` to allow access from other machines.
1723
http: [ip: {127, 0, 0, 1}, port: 4000],
1824
check_origin: false,
1925
code_reloader: true,
20-
debug_errors: true,
26+
debug_errors: debug_errors_setting,
2127
secret_key_base: "ssyvoq7X2T73sT2ZBVN1QEG0tNTulcqGf9FwczYr4RpmegDucPWqdVDhLtH8YhcB",
2228
watchers: [
2329
esbuild: {Esbuild, :install_and_run, [:phoenix_app, ~w(--sourcemap=inline --watch)]},
@@ -80,16 +86,29 @@ config :phoenix_live_view,
8086
config :swoosh, :api_client, false
8187

8288
dsn =
83-
if System.get_env("SENTRY_LOCAL"),
84-
do: System.get_env("SENTRY_DSN_LOCAL"),
85-
else: System.get_env("SENTRY_DSN")
89+
cond do
90+
System.get_env("SENTRY_E2E_TEST_MODE") == "true" ->
91+
# Use a fake DSN for e2e tests so events are processed
92+
"https://[email protected]/1"
93+
System.get_env("SENTRY_LOCAL") ->
94+
System.get_env("SENTRY_DSN_LOCAL")
95+
true ->
96+
System.get_env("SENTRY_DSN")
97+
end
98+
99+
# For e2e tracing tests, use the TestClient to log events to a file
100+
client =
101+
if System.get_env("SENTRY_E2E_TEST_MODE") == "true",
102+
do: PhoenixApp.TestClient,
103+
else: Sentry.HackneyClient
86104

87105
config :sentry,
88106
dsn: dsn,
89107
environment_name: :dev,
90108
enable_source_code_context: true,
91109
send_result: :sync,
92-
traces_sample_rate: 1.0
110+
traces_sample_rate: 1.0,
111+
client: client
93112

94113
config :phoenix_app, Oban,
95114
repo: PhoenixApp.Repo,

test_integrations/phoenix_app/config/runtime.exs

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

23+
# Allow runtime configuration of Sentry DSN and environment
24+
if dsn = System.get_env("SENTRY_DSN") do
25+
config :sentry,
26+
dsn: dsn,
27+
environment_name: System.get_env("SENTRY_ENVIRONMENT") || config_env()
28+
end
29+
2330
if config_env() == :prod do
2431
# database_url =
2532
# 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

0 commit comments

Comments
 (0)