Skip to content

Commit 526b78b

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

File tree

28 files changed

+1527
-5
lines changed

28 files changed

+1527
-5
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/

CONTRIBUTING.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ mix dialyzer
5555

5656
Once all checks are passing locally, you are ready to [open a pull request](https://help.github.com/articles/using-pull-requests/).
5757

58+
## Manual Testing with Integration Apps
59+
60+
For manual testing of integrations, you can use the `mix test.apps.start` task to start the Phoenix test application with a custom Sentry DSN:
61+
62+
```bash
63+
# Start the Phoenix app with your DSN
64+
mix test.apps.start --dsn https://your-public-key@o123456.ingest.sentry.io/123456
65+
66+
# Or set the DSN via environment variable
67+
export SENTRY_DSN=https://your-public-key@o123456.ingest.sentry.io/123456
68+
mix test.apps.start
69+
```
70+
71+
The Phoenix app will start on `http://localhost:4000` and includes:
72+
- LiveView pages for testing UI interactions
73+
- Oban for background job processing
74+
- OpenTelemetry integration for distributed tracing
75+
- Various error scenarios for testing error reporting
76+
77+
This is useful for testing changes to integrations like Phoenix, Plug, Oban, or OpenTelemetry before submitting a pull request.
78+
5879
That's it. You should be ready to make changes, run tests, and make commits! If you experience any problems, please don't hesitate to ping us in our [Discord Community](https://discord.com/invite/Ww9hbqr).
5980

6081
## Releasing a New Version

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://public@sentry.io/123
27+
28+
# Use DSN from SENTRY_DSN environment variable
29+
$ export SENTRY_DSN=https://public@sentry.io/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: 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,

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: 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

0 commit comments

Comments
 (0)