// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
//
// The evidence bundle for one ATTEMPT, fetched once and shared.
//
// It lives here rather than inside a component because two surfaces need the
// same bytes: the per-step "Page activity" block in the step expansion, and the
// run-level Evidence tab. Fetching in whichever mounted first would cost a
// second 256 KB round-trip for the other, and would let the two disagree.
//
// The raw `fetch` is deliberate. The bundle usually lives behind a presigned
// S3/MinIO URL; the `syntheticsService` axios wrapper attaches org auth headers,
// which a presigned URL rejects. That service therefore exposes URL builders
// only, and no body fetch exists to call.
//
// "Usually" is load-bearing. When the deployment stores artifacts on local disk
// — or when the batch presign call failed — the resolver hands back OUR proxy
// endpoint instead, and that one is cookie-authed. A cross-origin `fetch`
// omits cookies by default, so on any deployment where the API is on a
// different origin from the web app (every dev setup with `VITE_OPENOBSERVE_
// ENDPOINT` set) the proxy fetch arrived unauthenticated and the panel reported
// a bare "401 Unauthorized" that looked like the user's session had died.
import { computed, ref, watch, type Ref } from "vue";
import syntheticsService from "@/services/synthetics";
import {
indexEvidenceByStep,
parseEvidenceNdjson,
type EvidenceEvent,
} from "@/composables/synthetics/syntheticResultsSchema";
export type EvidenceStatus = "idle" | "loading" | "ready" | "error";
/**
* What went wrong, in terms the panel can write a sentence about.
*
* `unreachable` is the one that cannot carry an HTTP status: `fetch` rejects
* before a response exists (DNS, TLS, offline, a CORS preflight the object
* store refused), and reporting that as a server error sends the reader looking
* in the wrong place.
*/
export type EvidenceErrorKind = "unauthorized" | "expired" | "missing" | "unreachable" | "server";
/**
* HTTP status → what the reader should do about it.
*
* 401 and 403 are split because the fix differs and the two arrive from
* different halves of the system: 401 is our proxy saying the session is not
* valid, while 403 is overwhelmingly object storage rejecting a presigned
* signature that has aged out — which Retry genuinely fixes, because the run
* detail re-signs on reload.
*/
function classifyStatus(httpStatus: number): EvidenceErrorKind {
if (httpStatus === 401) return "unauthorized";
if (httpStatus === 403) return "expired";
if (httpStatus === 404 || httpStatus === 410) return "missing";
return "server";
}
/**
* What to tell the reader, per kind.
*
* Exported because two surfaces render this same failure — the per-step "Page
* activity" block and the run-level Evidence tab — and they must not describe
* one error two ways.
*/
export const EVIDENCE_ERROR_MESSAGE: Record = {
unauthorized: "synthetics.evidence.loadFailedUnauthorized",
expired: "synthetics.evidence.loadFailedExpired",
missing: "synthetics.evidence.loadFailedMissing",
unreachable: "synthetics.evidence.loadFailedUnreachable",
server: "synthetics.evidence.loadFailedServer",
};
/**
* Retry re-requests the SAME url, so it only helps where that could now
* succeed. A bundle that storage says is gone will not come back.
*/
export function evidenceErrorCanRetry(kind: EvidenceErrorKind | null): boolean {
return kind !== "missing";
}
/**
* An aged-out signature is re-minted by the run detail on load, and a dead
* session can only be fixed by signing in again. Neither is reachable from a
* Retry against the stale URL, so those two get a reload instead.
*/
export function evidenceErrorNeedsReload(kind: EvidenceErrorKind | null): boolean {
return kind === "expired" || kind === "unauthorized";
}
export function useSyntheticEvidence(
/** Object-storage key of the SELECTED attempt's bundle. Null when none exists. */
evidenceKey: Ref,
/** Resolves a key to a fetchable URL. Already presigned for every attempt. */
resolveUrl: (key: string) => string,
/** step_id -> definition, from the run's own snapshot. */
stepDefs: Ref