feat(webapp): mollifier API GET read-fallback — synthetic primitives + route wiring#3755
feat(webapp): mollifier API GET read-fallback — synthetic primitives + route wiring#3755d-cs wants to merge 3 commits into
Conversation
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
31f4726 to
b05929b
Compare
1838229 to
af0aeeb
Compare
b05929b to
b89da52
Compare
0919f7a to
f36c576
Compare
74fdf6d to
c6fa61f
Compare
c8ab214 to
047b240
Compare
…+ route wiring Synthesise QUEUED/FAILED responses from the mollifier buffer when a TaskRun row hasn't landed in Postgres yet. Wires the synthesis into: - ApiRetrieveRunPresenter - v1 trace GET route - v1 spans GET route - attempts route gains a GET loader (fixes pre-existing Remix 400) Stacked on the trigger-time decisions PR. The readFallback infra itself lives on the trigger PR (consumed by IdempotencyKeyConcern); this PR adds the route-level synthetic-rendering primitives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the ad-hoc \`as Record<string, unknown>\` + \`typeof === "string"\` checks in \`findBufferedRunRedirectInfo\` with a Zod \`safeParse\` against a schema for the subset of fields the redirect needs (envSlug / projectSlug / orgSlug / optional spanId). Wrong-typed or missing fields now collapse into a single parse-fail branch that logs the structured issue list and returns null. Adds a regression test for the structural-vs-typeof distinction: \`environment.slug: 42\` (number) is now rejected, where the previous \`typeof slug === "string"\` chain would silently accept any string- typed value but had no defence against shape drift in other fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l enum \`SyntheticRun.machinePreset\` is a plain string sourced from the mollifier snapshot, but \`SpanRun.machinePreset\` is the typed \`MachinePresetName\` enum (micro / small-1x / small-2x / medium-1x / medium-2x / large-1x / large-2x). The direct assignment failed \`tsc --noEmit\` and CI typecheck. Validate via \`MachinePresetName.safeParse\` and collapse unknown values to \`undefined\` so a stale preset returned by the buffer doesn't bleed into the UI as a typed-but-unknown value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
047b240 to
e57bc5e
Compare
| typeof buffered.metadata === "string" ? buffered.metadata : null; | ||
|
|
||
| return { | ||
| id: buffered.friendlyId, |
There was a problem hiding this comment.
🟡 synthesiseFoundRunFromBuffer uses friendlyId for internal id field
In synthesiseFoundRunFromBuffer, line 551 sets id: buffered.friendlyId instead of id: buffered.id. The SyntheticRun distinguishes between id (the internal cuid derived via RunId.fromFriendlyId() at apps/webapp/app/v3/mollifier/readFallback.server.ts:155) and friendlyId (the user-facing run_xxx identifier). The FoundRun type extends CommonRelatedRun, whose id corresponds to the Prisma TaskRun.id column (an internal cuid). By assigning the friendly ID to id, downstream logging in call() (e.g., taskRunId: taskRun.id at lines 176 and 205) will record the friendly ID instead of the internal ID, breaking log correlation with other systems that use the internal ID.
| id: buffered.friendlyId, | |
| id: buffered.id, |
Was this helpful? React with 👍 or 👎 to provide feedback.
| startTime: buffered.createdAt, | ||
| duration: 0, | ||
| isError: false, | ||
| isPartial: true, |
There was a problem hiding this comment.
🟡 isPartial hardcoded to true in trace route for cancelled buffered runs
In the buffered-run branch of api.v1.runs.$runId.trace.ts, isPartial is hardcoded to true (line 98). For cancelled runs, this is incorrect — cancelled runs have reached a terminal state and should have isPartial: false. The sibling span route (api.v1.runs.$runId.spans.$spanId.ts:96) correctly uses isPartial: resolved.run.status !== "CANCELED", and syntheticTrace.server.ts:27 correctly uses isPartial: !isCancelled. This inconsistency means the trace API returns a "still in progress" signal for cancelled buffered runs, which could confuse SDK consumers or dashboard components that rely on isPartial to determine if the span is finalized.
| isPartial: true, | |
| isPartial: buffered.status !== "CANCELED", |
Was this helpful? React with 👍 or 👎 to provide feedback.
| payload: typeof buffered.payload === "string" ? buffered.payload : "", | ||
| payloadType: buffered.payloadType ?? "application/json", |
There was a problem hiding this comment.
🚩 Buffered payload silently dropped when non-string
In synthesiseFoundRunFromBuffer at line 577, the payload is set to typeof buffered.payload === "string" ? buffered.payload : "". The SyntheticRun.payload is typed unknown and is assigned from snapshot.payload (readFallback.server.ts:164). If the snapshot stores the payload as a parsed JSON object rather than a serialized string, it gets replaced with "". This is then processed by conditionallyImportPacket and parsePacketAsJson, which returns undefined for empty strings (since !"" is truthy at ioSerialization.ts:52). So the API response will show no payload. This appears intentional (the PG column is String? so only strings are expected), but it means buffered runs with object-typed payloads in Redis will silently lose their payload in the API response.
Was this helpful? React with 👍 or 👎 to provide feedback.
| export async function loader({ request, params }: LoaderFunctionArgs) { | ||
| const authenticationResult = await authenticateApiRequest(request); | ||
| if (!authenticationResult) { | ||
| return json({ error: "Invalid or Missing API Key" }, { status: 401 }); | ||
| } | ||
|
|
||
| const parsed = ParamsSchema.safeParse(params); | ||
| if (!parsed.success) { | ||
| return json({ error: "Invalid or missing run ID" }, { status: 400 }); | ||
| } | ||
|
|
||
| return json({ attempts: [] }, { status: 200 }); | ||
| } |
There was a problem hiding this comment.
🚩 Attempts loader returns 200 for any run without ownership verification
The new loader in api.v1.runs.$runParam.attempts.ts authenticates the API key but does not verify that the run exists or that the authenticated environment owns it. Any valid API key holder can GET /api/v1/runs/<any-run-id>/attempts and receive { attempts: [] } with status 200. Since the response contains no sensitive data (just an empty array), this is not a security leak, but it deviates from the authorization pattern used by other run endpoints (spans, trace, retrieve) which verify run ownership. The PR comments indicate this is intentional for the parity script contract, but it means the endpoint cannot distinguish between "run exists with no attempts" and "run doesn't exist / belongs to another environment."
Was this helpful? React with 👍 or 👎 to provide feedback.
| @@ -0,0 +1,112 @@ | |||
| import { deserialiseSnapshot, type MollifierBuffer } from "@trigger.dev/redis-worker"; | |||
There was a problem hiding this comment.
🚩 syntheticRedirectInfo uses deserialiseSnapshot while readFallback uses deserialiseMollifierSnapshot
The syntheticRedirectInfo.server.ts imports deserialiseSnapshot from @trigger.dev/redis-worker (line 1), while readFallback.server.ts imports deserialiseMollifierSnapshot from ./mollifierSnapshot.server (line 4). These are different deserialisation paths for the same entry.payload data. If the two functions have different behaviour (e.g., one parses JSON while the other uses msgpack, or they handle errors differently), the two modules could disagree on the snapshot contents. Worth verifying that both deserialise functions are equivalent or intentionally different for their use cases.
Was this helpful? React with 👍 or 👎 to provide feedback.
Summary
Synthesise QUEUED/FAILED responses from the mollifier buffer when a TaskRun row hasn't landed in Postgres yet. Wires the synthesis into:
ApiRetrieveRunPresenterThe
readFallbackinfra itself lives on the trigger PR (consumed byIdempotencyKeyConcern); this PR adds the route-level synthetic-rendering primitives.Stacked on the replay PR.
Test plan