mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:30:57 +00:00
fix(discord): clarify deploy abort logs
This commit is contained in:
@@ -13,6 +13,10 @@ export type DiscordDeployErrorLike = {
|
||||
scope?: unknown;
|
||||
rawBody?: unknown;
|
||||
deployRequestBody?: unknown;
|
||||
deployRestMethod?: unknown;
|
||||
deployRestPath?: unknown;
|
||||
deployRequestMs?: unknown;
|
||||
deployTimeoutMs?: unknown;
|
||||
};
|
||||
|
||||
export type DiscordDeployRateLimitDetails = {
|
||||
@@ -32,6 +36,27 @@ export function attachDiscordDeployRequestBody(err: unknown, body: unknown) {
|
||||
}
|
||||
}
|
||||
|
||||
export function attachDiscordDeployRestContext(
|
||||
err: unknown,
|
||||
context: {
|
||||
method: string;
|
||||
path: string;
|
||||
requestMs: number;
|
||||
timeoutMs?: number;
|
||||
},
|
||||
) {
|
||||
if (!err || typeof err !== "object") {
|
||||
return;
|
||||
}
|
||||
const deployErr = err as DiscordDeployErrorLike;
|
||||
deployErr.deployRestMethod = context.method;
|
||||
deployErr.deployRestPath = context.path;
|
||||
deployErr.deployRequestMs = context.requestMs;
|
||||
if (typeof context.timeoutMs === "number" && Number.isFinite(context.timeoutMs)) {
|
||||
deployErr.deployTimeoutMs = context.timeoutMs;
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyDiscordDeployField(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return JSON.stringify(value);
|
||||
@@ -82,6 +107,78 @@ function readFiniteNumber(value: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatDurationMs(ms: number): string {
|
||||
return formatDurationSeconds(ms, { decimals: ms >= 1000 ? 1 : 0 });
|
||||
}
|
||||
|
||||
function isAbortLikeError(err: unknown): boolean {
|
||||
if (!err || typeof err !== "object") {
|
||||
return false;
|
||||
}
|
||||
const name = "name" in err && typeof err.name === "string" ? err.name : undefined;
|
||||
const message = formatErrorMessage(err);
|
||||
return (
|
||||
name === "AbortError" ||
|
||||
message === "This operation was aborted" ||
|
||||
message === "The operation was aborted" ||
|
||||
/\boperation was aborted\b/i.test(message)
|
||||
);
|
||||
}
|
||||
|
||||
function formatDiscordDeployRestOperation(err: DiscordDeployErrorLike): string {
|
||||
const method =
|
||||
typeof err.deployRestMethod === "string" && err.deployRestMethod.trim().length > 0
|
||||
? err.deployRestMethod.toUpperCase()
|
||||
: undefined;
|
||||
const path =
|
||||
typeof err.deployRestPath === "string" && err.deployRestPath.trim().length > 0
|
||||
? err.deployRestPath
|
||||
: undefined;
|
||||
if (method && path) {
|
||||
return `${method} ${path}`;
|
||||
}
|
||||
if (method) {
|
||||
return method;
|
||||
}
|
||||
if (path) {
|
||||
return path;
|
||||
}
|
||||
return "request";
|
||||
}
|
||||
|
||||
export function formatDiscordDeployErrorMessage(err: unknown): string {
|
||||
if (!isAbortLikeError(err)) {
|
||||
return formatErrorMessage(err);
|
||||
}
|
||||
const deployErr =
|
||||
err && typeof err === "object"
|
||||
? (err as DiscordDeployErrorLike)
|
||||
: ({} as DiscordDeployErrorLike);
|
||||
const requestMs = readFiniteNumber(deployErr.deployRequestMs);
|
||||
const timeoutMs = readFiniteNumber(deployErr.deployTimeoutMs);
|
||||
const operation = formatDiscordDeployRestOperation(deployErr);
|
||||
const hasRestContext =
|
||||
requestMs !== undefined ||
|
||||
timeoutMs !== undefined ||
|
||||
deployErr.deployRestMethod !== undefined ||
|
||||
deployErr.deployRestPath !== undefined;
|
||||
if (!hasRestContext) {
|
||||
return "Discord REST request was aborted";
|
||||
}
|
||||
const timing: string[] = [];
|
||||
if (timeoutMs !== undefined) {
|
||||
timing.push(`timeout=${formatDurationMs(timeoutMs)}`);
|
||||
}
|
||||
if (requestMs !== undefined) {
|
||||
timing.push(`observed=${formatDurationMs(requestMs)}`);
|
||||
}
|
||||
const timingText = timing.length > 0 ? ` (${timing.join(", ")})` : "";
|
||||
if (timeoutMs !== undefined && requestMs !== undefined && requestMs >= timeoutMs) {
|
||||
return `Discord REST ${operation} timed out${timingText}`;
|
||||
}
|
||||
return `Discord REST ${operation} was aborted${timingText}`;
|
||||
}
|
||||
|
||||
export function resolveDiscordDeployRateLimitDetails(
|
||||
err: unknown,
|
||||
): DiscordDeployRateLimitDetails | undefined {
|
||||
|
||||
@@ -2,8 +2,10 @@ import { formatDurationSeconds, warn, type RuntimeEnv } from "openclaw/plugin-sd
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
import { Client, overwriteApplicationCommands, type RequestClient } from "../internal/discord.js";
|
||||
import {
|
||||
attachDiscordDeployRestContext,
|
||||
attachDiscordDeployRequestBody,
|
||||
formatDiscordDeployErrorDetails,
|
||||
formatDiscordDeployErrorMessage,
|
||||
formatDiscordDeployRateLimitDetails,
|
||||
formatDiscordDeployRateLimitWarning,
|
||||
isDiscordDeployDailyCreateLimit,
|
||||
@@ -27,6 +29,7 @@ function wrapDeployRestMethod(params: {
|
||||
runtime: RuntimeEnv;
|
||||
accountId: string;
|
||||
startupStartedAt: number;
|
||||
timeoutMs?: number;
|
||||
shouldLogVerbose: () => boolean;
|
||||
}) {
|
||||
return async (path: string, data?: never, query?: never) => {
|
||||
@@ -51,20 +54,27 @@ function wrapDeployRestMethod(params: {
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
const requestMs = Date.now() - startedAt;
|
||||
attachDiscordDeployRequestBody(err, body);
|
||||
attachDiscordDeployRestContext(err, {
|
||||
method: params.method,
|
||||
path,
|
||||
requestMs,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
const rateLimitDetails = formatDiscordDeployRateLimitDetails(err);
|
||||
if (rateLimitDetails) {
|
||||
if (params.shouldLogVerbose()) {
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt}${rateLimitDetails}`,
|
||||
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:rate-limited ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs}${rateLimitDetails}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const details = formatDiscordDeployErrorDetails(err);
|
||||
params.runtime.error?.(
|
||||
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${Date.now() - startedAt} error=${formatErrorMessage(err)}${details}`,
|
||||
`discord startup [${params.accountId}] native-slash-command-deploy-rest:${params.method}:error ${Math.max(0, Date.now() - params.startupStartedAt)}ms path=${path} requestMs=${requestMs} error=${formatDiscordDeployErrorMessage(err)}${details}`,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
@@ -87,12 +97,14 @@ function installDeployRestLogging(params: {
|
||||
delete: params.rest.delete.bind(params.rest),
|
||||
};
|
||||
for (const method of Object.keys(original) as RestMethodName[]) {
|
||||
const timeout = (params.rest as { options?: { timeout?: unknown } }).options?.timeout;
|
||||
params.rest[method] = wrapDeployRestMethod({
|
||||
method,
|
||||
original,
|
||||
runtime: params.runtime,
|
||||
accountId: params.accountId,
|
||||
startupStartedAt: params.startupStartedAt,
|
||||
timeoutMs: typeof timeout === "number" ? timeout : undefined,
|
||||
shouldLogVerbose: params.shouldLogVerbose,
|
||||
}) as RequestClient[typeof method];
|
||||
}
|
||||
@@ -168,7 +180,7 @@ export async function deployDiscordCommands(params: {
|
||||
params.runtime.log?.(
|
||||
warn(
|
||||
rateLimitWarning ??
|
||||
`discord: native slash command deploy warning (not message send): ${formatErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
|
||||
`discord: native slash command deploy warning (not message send): ${formatDiscordDeployErrorMessage(err)}${formatDiscordDeployErrorDetails(err)}`,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -792,11 +792,25 @@ describe("monitorDiscordProvider", () => {
|
||||
.mock.calls.some(
|
||||
(call) =>
|
||||
String(call[0]).includes("native slash command deploy warning (not message send):") &&
|
||||
String(call[0]).includes("This operation was aborted"),
|
||||
String(call[0]).includes("Discord REST request was aborted"),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("formats native command deploy aborts with REST timeout context", () => {
|
||||
const error = Object.assign(new Error("This operation was aborted"), {
|
||||
name: "AbortError",
|
||||
deployRestMethod: "patch",
|
||||
deployRestPath: "/applications/app-1/commands/cmd-1",
|
||||
deployRequestMs: 24_657,
|
||||
deployTimeoutMs: 15_000,
|
||||
});
|
||||
|
||||
expect(providerTesting.formatDiscordDeployErrorMessage(error)).toBe(
|
||||
"Discord REST PATCH /applications/app-1/commands/cmd-1 timed out (timeout=15s, observed=24.7s)",
|
||||
);
|
||||
});
|
||||
|
||||
it("logs repeated native command deploy rate limits as one concise warning", async () => {
|
||||
const runtime = baseRuntime();
|
||||
const rateLimitError = createRateLimitError(
|
||||
|
||||
@@ -47,7 +47,10 @@ import {
|
||||
type GetPluginCommandSpecs,
|
||||
} from "./provider.commands.js";
|
||||
import { logDiscordResolvedConfig } from "./provider.config-log.js";
|
||||
import { formatDiscordDeployErrorDetails } from "./provider.deploy-errors.js";
|
||||
import {
|
||||
formatDiscordDeployErrorDetails,
|
||||
formatDiscordDeployErrorMessage,
|
||||
} from "./provider.deploy-errors.js";
|
||||
import {
|
||||
clearDiscordNativeCommands,
|
||||
runDiscordCommandDeployInBackground,
|
||||
@@ -644,6 +647,7 @@ export const __testing = {
|
||||
resolveDiscordRestFetch,
|
||||
resolveThreadBindingsEnabled: resolveThreadBindingsEnabledForTesting,
|
||||
formatDiscordDeployErrorDetails,
|
||||
formatDiscordDeployErrorMessage,
|
||||
setFetchDiscordApplicationId(mock?: typeof fetchDiscordApplicationId) {
|
||||
fetchDiscordApplicationIdForTesting = mock;
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user