feat(observability): session-replay + in-app feedback on catalog UX failures
The two things only the browser can add on top of the backend catalog-degradation reporting: - Replay-on-failure: when the catalog UI renders empty-tree (decoded vehicle, no categories) or a drill loadError, capture a browser Sentry warning and flush the Session Replay → you can WATCH the user hit the dead-end (serkan's session, reproducible). Per-session deduped (one replay/session covers the whole journey). - In-app feedback: a "Çalışmadı mı? Bildir" button on the empty-parts, empty-tree and loadError states opens the Sentry feedback dialog pre-tagged with the vehicle/category (+ session replay) — turns a parts shop's complaint into a structured, triageable report instead of an email. Browser events are fingerprinted source="browser" so they form their own "what users actually saw" issues (carrying replays) next to the server-side detections. tsc + biome + web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
ReportCatalogIssueButton,
|
||||
useReportCatalogDegradation,
|
||||
} from "@/components/catalog/report-catalog-issue";
|
||||
import { CategoryBreadcrumb } from "@/components/categories/category-breadcrumb";
|
||||
import { CategoryColumns } from "@/components/categories/category-columns";
|
||||
import { CategoryGrid } from "@/components/categories/category-grid";
|
||||
@@ -61,7 +65,9 @@ function VehicleCategoryPage() {
|
||||
const { data: vehicle } = useQuery({
|
||||
queryKey: ["vehicle", id],
|
||||
queryFn: () =>
|
||||
api.get<{ brandName?: string; model?: string; year?: number }>(`/vehicles/${id}`),
|
||||
api.get<{ brandName?: string; model?: string; year?: number; source?: string; vin?: string }>(
|
||||
`/vehicles/${id}`,
|
||||
),
|
||||
enabled: !!id,
|
||||
});
|
||||
|
||||
@@ -97,6 +103,17 @@ function VehicleCategoryPage() {
|
||||
? `${vehicle.brandName}${cleanModel ? ` ${cleanModel}` : ""}`
|
||||
: t("vehicle.title");
|
||||
|
||||
// A loadError = the drill/parts fetch failed and the user sees a "couldn't load"
|
||||
// panel instead of parts — report it (with a session replay) so it surfaces.
|
||||
useReportCatalogDegradation("drill-load-error", !!data?.loadError, {
|
||||
vehicleId: id,
|
||||
categoryId,
|
||||
categoryName: data?.name,
|
||||
vehicleLabel,
|
||||
source: vehicle?.source,
|
||||
vin: vehicle?.vin,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<CategoryBreadcrumb
|
||||
@@ -188,16 +205,28 @@ function VehicleCategoryPage() {
|
||||
<p className="font-medium text-destructive">{t("vehicle.catalogUnavailableTitle")}</p>
|
||||
<p className="mt-1 text-muted-foreground">{t("vehicle.catalogUnavailableHint")}</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
data-faro-user-action-name="category-loaderror-retry"
|
||||
>
|
||||
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||
</Button>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
data-faro-user-action-name="category-loaderror-retry"
|
||||
>
|
||||
{isFetching ? t("common.loading") : t("vehicle.retry")}
|
||||
</Button>
|
||||
<ReportCatalogIssueButton
|
||||
ctx={{
|
||||
vehicleId: id,
|
||||
categoryId,
|
||||
categoryName: data?.name,
|
||||
vehicleLabel,
|
||||
source: vehicle?.source,
|
||||
vin: vehicle?.vin,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Suspense fallback={<SchemaViewerFallback />}>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
ReportCatalogIssueButton,
|
||||
useReportCatalogDegradation,
|
||||
} from "@/components/catalog/report-catalog-issue";
|
||||
import { CatalogSearch } from "@/components/categories/catalog-search";
|
||||
import { CategoryBreadcrumb } from "@/components/categories/category-breadcrumb";
|
||||
import { CategoryColumns } from "@/components/categories/category-columns";
|
||||
@@ -105,6 +109,16 @@ function VehicleDetailPage() {
|
||||
? `${vehicle.brandName}${cleanModelName(vehicle.model) ? ` ${cleanModelName(vehicle.model)}` : ""}`
|
||||
: t("vehicle.title");
|
||||
|
||||
// An empty category tree on a decoded vehicle is a silent failure ("model var,
|
||||
// kategori yok") — report it to Sentry with a session replay so it surfaces.
|
||||
const treeEmpty = !categoriesLoading && Array.isArray(categoryTree) && categoryTree.length === 0;
|
||||
useReportCatalogDegradation("empty-tree", treeEmpty, {
|
||||
vehicleId: id,
|
||||
vehicleLabel,
|
||||
source: vehicle?.source,
|
||||
vin: vehicle?.vin,
|
||||
});
|
||||
|
||||
// Surface the viewed vehicle to the support chat widget (VIN/brand/model)
|
||||
// so agents have the car context for part-compatibility questions.
|
||||
useEffect(() => {
|
||||
@@ -344,6 +358,16 @@ function VehicleDetailPage() {
|
||||
<p className="mx-auto mt-1 max-w-md text-xs text-muted-foreground">
|
||||
{t("vehicle.noCategoriesHint")}
|
||||
</p>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<ReportCatalogIssueButton
|
||||
ctx={{
|
||||
vehicleId: id,
|
||||
vehicleLabel,
|
||||
source: vehicle?.source,
|
||||
vin: vehicle?.vin,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : viewMode === "grid" ? (
|
||||
<CategoryGrid categories={categoryTree} vehicleId={id} hideFilter />
|
||||
|
||||
Reference in New Issue
Block a user