Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
"**/node_modules/**": true,
"packages/cli-v3/e2e": true
},
"vitest.disableWorkspaceWarning": true
"vitest.disableWorkspaceWarning": true,
"typescript.experimental.useTsgo": false
}
188 changes: 188 additions & 0 deletions TaskEvent_Property_Analysis.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1109,6 +1109,21 @@ const EnvironmentSchema = z
CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"),
CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),

EVENTS_CLICKHOUSE_URL: z
.string()
.optional()
.transform((v) => v ?? process.env.CLICKHOUSE_URL),
EVENTS_CLICKHOUSE_KEEP_ALIVE_ENABLED: z.string().default("1"),
EVENTS_CLICKHOUSE_KEEP_ALIVE_IDLE_SOCKET_TTL_MS: z.coerce.number().int().optional(),
EVENTS_CLICKHOUSE_MAX_OPEN_CONNECTIONS: z.coerce.number().int().default(10),
EVENTS_CLICKHOUSE_LOG_LEVEL: z.enum(["log", "error", "warn", "info", "debug"]).default("info"),
EVENTS_CLICKHOUSE_COMPRESSION_REQUEST: z.string().default("1"),
EVENTS_CLICKHOUSE_BATCH_SIZE: z.coerce.number().int().default(1000),
EVENTS_CLICKHOUSE_FLUSH_INTERVAL_MS: z.coerce.number().int().default(1000),
EVENT_REPOSITORY_CLICKHOUSE_ROLLOUT_PERCENT: z.coerce.number().optional(),
EVENT_REPOSITORY_DEFAULT_STORE: z.enum(["postgres", "clickhouse"]).default("postgres"),
EVENTS_CLICKHOUSE_MAX_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(250_000),

