Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion test_integrations/phoenix_app/config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ config :sentry,
logs: [
level: :info,
metadata: :all
]
],
scrubber: [param_keys: ["internal_ref"]]

config :phoenix_app, Oban,
repo: PhoenixApp.Repo,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule PhoenixAppWeb.ScrubbingDemoController do
@moduledoc """
Fixture for the parameter-scrubbing e2e spec.

`index/2` renders a page to interact with: a reset form to fill in and submit,
and links carrying a secret in the path. `reset_password/2` never matches the
params those send, so Phoenix raises a `Phoenix.ActionClauseError` whose
message embeds the whole `%Plug.Conn{}`. That is what puts `request_path`,
`path_info`, `path_params` and `query_string` into the reported event,
alongside the request interface, so the spec can assert on each of them.
"""

use PhoenixAppWeb, :controller

plug Sentry.PlugContext,
[url_scrubber: {__MODULE__, :scrub_url}] when action == :reset_password

plug :tag_run when action == :reset_password

# The spec owns the exact bytes it wants on the wire, so the links echo the
# query this page was opened with rather than rebuilding it — rebuilding would
# re-encode it and hide what the scrubber does or does not rewrite. That puts
# the secrets in this page's own URL, which is the point: the browser sends it
# as the `Referer` of everything clicked from here.
def index(conn, params) do
path_secret = Map.get(params, "path_secret", "pathsecret")
reset_path = "/scrubbing-demo/reset-password/#{path_secret}"
query = drop_param(conn.query_string, "path_secret")

render(conn, :index,
form_action: reset_path,
probe_href: "#{reset_path}?#{query}",
forwarded_href: "/scrubbing-demo/forwarded/reset-password/#{path_secret}?#{query}",
keep: Map.get(params, "keep", ""),
run: Map.get(params, "e2e_run", "")
)
end

defp drop_param(query_string, name) do
query_string
|> String.split("&")
|> Enum.reject(&String.starts_with?(&1, "#{name}="))
|> Enum.join("&")
end

def reset_password(conn, %{"confirmed" => true}), do: json(conn, %{status: "ok"})

@doc """
Redacts the token segment of the demo path on top of the SDK's default URL
scrubbing, the way an application would for a URL that carries a secret.
"""
def scrub_url(conn) do
conn
|> Sentry.PlugContext.default_url_scrubber()
|> String.replace(
~r{/reset-password/[^/?]+},
"/reset-password/#{Sentry.Scrubber.scrubbed_value()}"
)
end

defp tag_run(conn, _opts) do
case conn.params do
%{"e2e_run" => run_id} when is_binary(run_id) ->
Sentry.Context.set_tags_context(%{"e2e_run" => run_id})
conn

_ ->
conn
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule PhoenixAppWeb.ScrubbingDemoHTML do
@moduledoc """
The page the parameter-scrubbing e2e spec interacts with.

See the `scrubbing_demo_html` directory for all templates available.
"""
use PhoenixAppWeb, :html

embed_templates "scrubbing_demo_html/*"
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div id="scrubbing-demo" class="mx-auto max-w-xl p-8">
<h1 class="text-2xl font-semibold">Reset your password</h1>

<form id="reset-form" method="get" action={@form_action} class="mt-6 space-y-3">
<input type="hidden" name="keep" value={@keep} />
<input type="hidden" name="e2e_run" value={@run} />

<label class="block">
Reset token <input type="text" id="token" name="token" class="border" />
</label>
<label class="block">
API key <input type="text" id="api_key" name="api_key" class="border" />
</label>
<label class="block">
PASSWORD <input type="text" id="upper_password" name="PASSWORD" class="border" />
</label>
<label class="block">
User password <input type="text" id="user_password" name="user_password" class="border" />
</label>
<label class="block">
Internal reference
<input type="text" id="internal_ref" name="internal_ref" class="border" />
</label>
<label class="block">
Password <input type="text" id="password" name="password" class="border" />
</label>

