add analytics-powered 404 monitoring with FTS5 auto-resolve
All checks were successful
deploy / deploy (push) Successful in 9m48s
All checks were successful
deploy / deploy (push) Successful in 9m48s
BrokenUrlTracker now queries real analytics pageview counts instead of hardcoding 0, so broken URLs with prior traffic are distinguished from bot noise. For /products/ 404s with a single FTS5 search match, auto- creates a redirect and marks the broken URL resolved. 1232 tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -364,6 +364,20 @@ defmodule Berrypod.Analytics do
|
||||
|> Repo.all()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts all-time pageviews for a specific path.
|
||||
|
||||
Used by the broken URL tracker to distinguish real broken URLs
|
||||
(paths that previously had traffic) from bot noise.
|
||||
"""
|
||||
def count_pageviews_for_path(path) do
|
||||
from(e in Event,
|
||||
where: e.name == "pageview" and e.pathname == ^path,
|
||||
select: count()
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes events older than the given datetime. Used by the retention worker.
|
||||
"""
|
||||
|
||||
@@ -336,6 +336,44 @@ defmodule Berrypod.Redirects do
|
||||
Repo.one(from b in BrokenUrl, where: b.path == ^path and b.status == "pending")
|
||||
end
|
||||
|
||||
# ── Auto-resolve ──
|
||||
|
||||
@doc """
|
||||
Attempts to auto-resolve a broken product URL using FTS5 search.
|
||||
|
||||
Converts the slug back to words and searches the product index.
|
||||
If exactly one result is found, creates a redirect and marks
|
||||
the broken URL as resolved. Multiple or zero results are left
|
||||
for admin review.
|
||||
"""
|
||||
def attempt_auto_resolve("/products/" <> old_slug = path) do
|
||||
query = old_slug |> String.replace("-", " ") |> String.trim()
|
||||
|
||||
case Berrypod.Search.search(query) do
|
||||
[%{slug: new_slug}] when new_slug != old_slug ->
|
||||
case create_auto(%{
|
||||
from_path: path,
|
||||
to_path: "/products/#{new_slug}",
|
||||
source: "analytics_auto_resolved"
|
||||
}) do
|
||||
{:ok, _redirect} ->
|
||||
if broken = get_broken_url_by_path(path) do
|
||||
mark_broken_url_resolved(broken)
|
||||
end
|
||||
|
||||
:resolved
|
||||
|
||||
_error ->
|
||||
:no_match
|
||||
end
|
||||
|
||||
_ ->
|
||||
:no_match
|
||||
end
|
||||
end
|
||||
|
||||
def attempt_auto_resolve(_path), do: :no_match
|
||||
|
||||
# ── Pruning ──
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule Berrypod.Redirects.Redirect do
|
||||
@primary_key {:id, :binary_id, autogenerate: true}
|
||||
@foreign_key_type :binary_id
|
||||
|
||||
@sources ~w(auto_slug_change auto_product_deleted analytics_detected admin)
|
||||
@sources ~w(auto_slug_change auto_product_deleted analytics_auto_resolved admin)
|
||||
|
||||
schema "redirects" do
|
||||
field :from_path, :string
|
||||
|
||||
@@ -20,7 +20,12 @@ defmodule BerrypodWeb.Plugs.BrokenUrlTracker do
|
||||
rescue
|
||||
e in Phoenix.Router.NoRouteError ->
|
||||
unless static_path?(conn.request_path) do
|
||||
Berrypod.Redirects.record_broken_url(conn.request_path, 0)
|
||||
prior_hits = Berrypod.Analytics.count_pageviews_for_path(conn.request_path)
|
||||
Berrypod.Redirects.record_broken_url(conn.request_path, prior_hits)
|
||||
|
||||
if prior_hits > 0 do
|
||||
Berrypod.Redirects.attempt_auto_resolve(conn.request_path)
|
||||
end
|
||||
end
|
||||
|
||||
reraise e, __STACKTRACE__
|
||||
|
||||
Reference in New Issue
Block a user