Fix production errors: onboarding crash hardening + DataForSEO spend/noise cleanup (#282)

This commit is contained in:
Ben Senescu 2026-06-30 17:41:21 -04:00 committed by Ben Senescu
parent 868924e1fe
commit 4e48f8344f
10 changed files with 161 additions and 12 deletions

View File

@ -9,6 +9,36 @@ let browserPostHogClientPromise: Promise<BrowserPostHogClient | null> | null =
let browserPostHogInitialized = false;
let analyticsCaptureEnabled = true;
type ExceptionEntry = {
value?: unknown;
mechanism?: { synthetic?: boolean };
stacktrace?: { frames?: unknown[] };
};
// Unactionable exceptions we don't want polluting error tracking. They share
// the trait of not being our code: browser extensions inject promise rejections
// and cross-origin scripts surface as a detail-less "Script error.", while the
// global onerror handler synthesizes a stackless "undefined" when it fires
// without a real Error object. Real app errors always carry a stack, so the
// "undefined" rule is gated on synthetic + no frames to avoid false drops.
function isIgnorableException(
properties: Record<string, unknown> | undefined,
): boolean {
const list = properties?.["$exception_list"];
if (!Array.isArray(list) || list.length === 0) return false;
return list.every((entry: ExceptionEntry) => {
const value = typeof entry?.value === "string" ? entry.value : "";
if (value.includes("Object Not Found Matching Id")) return true;
if (value === "Script error.") return true;
const frames = entry?.stacktrace?.frames;
return (
value === "undefined" &&
entry?.mechanism?.synthetic === true &&
(!Array.isArray(frames) || frames.length === 0)
);
});
}
function getBrowserPostHogClient(): Promise<BrowserPostHogClient | null> {
if (typeof window === "undefined" || !isHostedClientAuthMode()) {
return Promise.resolve(null);
@ -34,6 +64,15 @@ function getBrowserPostHogClient(): Promise<BrowserPostHogClient | null> {
api_host: host,
defaults: "2026-01-30",
capture_exceptions: true,
before_send(event) {
if (
event?.event === "$exception" &&
isIgnorableException(event.properties)
) {
return null;
}
return event;
},
capture_pageview: "history_change",
respect_dnt: true,
session_recording: {

View File

@ -48,6 +48,15 @@ export const Route = createRootRoute({
name: "viewport",
content: "width=device-width, initial-scale=1, viewport-fit=cover",
},
// Disable browser auto-translate (Google Translate) app-wide. It rewrites
// text nodes into <font> wrappers, which React then can't remove/insert,
// crashing render with NotFoundError ("removeChild"/"insertBefore"). The
// product UI is data-dense (keywords, domains, metrics) and not meaningful
// to machine-translate; the marketing site is a separate app and unaffected.
{
name: "google",
content: "notranslate",
},
{
name: "apple-mobile-web-app-capable",
content: "yes",
@ -155,7 +164,7 @@ function RootDocument({ children }: { children: React.ReactNode }) {
import.meta.env.DEV && import.meta.env.VITE_SHOW_DEVTOOLS !== "false";
return (
<html suppressHydrationWarning>
<html suppressHydrationWarning translate="no">
<head>
<script
dangerouslySetInnerHTML={{ __html: themePreferenceInitScript }}

View File

@ -173,14 +173,26 @@ async function remove(auditId: string, projectId: string) {
);
}
const instance = await env.SITE_AUDIT_WORKFLOW.get(
audit.workflowInstanceId,
);
try {
const instance = await env.SITE_AUDIT_WORKFLOW.get(
audit.workflowInstanceId,
);
await instance.terminate();
} catch (error) {
console.error(`Failed to terminate audit workflow ${audit.id}:`, error);
throw new AppError("CONFLICT", "Unable to stop the running audit.");
// terminate() throws when the instance already reached a terminal state
// (it completed or errored in the moment before the user hit stop). That
// race shouldn't block deletion — re-check the live status and only fail
// if the workflow is genuinely still running.
const status = await instance.status().catch(() => null);
const stillRunning =
status != null &&
["queued", "running", "paused", "waiting", "waitingForPause"].includes(
status.status,
);
if (stillRunning) {
console.error(`Failed to terminate audit workflow ${audit.id}:`, error);
throw new AppError("CONFLICT", "Unable to stop the running audit.");
}
}
}

View File

@ -29,7 +29,10 @@ export async function fetchBusinessListingsSearch(input: {
limit: input.limit,
}),
]);
const task = assertOk(response);
// "No Search Results" (40501) is a valid empty result for obscure
// businesses/keywords — DataForSEO still charges for it, so treat it as an
// empty success instead of surfacing a charged-task error to the user.
const task = assertOk(response, { treatNoResultsAsEmpty: true });
return {
data: task.result?.[0]?.items ?? [],
billing: buildTaskBilling(task),
@ -75,7 +78,10 @@ export async function fetchQuestionsAnswers(input: {
depth: input.depth,
}),
]);
const task = assertOk(response);
// "No Search Results" (40501) is a valid empty result for obscure
// businesses/keywords — DataForSEO still charges for it, so treat it as an
// empty success instead of surfacing a charged-task error to the user.
const task = assertOk(response, { treatNoResultsAsEmpty: true });
return {
data: combinedQuestionItems(task.result),
billing: buildTaskBilling(task),

View File

@ -60,6 +60,28 @@ describe("assertOk", () => {
}
});
it("appends the echoed request value to opaque 'Invalid Field' failures", () => {
const task = {
status_code: 40501,
status_message: "Invalid Field: 'target'.",
path: ["v3", "dataforseo_labs", "google", "domain_rank_overview", "live"],
cost: 0.02,
result_count: 0,
data: { target: "not a valid domain", language_code: "en" },
};
try {
assertOk({ status_code: 20000, tasks: [task] });
throw new Error("expected assertOk to throw");
} catch (error) {
expect(error).toBeInstanceOf(DataforseoChargedTaskError);
if (error instanceof DataforseoChargedTaskError) {
expect(error.message).toBe(
`Invalid Field: 'target'. (sent target="not a valid domain")`,
);
}
}
});
it("uses the classifier for non-charged (no-cost) failures", () => {
const classify = vi.fn(() => new AppError("BACKLINKS_NOT_ENABLED", "nope"));
const task = {

View File

@ -82,6 +82,26 @@ export function buildTaskBilling(
return billing;
}
/**
* DataForSEO echoes the posted request params back on `task.data`. Its
* validation rejections are opaque ("Invalid Field: 'target'.") and name the
* field but not the value we sent and these tasks are charged, so we want to
* know exactly what tripped them. Append the offending value so the charged
* failure is diagnosable from the captured message alone.
*/
function describeInvalidField(
message: string,
task: DataforseoTaskLike,
): string {
const match = message.match(/Invalid Field:\s*'([^']+)'/i);
if (!match) return message;
const field = match[1];
if (!isRecord(task.data)) return message;
const value = task.data[field];
if (value === undefined) return message;
return `${message} (sent ${field}=${JSON.stringify(value)})`;
}
/** DataForSEO's "No Search Results" (40501) — a successful empty result, not a failure. */
export function isNoResultsTask(task: DataforseoTaskLike): boolean {
return (
@ -139,10 +159,11 @@ export function assertOk<T extends DataforseoTaskLike>(
const classified = classify?.(task.status_code, message, path);
if (classified) throw classified;
const detailedMessage = describeInvalidField(message, task);
const billing = tryBuildTaskBilling(task);
if (billing) throw new DataforseoChargedTaskError(message, billing);
if (billing) throw new DataforseoChargedTaskError(detailedMessage, billing);
throw new AppError("INTERNAL_ERROR", message);
throw new AppError("INTERNAL_ERROR", detailedMessage);
}
return task;

View File

@ -1,6 +1,9 @@
import { z } from "zod";
import { AppError } from "@/server/lib/errors";
import { getKeywordDataProvider } from "@/shared/keyword-locations";
import {
getKeywordDataProvider,
isSupportedLanguageCode,
} from "@/shared/keyword-locations";
export const DEFAULT_LOCATION_CODE = 2840;
export const DEFAULT_LANGUAGE_CODE = "en";
@ -35,5 +38,8 @@ export function assertLabsLocationCode(locationCode: number | undefined) {
export const languageCodeSchema = z
.string()
.min(2)
.refine(isSupportedLanguageCode, {
message:
"Unsupported language code. Use a supported code such as 'en', 'es', 'de', or 'fr'.",
})
.describe("Language code (e.g. 'en', 'es', 'fr'). Defaults to 'en'.");

View File

@ -14,5 +14,9 @@ describe("shouldCaptureAppErrorCode", () => {
it("captures unexpected errors and unknown failures", () => {
expect(shouldCaptureAppErrorCode("INTERNAL_ERROR")).toBe(true);
expect(shouldCaptureAppErrorCode(undefined)).toBe(true);
// On cloud the shared DataForSEO account has these add-ons, so these firing
// signals a real platform problem — keep them reportable, don't suppress.
expect(shouldCaptureAppErrorCode("BACKLINKS_NOT_ENABLED")).toBe(true);
expect(shouldCaptureAppErrorCode("AI_SEARCH_NOT_ENABLED")).toBe(true);
});
});

View File

@ -5,6 +5,7 @@ import {
getKeywordDataProvider,
getLanguageCode,
isLabsLocationCode,
isSupportedLanguageCode,
isSupportedLocationCode,
} from "./keyword-locations";
@ -40,6 +41,21 @@ describe("keyword locations", () => {
);
});
it("accepts every supported language code and rejects unknown ones", () => {
// Every per-country default we send is, by construction, a supported code.
for (const option of LOCATION_OPTIONS) {
expect(isSupportedLanguageCode(option.languageCode)).toBe(true);
}
expect(isSupportedLanguageCode("en")).toBe(true);
expect(isSupportedLanguageCode("zh-TW")).toBe(true);
// Non-default codes from the master picker list are valid too (e.g. Hindi).
expect(isSupportedLanguageCode("hi")).toBe(true);
// Malformed/unsupported codes DataForSEO would reject as a charged failure.
expect(isSupportedLanguageCode("english")).toBe(false);
expect(isSupportedLanguageCode("en-US")).toBe(false);
expect(isSupportedLanguageCode("zh-tw")).toBe(false);
});
it("keeps the picker sorted alphabetically with unique codes", () => {
const labels = LOCATION_OPTIONS.map((option) => option.label);
expect(labels).toEqual(labels.toSorted((a, b) => a.localeCompare(b)));

View File

@ -658,10 +658,24 @@ const LOCATION_LANGUAGE: Record<number, string> = Object.fromEntries(
LOCATION_OPTIONS.map((option) => [option.code, option.languageCode]),
);
const SUPPORTED_LANGUAGE_CODES = new Set<string>(
LANGUAGE_OPTIONS.map((language) => language.code),
);
export function getLanguageCode(locationCode: number): string {
return LOCATION_LANGUAGE[locationCode] ?? "en";
}
/**
* Language codes DataForSEO accepts the master LANGUAGE_OPTIONS list. Callers
* (e.g. MCP tools) can pass an arbitrary `language_code`; an unsupported one is
* otherwise rejected by DataForSEO as an opaque *charged* "Invalid Field:
* 'language_code'." failure, so we validate against this set first (cost 0).
*/
export function isSupportedLanguageCode(languageCode: string): boolean {
return SUPPORTED_LANGUAGE_CODES.has(languageCode);
}
/**
* Countries where DataForSEO offers more than one language, from the Labs
* locations_and_languages endpoint (each country's default is included).