mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-21 22:32:22 +03:00
fix(dashboard): explicit notice for size-limit-omitted log payloads (#14045)
Merged via /merge-batch (2026-09-19) on top of the current `release/v3.8.51` tip. **Reconciled before landing:** `requestLogger.detail.payloadSizeLimitOmitted` was stamped `__MISSING__:` in 65 locales and the new-key gate rejects markers since 2026-09-17. The LAN translation backends were unavailable (codex accounts at quota for ~136h, claude connection expired, .113 instance in resource_pressure), so the single string was translated by hand in `818a1151` — `CALL_LOG_PIPELINE_MAX_SIZE_KB` kept verbatim in every locale, one line changed per catalog. **Evidence on the merged tree:** `check-new-key-coverage` PASS (every new key reached all 65 locales); `payload-section-size-limit-notice.test.tsx` 7/7; `typecheck:core` clean; file-size, changelog-integrity, complexity and cognitive-complexity gates OK; prettier clean on all 65 catalogs. **Inherited, not from this PR** (identical on the pure tip): `check-env-doc-sync` misses `BRIDGE_PORT`/`CERT_DIR`/`OPENWA_SERVICE_PORT`/`ROUTER_URL` in `.env.example`, and `check-key-completeness` reports 7 keys (`providers.claude*`, `sidebar.pin*`) present in `en.json` but absent from every locale. Closes #13894
This commit is contained in:
committed by
GitHub
parent
ec4d1eff43
commit
3fd1265d89
@@ -13,6 +13,47 @@ import {
|
||||
} from "@/shared/hooks/useTimestampTitles";
|
||||
import { JsonTreeExpandControls } from "@/shared/components/JsonTreeExpandControls";
|
||||
import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
import {
|
||||
isPipelineSizeLimitMarker,
|
||||
isSizeLimitOmissionMarker,
|
||||
} from "@/shared/constants/callLogSizeLimitMarkers";
|
||||
|
||||
// ─── Size-limit omission detection (#13894) ─────────────────────────────────
|
||||
// A size-limited call-log artifact does not simply drop a payload -- it writes
|
||||
// an explicit marker in its place (see callLogArtifacts.ts's
|
||||
// omitOversizedPipeline()/buildMinimalArtifactForSizeLimit()). Before this fix
|
||||
// the detail view fed that marker straight into the generic JSON/`<pre>`
|
||||
// renderer, so a size-limit omission was indistinguishable from a real
|
||||
// upstream error or a genuinely empty payload -- a silent fallback. These
|
||||
// helpers turn the marker into an explicit, labeled notice instead.
|
||||
|
||||
/** Builds the pipeline payload sections, replacing the `error` marker object
|
||||
* left by a size-limited pipeline capture with an explicit notice entry
|
||||
* instead of letting it render as if it were a real pipeline error. */
|
||||
export function buildPipelinePayloadSections(entries, pipelinePayloads) {
|
||||
return entries
|
||||
.map(([key, title]) => {
|
||||
const value = pipelinePayloads?.[key];
|
||||
if (key === "error" && isPipelineSizeLimitMarker(value)) {
|
||||
return { key, title, json: null, notice: true };
|
||||
}
|
||||
if (value === null || value === undefined) return { key, title, json: null, notice: false };
|
||||
let json;
|
||||
try {
|
||||
json = JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
json = String(value);
|
||||
}
|
||||
return { key, title, json, notice: false };
|
||||
})
|
||||
.filter((section) => section.json || section.notice);
|
||||
}
|
||||
|
||||
/** True when a top-level requestBody/responseBody was replaced by the
|
||||
* size-limit omission placeholder string rather than genuinely absent. */
|
||||
export function isBodySizeLimitOmission(value) {
|
||||
return isSizeLimitOmissionMarker(value);
|
||||
}
|
||||
|
||||
// ─── Payload Code Block ─────────────────────────────────────────────────────
|
||||
// Renders parsed payloads as a collapsible JSON tree (react18-json-view) so
|
||||
@@ -21,11 +62,15 @@ import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
// the plain <pre> dump for anything that isn't valid JSON (e.g. a captured
|
||||
// error string), since json is display text sourced from JSON.stringify with
|
||||
// a String() fallback on failure -- it is not guaranteed parseable.
|
||||
// `notice`, when true, takes over rendering entirely: it means `json` is not a
|
||||
// real payload but a size-limit omission marker (#13894) that must be shown as
|
||||
// an explicit, labeled notice rather than a generic JSON/error dump.
|
||||
|
||||
export function PayloadSection({
|
||||
title,
|
||||
sectionId,
|
||||
json,
|
||||
notice = false,
|
||||
onCopy,
|
||||
collapsible = true,
|
||||
defaultOpen = true,
|
||||
@@ -78,20 +123,28 @@ export function PayloadSection({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
{!notice && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-text-muted hover:text-text-primary transition-colors"
|
||||
aria-label={t("copyTitle", { title })}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px]">
|
||||
{copied ? "check" : "content_copy"}
|
||||
</span>
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</button>
|
||||
)}
|
||||
{parsedJson !== null && <JsonTreeExpandControls sectionId={resolvedSectionId} />}
|
||||
</div>
|
||||
</div>
|
||||
{open && parsedJson !== null && (
|
||||
{open && notice && (
|
||||
<div className="p-4 rounded-xl border border-amber-500/40 bg-amber-500/10 text-xs text-amber-700 dark:text-amber-300 flex items-start gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] shrink-0">warning</span>
|
||||
<span>{t("payloadSizeLimitOmitted")}</span>
|
||||
</div>
|
||||
)}
|
||||
{open && !notice && parsedJson !== null && (
|
||||
<div
|
||||
ref={treeContainerRef}
|
||||
className="rounded-xl bg-black/5 dark:bg-black/30 border border-border max-h-150 overflow-auto p-4 text-xs font-mono"
|
||||
@@ -105,7 +158,7 @@ export function PayloadSection({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{open && parsedJson === null && (
|
||||
{open && !notice && parsedJson === null && (
|
||||
<pre className="p-4 rounded-xl bg-black/5 dark:bg-black/30 border border-border overflow-x-auto text-xs font-mono text-text-main max-h-150 overflow-y-auto leading-relaxed whitespace-pre-wrap break-words">
|
||||
{json}
|
||||
</pre>
|
||||
|
||||
@@ -20,6 +20,8 @@ import { useJsonTreeExpandLevel } from "@/store/jsonTreeExpandStore";
|
||||
import {
|
||||
PayloadSection,
|
||||
ConversationContextSection,
|
||||
buildPipelinePayloadSections,
|
||||
isBodySizeLimitOmission,
|
||||
} from "@/shared/components/RequestLoggerDetail.sections";
|
||||
|
||||
// ─── Copy-all composition ────────────────────────────────────────────────────
|
||||
@@ -470,22 +472,21 @@ export default function RequestLoggerDetail({
|
||||
|
||||
const pipelinePayloads = detail?.pipelinePayloads || null;
|
||||
const payloadSections = pipelinePayloads
|
||||
? [
|
||||
["clientRawRequest", t("payload.clientRawRequest")],
|
||||
["clientRequest", t("payload.clientRequest")],
|
||||
["openaiRequest", t("payload.openaiRequest")],
|
||||
["providerRequest", t("payload.providerRequest")],
|
||||
["providerResponse", t("payload.providerResponse")],
|
||||
["clientResponse", t("payload.clientResponse")],
|
||||
["error", t("payload.pipelineError")],
|
||||
]
|
||||
.map(([key, title]) => ({
|
||||
key,
|
||||
title,
|
||||
json: toPrettyJson(pipelinePayloads[key]),
|
||||
}))
|
||||
.filter((section) => section.json)
|
||||
? buildPipelinePayloadSections(
|
||||
[
|
||||
["clientRawRequest", t("payload.clientRawRequest")],
|
||||
["clientRequest", t("payload.clientRequest")],
|
||||
["openaiRequest", t("payload.openaiRequest")],
|
||||
["providerRequest", t("payload.providerRequest")],
|
||||
["providerResponse", t("payload.providerResponse")],
|
||||
["clientResponse", t("payload.clientResponse")],
|
||||
["error", t("payload.pipelineError")],
|
||||
],
|
||||
pipelinePayloads
|
||||
)
|
||||
: [];
|
||||
const requestBodyOmitted = isBodySizeLimitOmission(detail?.requestBody);
|
||||
const responseBodyOmitted = isBodySizeLimitOmission(detail?.responseBody);
|
||||
const requestJson = detail?.requestBody ? toPrettyJson(detail.requestBody) : null;
|
||||
const responseJson = detail?.responseBody ? toPrettyJson(detail.responseBody) : null;
|
||||
const streamChunks = (() => {
|
||||
@@ -1155,6 +1156,7 @@ export default function RequestLoggerDetail({
|
||||
title={section.title}
|
||||
sectionId={section.key}
|
||||
json={section.json}
|
||||
notice={section.notice}
|
||||
onCopy={() => onCopy(section.json)}
|
||||
/>
|
||||
))}
|
||||
@@ -1164,6 +1166,7 @@ export default function RequestLoggerDetail({
|
||||
title={t("responsePayloadLegacy")}
|
||||
sectionId="responsePayloadLegacy"
|
||||
json={responseJson}
|
||||
notice={responseBodyOmitted}
|
||||
onCopy={() => onCopy(responseJson)}
|
||||
/>
|
||||
)}
|
||||
@@ -1173,6 +1176,7 @@ export default function RequestLoggerDetail({
|
||||
title={t("requestPayloadLegacy")}
|
||||
sectionId="requestPayloadLegacy"
|
||||
json={requestJson}
|
||||
notice={requestBodyOmitted}
|
||||
onCopy={() => onCopy(requestJson)}
|
||||
/>
|
||||
)}
|
||||
|
||||
40
src/shared/constants/callLogSizeLimitMarkers.ts
Normal file
40
src/shared/constants/callLogSizeLimitMarkers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
// Sentinel markers written by src/lib/usage/callLogArtifacts.ts when a call-log
|
||||
// artifact's request/response body or pipeline payload had to be dropped because
|
||||
// it exceeded the configured size cap (CALL_LOG_PIPELINE_MAX_SIZE_KB /
|
||||
// MAX_CALL_LOG_ARTIFACT_BYTES). Kept here — not inside callLogArtifacts.ts, which
|
||||
// pulls in `fs`/`path` and cannot be imported by a client component — so the
|
||||
// artifact writer and the request-log detail view (RequestLoggerDetail.tsx) share
|
||||
// one definition of "this is a size-limit omission" instead of each guessing at
|
||||
// the shape independently (see issue #13894: the previous frontend rendered the
|
||||
// pipeline marker verbatim as if it were a real upstream error).
|
||||
|
||||
export const CALL_LOG_SIZE_LIMIT_REASON = "call_log_artifact_size_limit_exceeded";
|
||||
|
||||
export const CALL_LOG_BODY_OMITTED_FOR_SIZE_LIMIT =
|
||||
"[omitted: call log artifact size limit exceeded]";
|
||||
|
||||
export const CALL_LOG_STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT =
|
||||
"[stream chunks omitted: call log artifact size limit exceeded]";
|
||||
|
||||
/**
|
||||
* True for a placeholder a size-limit fallback wrote in place of a real
|
||||
* requestBody/responseBody/stream-chunk payload.
|
||||
*/
|
||||
export function isSizeLimitOmissionMarker(value: unknown): boolean {
|
||||
return (
|
||||
value === CALL_LOG_BODY_OMITTED_FOR_SIZE_LIMIT ||
|
||||
value === CALL_LOG_STREAM_CHUNKS_OMITTED_FOR_SIZE_LIMIT
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* True for the `pipeline.error` marker object omitOversizedPipeline() writes in
|
||||
* place of the real pipeline payload once it exceeds CALL_LOG_PIPELINE_MAX_SIZE_KB.
|
||||
* Checked by shape (not just truthiness) so a real upstream error that happens to
|
||||
* be named `error` is never mistaken for the size-limit marker.
|
||||
*/
|
||||
export function isPipelineSizeLimitMarker(pipelineError: unknown): boolean {
|
||||
if (!pipelineError || typeof pipelineError !== "object") return false;
|
||||
const candidate = pipelineError as { _omniroute_truncated?: unknown; reason?: unknown };
|
||||
return candidate._omniroute_truncated === true && candidate.reason === CALL_LOG_SIZE_LIMIT_REASON;
|
||||
}
|
||||
Reference in New Issue
Block a user