fix: capture DataForSEO errors with context (#138)
This commit is contained in:
parent
94d1ee7bd5
commit
17298da7ee
@ -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;
|
const isPending = createMutation.isPending || updateMutation.isPending;
|
||||||
|
|
||||||
if (step === "keywords" && createdConfigId) {
|
if (step === "keywords" && createdConfigId) {
|
||||||
@ -156,7 +164,7 @@ export function RankTrackingConfigModal({
|
|||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
value={domain}
|
value={domain}
|
||||||
onChange={(e) => setDomain(e.target.value)}
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
onBlur={() => setDomain(normalizeDomain(domain))}
|
onBlur={handleDomainBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export const errorHandlingMiddleware = createMiddleware({
|
|||||||
errorCode: appError?.code ?? "INTERNAL_ERROR",
|
errorCode: appError?.code ?? "INTERNAL_ERROR",
|
||||||
method: request.method,
|
method: request.method,
|
||||||
path: url.pathname,
|
path: url.pathname,
|
||||||
|
...appError?.details,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export type {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function createAuthenticatedFetch() {
|
function createAuthenticatedFetch() {
|
||||||
return (url: RequestInfo, init?: RequestInit): Promise<Response> => {
|
return async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
|
||||||
const headers = new Headers(init?.headers);
|
const headers = new Headers(init?.headers);
|
||||||
headers.set("Authorization", `Basic ${env.DATAFORSEO_API_KEY}`);
|
headers.set("Authorization", `Basic ${env.DATAFORSEO_API_KEY}`);
|
||||||
|
|
||||||
@ -48,7 +48,26 @@ function createAuthenticatedFetch() {
|
|||||||
...init,
|
...init,
|
||||||
headers,
|
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() });
|
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(
|
async function postDataforseo(
|
||||||
path: string,
|
path: string,
|
||||||
payload: unknown,
|
payload: unknown,
|
||||||
@ -74,13 +102,6 @@ async function postDataforseo(
|
|||||||
|
|
||||||
const rawText = await response.text();
|
const rawText = await response.text();
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new AppError(
|
|
||||||
"INTERNAL_ERROR",
|
|
||||||
`DataForSEO HTTP ${response.status} on ${path}. Response: ${formatDataforseoErrorPayload(rawText)}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return JSON.parse(rawText);
|
return JSON.parse(rawText);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export class AppError extends Error {
|
|||||||
constructor(
|
constructor(
|
||||||
public readonly code: ErrorCode,
|
public readonly code: ErrorCode,
|
||||||
message?: string,
|
message?: string,
|
||||||
|
public readonly details?: Record<string, string>,
|
||||||
) {
|
) {
|
||||||
super(message ?? code);
|
super(message ?? code);
|
||||||
this.name = "AppError";
|
this.name = "AppError";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user