// Bootstrap
TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"),
TRIGGER_BOOTSTRAP_WORKER_GROUP_NAME: z.string().optional(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export class ApiRetrieveRunPresenter {
},
select: {
...commonRunSelect,
traceId: true,
payload: true,
payloadType: true,
output: true,
Expand Down
47 changes: 42 additions & 5 deletions apps/webapp/app/presenters/v3/RunPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { createTreeFromFlatItems, flattenTree } from "~/components/primitives/Tr
import { prisma, type PrismaClient } from "~/db.server";
import { createTimelineSpanEventsFromSpanEvents } from "~/utils/timelineSpanEvents";
import { getUsername } from "~/utils/username";
import { eventRepository } from "~/v3/eventRepository.server";
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
import { SpanSummary } from "~/v3/eventRepository/eventRepository.types";
import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server";
import { isFinalRunStatus } from "~/v3/taskStatus";

Expand Down Expand Up @@ -45,9 +46,11 @@ export class RunPresenter {
id: true,
createdAt: true,
taskEventStore: true,
taskIdentifier: true,
number: true,
traceId: true,
spanId: true,
parentSpanId: true,
friendlyId: true,
status: true,
startedAt: true,
Expand Down Expand Up @@ -140,18 +143,51 @@ export class RunPresenter {
};
}

const eventRepository = resolveEventRepositoryForStore(run.taskEventStore);

// get the events
const traceSummary = await eventRepository.getTraceSummary(
let traceSummary = await eventRepository.getTraceSummary(
getTaskEventStoreTableForRun(run),
run.runtimeEnvironment.id,
run.traceId,
run.rootTaskRun?.createdAt ?? run.createdAt,
run.completedAt ?? undefined,
{ includeDebugLogs: showDebug }
);

if (!traceSummary) {
return {
run: runData,
trace: undefined,
const spanSummary: SpanSummary = {
id: run.spanId,
parentId: run.parentSpanId ?? undefined,
runId: run.friendlyId,
data: {
message: run.taskIdentifier,
style: { icon: "task", variant: "primary" },
events: [],
startTime: run.createdAt,
duration: 0,
isError:
run.status === "COMPLETED_WITH_ERRORS" ||
run.status === "CRASHED" ||
run.status === "EXPIRED" ||
run.status === "SYSTEM_FAILURE" ||
run.status === "TIMED_OUT",
isPartial:
run.status === "DELAYED" ||
run.status === "PENDING" ||
run.status === "PAUSED" ||
run.status === "RETRYING_AFTER_FAILURE" ||
run.status === "DEQUEUED" ||
run.status === "EXECUTING",
isCancelled: run.status === "CANCELED",
isDebug: false,
level: "TRACE",
},
};

traceSummary = {
rootSpan: spanSummary,
spans: [spanSummary],
};
}

Expand Down Expand Up @@ -220,6 +256,7 @@ export class RunPresenter {
queuedDuration: run.startedAt
? millisecondsToNanoseconds(run.startedAt.getTime() - run.createdAt.getTime())
: undefined,
overridesBySpanId: traceSummary.overridesBySpanId,
},
};
}
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/presenters/v3/RunStreamPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { eventStream } from "remix-utils/sse/server";
import { PrismaClient, prisma } from "~/db.server";
import { logger } from "~/services/logger.server";
import { throttle } from "~/utils/throttle";
import { eventRepository } from "~/v3/eventRepository.server";
import { tracePubSub } from "~/v3/services/tracePubSub.server";

const pingInterval = 1000;

Expand Down Expand Up @@ -41,7 +41,7 @@ export class RunStreamPresenter {

let pinger: NodeJS.Timeout | undefined = undefined;

const { unsubscribe, eventEmitter } = await eventRepository.subscribeToTrace(run.traceId);
const { unsubscribe, eventEmitter } = await tracePubSub.subscribeToTrace(run.traceId);

return eventStream(request.signal, (send, close) => {
const safeSend = (args: { event?: string; data: string }) => {
Expand Down
94 changes: 63 additions & 31 deletions apps/webapp/app/presenters/v3/SpanPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,23 @@ import {
import { AttemptId, getMaxDuration, parseTraceparent } from "@trigger.dev/core/v3/isomorphic";
import { RUNNING_STATUSES } from "~/components/runs/v3/TaskRunStatus";
import { logger } from "~/services/logger.server";
import { eventRepository, rehydrateAttribute } from "~/v3/eventRepository.server";
import { rehydrateAttribute } from "~/v3/eventRepository/eventRepository.server";
import { machinePresetFromRun } from "~/v3/machinePresets.server";
import { getTaskEventStoreTableForRun, type TaskEventStoreTable } from "~/v3/taskEventStore.server";
import { isFailedRunStatus, isFinalRunStatus } from "~/v3/taskStatus";
import { BasePresenter } from "./basePresenter.server";
import { WaitpointPresenter } from "./WaitpointPresenter.server";
import { engine } from "~/v3/runEngine.server";
import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server";
import { IEventRepository, SpanDetail } from "~/v3/eventRepository/eventRepository.types";

type Result = Awaited<ReturnType<SpanPresenter["call"]>>;
export type Span = NonNullable<NonNullable<Result>["span"]>;
export type SpanRun = NonNullable<NonNullable<Result>["run"]>;
type FindRunResult = NonNullable<
Awaited<ReturnType<InstanceType<typeof SpanPresenter>["findRun"]>>
>;
type GetSpanResult = NonNullable<Awaited<ReturnType<(typeof eventRepository)["getSpan"]>>>;
type GetSpanResult = SpanDetail;

export class SpanPresenter extends BasePresenter {
public async call({
Expand Down Expand Up @@ -74,14 +76,20 @@ export class SpanPresenter extends BasePresenter {
return;
}

const { traceId } = parentRun;

const eventRepository = resolveEventRepositoryForStore(parentRun.taskEventStore);

const eventStore = getTaskEventStoreTableForRun(parentRun);

const run = await this.getRun({
eventStore,
environmentId: parentRun.runtimeEnvironmentId,
traceId,
eventRepository,
spanId,
createdAt: parentRun.createdAt,
completedAt: parentRun.completedAt,
environmentId: parentRun.runtimeEnvironmentId,
});
if (run) {
return {
Expand All @@ -93,10 +101,12 @@ export class SpanPresenter extends BasePresenter {
const span = await this.#getSpan({
eventStore,
spanId,
traceId,
environmentId: parentRun.runtimeEnvironmentId,
projectId: parentRun.projectId,
createdAt: parentRun.createdAt,
completedAt: parentRun.completedAt,
eventRepository,
});

if (!span) {
Expand All @@ -112,29 +122,30 @@ export class SpanPresenter extends BasePresenter {
async getRun({
eventStore,
environmentId,
traceId,
eventRepository,
spanId,
createdAt,
completedAt,
}: {
eventStore: TaskEventStoreTable;
environmentId: string;
traceId: string;
eventRepository: IEventRepository;
spanId: string;
createdAt: Date;
completedAt: Date | null;
}) {
const span = await eventRepository.getSpan({
storeTable: eventStore,
const originalRunId = await eventRepository.getSpanOriginalRunId(
eventStore,
environmentId,
spanId,
environmentId,
startCreatedAt: createdAt,
endCreatedAt: completedAt ?? undefined,
});
createdAt,
completedAt ?? undefined
);
Comment on lines +139 to +146
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Pass the traceId into getSpanOriginalRunId

IEventRepository.getSpanOriginalRunId expects (storeTable, environmentId, spanId, traceId, …), but this call passes environmentId for the traceId slot. That breaks ClickHouse-backed lookups (and pollutes the cache key) so the original run is never found, causing cached runs to fall back to the plain span path. Please wire the actual traceId through.

     const originalRunId = await eventRepository.getSpanOriginalRunId(
       eventStore,
       environmentId,
       spanId,
-      environmentId,
+      traceId,
       createdAt,
       completedAt ?? undefined
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const originalRunId = await eventRepository.getSpanOriginalRunId(
eventStore,
environmentId,
spanId,
environmentId,
startCreatedAt: createdAt,
endCreatedAt: completedAt ?? undefined,
});
createdAt,
completedAt ?? undefined
);
const originalRunId = await eventRepository.getSpanOriginalRunId(
eventStore,
environmentId,
spanId,
traceId,
createdAt,
completedAt ?? undefined
);
🤖 Prompt for AI Agents
In apps/webapp/app/presenters/v3/SpanPresenter.server.ts around lines 139 to
146, the call to eventRepository.getSpanOriginalRunId is passing environmentId
into the traceId parameter slot; change the fourth argument to the actual
traceId variable (ensure traceId is available in scope or retrieve it from the
event/span context) so the function is invoked as (eventStore, environmentId,
spanId, traceId, createdAt, completedAt ?? undefined), which fixes ClickHouse
lookups and prevents cache key pollution.


if (!span) {
return;
}

const run = await this.findRun({ span, spanId });
const run = await this.findRun({ originalRunId, spanId, environmentId });

if (!run) {
return;
Expand Down Expand Up @@ -259,7 +270,7 @@ export class SpanPresenter extends BasePresenter {
workerQueue: run.workerQueue,
traceId: run.traceId,
spanId: run.spanId,
isCached: !!span.originalRun,
isCached: !!originalRunId,
machinePreset: machine?.name,
externalTraceId,
};
Expand Down Expand Up @@ -294,7 +305,15 @@ export class SpanPresenter extends BasePresenter {
};
}

async findRun({ span, spanId }: { span: GetSpanResult; spanId: string }) {
async findRun({
originalRunId,
spanId,
environmentId,
}: {
originalRunId?: string;
spanId: string;
environmentId: string;
}) {
const run = await this._replica.taskRun.findFirst({
select: {
id: true,
Expand Down Expand Up @@ -404,12 +423,14 @@ export class SpanPresenter extends BasePresenter {
},
},
},
where: span.originalRun
where: originalRunId
? {
friendlyId: span.originalRun,
friendlyId: originalRunId,
runtimeEnvironmentId: environmentId,
}
: {
spanId,
runtimeEnvironmentId: environmentId,
},
});

Expand All @@ -418,27 +439,32 @@ export class SpanPresenter extends BasePresenter {

async #getSpan({
eventStore,
eventRepository,
traceId,
spanId,
environmentId,
projectId,
createdAt,
completedAt,
}: {
eventRepository: IEventRepository;
traceId: string;
spanId: string;
environmentId: string;
projectId: string;
eventStore: TaskEventStoreTable;
createdAt: Date;
completedAt: Date | null;
}) {
const span = await eventRepository.getSpan({
storeTable: eventStore,
spanId,
const span = await eventRepository.getSpan(
eventStore,
environmentId,
startCreatedAt: createdAt,
endCreatedAt: completedAt ?? undefined,
options: { includeDebugLogs: true },
});
spanId,
traceId,
createdAt,
completedAt ?? undefined,
{ includeDebugLogs: true }
);

if (!span) {
return;
Expand All @@ -451,23 +477,29 @@ export class SpanPresenter extends BasePresenter {
spanId: true,
createdAt: true,
number: true,
lockedToVersion: {
select: {
version: true,
},
},
taskVersion: true,
},
where: {
parentSpanId: spanId,
},
});

const data = {
...span,
spanId: span.spanId,
parentId: span.parentId,
message: span.message,
isError: span.isError,
isPartial: span.isPartial,
isCancelled: span.isCancelled,
level: span.level,
startTime: span.startTime,
duration: span.duration,
events: span.events,
style: span.style,
properties: span.properties ? JSON.stringify(span.properties, null, 2) : undefined,
entity: span.entity,
metadata: span.metadata,
triggeredRuns,
showActionBar: span.show?.actions === true,
};

switch (span.entity.type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route";
import { useSearchParams } from "~/hooks/useSearchParam";
import { CopyableText } from "~/components/primitives/CopyableText";
import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types";

const resizableSettings = {
parent: {
Expand Down Expand Up @@ -301,7 +302,8 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade
return <></>;
}

const { events, duration, rootSpanStatus, rootStartedAt, queuedDuration } = trace;
const { events, duration, rootSpanStatus, rootStartedAt, queuedDuration, overridesBySpanId } =
trace;
const shouldLiveReload = events.length <= maximumLiveReloadingSetting;

const changeToSpan = useDebounce((selectedSpan: string) => {
Expand All @@ -323,6 +325,8 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade
// WARNING Don't put the revalidator in the useEffect deps array or bad things will happen
}, [streamedEvents]); // eslint-disable-line react-hooks/exhaustive-deps

const spanOverrides = selectedSpanId ? overridesBySpanId?.[selectedSpanId] : undefined;

return (
<div className={cn("grid h-full max-h-full grid-cols-1 overflow-hidden")}>
<ResizablePanelGroup
Expand Down Expand Up @@ -371,6 +375,7 @@ function TraceView({ run, trace, maximumLiveReloadingSetting, resizable }: Loade
<SpanView
runParam={run.friendlyId}
spanId={selectedSpanId}
spanOverrides={spanOverrides as SpanOverride | undefined}
closePanel={() => replaceSearchParam("span")}
/>
</ResizablePanel>
Expand Down
Loading
Loading