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:
Diego Rodrigues de Sa e Souza
2026-09-19 00:41:36 -03:00
committed by GitHub
parent ec4d1eff43
commit 3fd1265d89
73 changed files with 348 additions and 54 deletions

View File

@@ -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>

View File

@@ -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)}
/>
)}

View 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;
}