|
| 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 |
0 commit comments