204 lines
7.4 KiB
TypeScript
204 lines
7.4 KiB
TypeScript
import { prettyPrintPacket, RunAnnotations } from "@trigger.dev/core/v3";
|
|
import { getMaxDuration } from "@trigger.dev/core/v3/isomorphic";
|
|
import {
|
|
extractIdempotencyKeyScope,
|
|
getUserProvidedIdempotencyKey,
|
|
} from "@trigger.dev/core/v3/serverOnly";
|
|
import { MachinePresetName } from "@trigger.dev/core/v3/schemas";
|
|
import type { SpanRun } from "~/presenters/v3/SpanPresenter.server";
|
|
import type { SyntheticRun } from "./readFallback.server";
|
|
|
|
// `SyntheticRun.machinePreset` is sourced from the snapshot payload as
|
|
// a plain string, but `SpanRun.machinePreset` is the narrowed enum.
|
|
// Validate against the canonical enum so an unknown / stale preset
|
|
// string collapses to undefined rather than fighting the type checker.
|
|
function narrowMachinePreset(value: string | undefined): SpanRun["machinePreset"] {
|
|
if (value === undefined) return undefined;
|
|
const parsed = MachinePresetName.safeParse(value);
|
|
return parsed.success ? parsed.data : undefined;
|
|
}
|
|
|
|
// Synthesise a SpanRun-shaped object from a buffered run so the run-detail
|
|
// page's right-side details panel renders identically to a PG-resident
|
|
// run. The shape matches `SpanPresenter.getRun`'s return value;
|
|
// buffered-irrelevant fields (output, attempts, schedule, session,
|
|
// region, batch) are filled with sensible defaults, while terminal state
|
|
// (CANCELED / FAILED) is reflected into `status`, `isFinished`, `isError`
|
|
// and `error` so a finished buffered run does not render as PENDING.
|
|
//
|
|
// Pretty-printing for payload and metadata mirrors SpanPresenter so the
|
|
// UI receives data in the same shape. Buffered runs cannot use the
|
|
// `application/store` packet path (no R2 object yet) so we treat raw
|
|
// snapshot fields as inline packets.
|
|
export async function buildSyntheticSpanRun(args: {
|
|
run: SyntheticRun;
|
|
environment: {
|
|
id: string;
|
|
slug: string;
|
|
type: "PRODUCTION" | "DEVELOPMENT" | "STAGING" | "PREVIEW";
|
|
};
|
|
}): Promise<SpanRun> {
|
|
const { run, environment } = args;
|
|
|
|
const payload =
|
|
typeof run.payload !== "undefined" && run.payload !== null
|
|
? await prettyPrintPacket(run.payload, run.payloadType ?? undefined)
|
|
: undefined;
|
|
|
|
// Nullish check, not truthy — matches the payload branch above so an
|
|
// intentionally-empty packet (e.g. metadata: "") still gets handed to
|
|
// `prettyPrintPacket` and renders consistently. A truthy check would
|
|
// drop the empty-string case and the two paths would diverge.
|
|
const metadata =
|
|
typeof run.metadata !== "undefined" && run.metadata !== null
|
|
? await prettyPrintPacket(run.metadata, run.metadataType, {
|
|
filteredKeys: ["$$streams", "$$streamsVersion", "$$streamsBaseUrl"],
|
|
})
|
|
: undefined;
|
|
|
|
const idempotencyShape = {
|
|
idempotencyKey: run.idempotencyKey ?? null,
|
|
idempotencyKeyExpiresAt: null,
|
|
idempotencyKeyOptions: run.idempotencyKeyOptions ?? null,
|
|
};
|
|
|
|
const idempotencyKey = getUserProvidedIdempotencyKey(idempotencyShape);
|
|
const idempotencyKeyScope = extractIdempotencyKeyScope(idempotencyShape);
|
|
const idempotencyKeyStatus: SpanRun["idempotencyKeyStatus"] = idempotencyKey
|
|
? "active"
|
|
: idempotencyKeyScope
|
|
? "inactive"
|
|
: undefined;
|
|
|
|
const taskKind = RunAnnotations.safeParse(run.annotations).data?.taskKind;
|
|
const isAgentRun = taskKind === "AGENT";
|
|
const isScheduled = taskKind === "SCHEDULED";
|
|
|
|
const queueName = run.queue ?? "task/";
|
|
const isCancelled = run.status === "CANCELED";
|
|
const isFailed = run.status === "FAILED";
|
|
|
|
// The run-detail panel derives terminal/error state from `status`,
|
|
// `isFinished` and `isError` (SpanPresenter.getRun -> isFinalRunStatus /
|
|
// isFailedRunStatus). Buffered FAILED runs surface as SYSTEM_FAILURE to
|
|
// match ApiRetrieveRunPresenter.bufferedStatusToTaskRunStatus; both
|
|
// CANCELED and SYSTEM_FAILURE are final run statuses, and SYSTEM_FAILURE
|
|
// is also a failed status.
|
|
const status: SpanRun["status"] = isCancelled
|
|
? "CANCELED"
|
|
: isFailed
|
|
? "SYSTEM_FAILURE"
|
|
: "PENDING";
|
|
|
|
// Mirror ApiRetrieveRunPresenter's STRING_ERROR synthesis so the panel
|
|
// shows why a buffered run failed instead of an empty error block.
|
|
const error: SpanRun["error"] =
|
|
isFailed && run.error
|
|
? { type: "STRING_ERROR", raw: `${run.error.code}: ${run.error.message}` }
|
|
: undefined;
|
|
|
|
return {
|
|
id: run.id,
|
|
friendlyId: run.friendlyId,
|
|
status,
|
|
statusReason: isCancelled
|
|
? (run.cancelReason ?? undefined)
|
|
: isFailed
|
|
? (run.error?.message ?? undefined)
|
|
: undefined,
|
|
createdAt: run.createdAt,
|
|
startedAt: null,
|
|
executedAt: null,
|
|
updatedAt: run.cancelledAt ?? run.createdAt,
|
|
delayUntil: run.delayUntil ?? null,
|
|
expiredAt: null,
|
|
// Symmetric with `ApiRetrieveRunPresenter` — FAILED buffered runs
|
|
// must surface a non-null `completedAt` so the run-detail panel
|
|
// (and any caller checking `isFinished && completedAt`) doesn't
|
|
// render a finished run with no completion timestamp. PG-resident
|
|
// SYSTEM_FAILURE rows always have completedAt set; the buffer
|
|
// entry has no separate failedAt, so we fall back to createdAt
|
|
// as the best proxy for when the terminal state landed.
|
|
completedAt: run.cancelledAt ?? (isFailed ? run.createdAt : null),
|
|
logsDeletedAt: null,
|
|
ttl: run.ttl ?? null,
|
|
taskIdentifier: run.taskIdentifier ?? "",
|
|
version: undefined,
|
|
sdkVersion: undefined,
|
|
runtime: undefined,
|
|
runtimeVersion: undefined,
|
|
isTest: run.isTest,
|
|
replayedFromTaskRunFriendlyId: run.replayedFromTaskRunFriendlyId ?? null,
|
|
environmentId: environment.id,
|
|
idempotencyKey,
|
|
idempotencyKeyExpiresAt: null,
|
|
idempotencyKeyScope,
|
|
idempotencyKeyStatus,
|
|
debounce: null,
|
|
schedule: undefined,
|
|
queue: {
|
|
name: queueName,
|
|
isCustomQueue: !queueName.startsWith("task/"),
|
|
concurrencyKey: run.concurrencyKey ?? null,
|
|
},
|
|
tags: run.runTags,
|
|
baseCostInCents: 0,
|
|
costInCents: 0,
|
|
totalCostInCents: 0,
|
|
usageDurationMs: 0,
|
|
isFinished: isCancelled || isFailed,
|
|
isRunning: false,
|
|
isError: isFailed,
|
|
isAgentRun,
|
|
isScheduled,
|
|
payload,
|
|
payloadType: run.payloadType ?? "application/json",
|
|
output: undefined,
|
|
outputType: "application/json",
|
|
error,
|
|
// The snapshot only carries the root/parent friendly IDs, not the
|
|
// spanId or taskIdentifier that SpanPresenter sources from the joined
|
|
// PG rows. Emitting them with empty-string stubs renders a blank task
|
|
// name and a misleading `?span=` jump target, so we omit the
|
|
// relationships until the drainer materialises the row (a transient
|
|
// window). Top-level buffered runs have no relationships regardless.
|
|
relationships: {
|
|
root: undefined,
|
|
parent: undefined,
|
|
},
|
|
context: JSON.stringify(
|
|
{
|
|
task: {
|
|
id: run.taskIdentifier ?? "",
|
|
},
|
|
run: {
|
|
id: run.friendlyId,
|
|
createdAt: run.createdAt,
|
|
isTest: run.isTest,
|
|
},
|
|
environment: {
|
|
id: environment.id,
|
|
slug: environment.slug,
|
|
type: environment.type,
|
|
},
|
|
},
|
|
null,
|
|
2
|
|
),
|
|
metadata,
|
|
maxDurationInSeconds: getMaxDuration(run.maxDurationInSeconds),
|
|
batch: undefined,
|
|
session: undefined,
|
|
engine: "V2",
|
|
region: null,
|
|
workerQueue: run.workerQueue ?? "",
|
|
traceId: run.traceId ?? "",
|
|
spanId: run.spanId ?? "",
|
|
isCached: false,
|
|
isBuffered: true,
|
|
machinePreset: narrowMachinePreset(run.machinePreset),
|
|
taskEventStore: "taskEvent",
|
|
externalTraceId: undefined,
|
|
};
|
|
}
|