<button type="submit" id="submit-reset" class="rounded bg-zinc-900 px-4 py-2 text-white">
Reset password
</button>
</form>

<ul class="mt-8 space-y-2">
<li><a id="probe-link" href={@probe_href}>Open the reset link we emailed you</a></li>
<li><a id="forwarded-link" href={@forwarded_href}>Open it through the forwarded mount</a></li>
</ul>
</div>
5 changes: 5 additions & 0 deletions test_integrations/phoenix_app/lib/phoenix_app_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ defmodule PhoenixAppWeb.Router do

live "/users/:id", UserLive.Show, :show
live "/users/:id/show/edit", UserLive.Show, :edit

get "/scrubbing-demo", ScrubbingDemoController, :index
get "/scrubbing-demo/reset-password/:token", ScrubbingDemoController, :reset_password
end

# For e2e DT tests with a front-end app
Expand All @@ -74,6 +77,8 @@ defmodule PhoenixAppWeb.Router do
put "/sentry-test-config", TestConfigController, :update
end

forward "/scrubbing-demo/forwarded", PhoenixAppWeb.ScrubbingDemoForwardedRouter

# Other scopes may use custom stacks.
# scope "/api", PhoenixAppWeb do
# pipe_through :api
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule PhoenixAppWeb.ScrubbingDemoForwardedRouter do
use Phoenix.Router

pipeline :browser do
plug :accepts, ["html"]
end

scope "/", PhoenixAppWeb do
pipe_through :browser

get "/reset-password/:token", ScrubbingDemoController, :reset_password
end
end
7 changes: 6 additions & 1 deletion test_integrations/phoenix_app/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule PhoenixApp.MixProject do
start_permanent: Mix.env() == :prod,
aliases: aliases(),
deps: deps()
]
] ++ listeners(current_elixir_version())
end

# Configuration for the OTP application.
Expand Down Expand Up @@ -100,6 +100,11 @@ defmodule PhoenixApp.MixProject do

defp current_elixir_version, do: Version.parse!(System.version())

# Phoenix.CodeReloader warns on every recompile when it is not registered as a
# Mix listener. The `:listeners` option only exists from Elixir 1.18 on.
defp listeners(%Version{major: 1, minor: minor}) when minor < 18, do: []
defp listeners(%Version{}), do: [listeners: [Phoenix.CodeReloader]]

defp lockfile(%Version{major: 1, minor: minor}) when minor < 18,
do: "mix-1.15-1.17.lock"

Expand Down
13 changes: 11 additions & 2 deletions test_integrations/tracing/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const SVELTE_URL = requireEnv("SENTRY_E2E_SVELTE_APP_URL");

// When servers are started externally (e.g., in CI workflow steps), skip webServer config
const serversRunningExternally = process.env.SENTRY_E2E_SERVERS_RUNNING === "true";
const realDsn = process.env.SENTRY_E2E_REAL_DSN === "true";

const phoenixEnv: Record<string, string> = realDsn
? {
SENTRY_DSN: requireEnv("SENTRY_DSN"),
SENTRY_ENVIRONMENT: process.env.SENTRY_ENVIRONMENT ?? "e2e-scrubbing",
}
: { SENTRY_E2E_TEST_MODE: "true", SENTRY_ORG_ID: "123" };

export default defineConfig({
testDir: "./tests",
Expand Down Expand Up @@ -51,9 +59,10 @@ export default defineConfig({
webServer: [
{
command:
'cd ../phoenix_app && rm -f tmp/sentry_debug_events.log && SENTRY_E2E_TEST_MODE=true SENTRY_ORG_ID=123 mix phx.server',
'cd ../phoenix_app && rm -f tmp/sentry_debug_events.log && mix phx.server',
url: `${PHOENIX_URL}/health`,
reuseExistingServer: true
env: phoenixEnv,
reuseExistingServer: !realDsn
},
{
command:
Expand Down
Loading
Loading