fix: capture DataForSEO errors with context (#138)

This commit is contained in:
Ben Senescu 2026-04-24 00:16:20 -04:00 committed by GitHub
parent 94d1ee7bd5
commit 17298da7ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 41 additions and 10 deletions

View File

@ -116,6 +116,14 @@ export function RankTrackingConfigModal({
}
};
const handleDomainBlur = () => {
try {
setDomain(normalizeDomain(domain));
} catch {
// Keep invalid partial input editable; submit validation will show the error.
}
};
const isPending = createMutation.isPending || updateMutation.isPending;
if (step === "keywords" && createdConfigId) {
@ -156,7 +164,7 @@ export function RankTrackingConfigModal({
className="input input-bordered w-full"
value={domain}
onChange={(e) => setDomain(e.target.value)}
onBlur={() => setDomain(normalizeDomain(domain))}
onBlur={handleDomainBlur}
/>
</div>

View File

@ -29,6 +29,7 @@ export const errorHandlingMiddleware = createMiddleware({
errorCode: appError?.code ?? "INTERNAL_ERROR",
method: request.method,
path: url.pathname,
...appError?.details,
}),
);
}

View File

@ -40,7 +40,7 @@ export type {
// ---------------------------------------------------------------------------
function createAuthenticatedFetch() {
return (url: RequestInfo, init?: RequestInit): Promise<Response> => {
return async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
const headers = new Headers(init?.headers);
headers.set("Authorization", `Basic ${env.DATAFORSEO_API_KEY}`);
@ -48,7 +48,26 @@ function createAuthenticatedFetch() {
...init,
headers,
};
return fetch(url, newInit);
const response = await fetch(url, newInit);
if (!response.ok) {
const rawText = await response.text();
const path = formatDataforseoRequestPath(url);
const err = new AppError(
response.status === 429 ? "RATE_LIMITED" : "INTERNAL_ERROR",
`DataForSEO HTTP ${response.status} on ${path}`,
{
provider: "dataforseo",
providerStatus: String(response.status),
providerPath: path,
responseBody: formatDataforseoErrorPayload(rawText),
},
);
err.name = "DataForSEOHttpError";
throw err;
}
return response;
};
}
@ -59,6 +78,15 @@ function getLabsApi() {
return new DataforseoLabsApi(API_BASE, { fetch: createAuthenticatedFetch() });
}
function formatDataforseoRequestPath(url: RequestInfo): string {
const rawUrl = typeof url === "string" ? url : url.url;
try {
return new URL(rawUrl).pathname;
} catch {
return rawUrl;
}
}
async function postDataforseo(
path: string,
payload: unknown,
@ -74,13 +102,6 @@ async function postDataforseo(
const rawText = await response.text();
if (!response.ok) {
throw new AppError(
"INTERNAL_ERROR",
`DataForSEO HTTP ${response.status} on ${path}. Response: ${formatDataforseoErrorPayload(rawText)}`,
);
}
try {
return JSON.parse(rawText);
} catch {

View File

@ -4,6 +4,7 @@ export class AppError extends Error {
constructor(
public readonly code: ErrorCode,
message?: string,
public readonly details?: Record<string, string>,
) {
super(message ?? code);
this.name = "AppError";