Skip to content

Commit de7c36a

Browse files
MariefayTrigger.dev RepoOps
authored andcommitted
feat(webapp): link to the session a span started from the span panel
Selecting a span that started a session (`sessions.start()`) now shows a "Started session" row in the span panel, with the session's ID and status and a link to it. Orchestrator agents that start sub-agents can now be followed from the parent run to each sub-agent's session. Mono-RevId: 2501ecd7b7df2da396e0afa3af77090e37d28d11
1 parent 3eff04a commit de7c36a

5 files changed

Lines changed: 418 additions & 14 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Selecting a span that started a session now shows a link to that session in the span panel, so you can jump from an orchestrator's run to its sub-agents.

‎apps/webapp/app/presenters/v3/SpanPresenter.server.ts‎

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ import {
5353
} from "~/v3/runOpsMigration/controlPlaneResolver.server";
5454
import type { AuthenticatedEnvironment } from "@trigger.dev/core/v3/auth/environment";
5555
import { getTraceStartTimeEnd } from "~/v3/eventRepository/traceInsertedAtBound";
56+
import {
57+
findStartedSession,
58+
startedSessionExternalId,
59+
traceRootRunId,
60+
} from "./startedSession.server";
5661

5762
export type PromptSpanData = {
5863
slug: string;
@@ -155,6 +160,8 @@ export class SpanPresenter extends BasePresenter {
155160
},
156161
{
157162
select: {
163+
id: true,
164+
rootTaskRunId: true,
158165
traceId: true,
159166
runtimeEnvironmentId: true,
160167
projectId: true,
@@ -233,6 +240,8 @@ export class SpanPresenter extends BasePresenter {
233240
endCreatedAt: getTraceStartTimeEnd(parentRun),
234241
eventRepository: repository,
235242
showDebug,
243+
rootRunId: traceRootRunId(parentRun),
244+
userId,
236245
});
237246

238247
if (!span) {
@@ -698,6 +707,8 @@ export class SpanPresenter extends BasePresenter {
698707
createdAt,
699708
endCreatedAt,
700709
showDebug,
710+
rootRunId,
711+
userId,
701712
}: {
702713
eventRepository: IEventRepository;
703714
traceId: string;
@@ -708,6 +719,8 @@ export class SpanPresenter extends BasePresenter {
708719
createdAt: Date;
709720
endCreatedAt: Date | undefined;
710721
showDebug: boolean;
722+
rootRunId: string;
723+
userId: string;
711724
}) {
712725
const span = await eventRepository.getSpan(
713726
eventStore,
@@ -723,21 +736,33 @@ export class SpanPresenter extends BasePresenter {
723736
return;
724737
}
725738

726-
const triggeredRuns = await runStore.findRuns(
727-
{
728-
where: {
729-
parentSpanId: spanId,
730-
},
731-
select: {
732-
friendlyId: true,
733-
taskIdentifier: true,
734-
spanId: true,
735-
createdAt: true,
736-
status: true,
739+
const childExternalId = startedSessionExternalId(span);
740+
741+
const [triggeredRuns, startedSession] = await Promise.all([
742+
runStore.findRuns(
743+
{
744+
where: {
745+
parentSpanId: spanId,
746+
},
747+
select: {
748+
friendlyId: true,
749+
taskIdentifier: true,
750+
spanId: true,
751+
createdAt: true,
752+
status: true,
753+
},
737754
},
738-
},
739-
this._replica
740-
);
755+
this._replica
756+
),
757+
childExternalId
758+
? findStartedSession(this._replica, {
759+
environmentId,
760+
externalId: childExternalId,
761+
rootRunId,
762+
userId,
763+
})
764+
: undefined,
765+
]);
741766

742767
const data = {
743768
spanId: span.spanId,
@@ -766,6 +791,7 @@ export class SpanPresenter extends BasePresenter {
766791
entity: span.entity,
767792
metadata: span.metadata,
768793
triggeredRuns,
794+
startedSession,
769795
aiData:
770796
span.properties && typeof span.properties === "object"
771797
? extractAISpanData(span.properties as Record<string, unknown>, span.duration / 1_000_000)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { PrismaClientOrTransaction } from "~/db.server";
2+
import type { SessionStatus } from "~/services/sessionsRepository/sessionsRepository.server";
3+
4+
const SESSION_START_SPAN_MESSAGE = "sessions.start()";
5+
6+
export type StartedSession = {
7+
friendlyId: string;
8+
externalId: string;
9+
status: SessionStatus;
10+
/** The trace already belongs to this session, so it isn't linked. */
11+
isCurrent: boolean;
12+
};
13+
14+
/** The `externalId` of a `sessions.start()` span. The `session` attribute falls back to the type, so it isn't used. */
15+
export function startedSessionExternalId(span: {
16+
message: string;
17+
properties: unknown;
18+
}): string | undefined {
19+
if (span.message !== SESSION_START_SPAN_MESSAGE) return;
20+
if (!span.properties || typeof span.properties !== "object") return;
21+
22+
const externalId = (span.properties as Record<string, unknown>).externalId;
23+
if (typeof externalId !== "string" || externalId.length === 0) return;
24+
25+
return externalId;
26+
}
27+
28+
/** Session runs start their own trace, so the root run says which session a trace is in. */
29+
export function traceRootRunId(run: { id: string; rootTaskRunId: string | null }): string {
30+
return run.rootTaskRunId ?? run.id;
31+
}
32+
33+
export async function findStartedSession(
34+
db: PrismaClientOrTransaction,
35+
{
36+
environmentId,
37+
externalId,
38+
rootRunId,
39+
userId,
40+
}: {
41+
environmentId: string;
42+
externalId: string;
43+
rootRunId: string;
44+
userId: string;
45+
}
46+
): Promise<StartedSession | undefined> {
47+
const [session, viewableEnvironment, rootSessionRun] = await Promise.all([
48+
db.session.findFirst({
49+
where: { runtimeEnvironmentId: environmentId, externalId },
50+
select: { id: true, friendlyId: true, closedAt: true, expiresAt: true },
51+
}),
52+
// Same rule as the session page: dev environments are visible to their owner only.
53+
db.runtimeEnvironment.findFirst({
54+
where: {
55+
id: environmentId,
56+
OR: [{ type: { not: "DEVELOPMENT" } }, { orgMember: { userId } }],
57+
},
58+
select: { id: true },
59+
}),
60+
db.sessionRun.findFirst({
61+
where: { runId: rootRunId },
62+
select: { sessionId: true },
63+
}),
64+
]);
65+
66+
if (!session || !viewableEnvironment) return;
67+
68+
return {
69+
friendlyId: session.friendlyId,
70+
externalId,
71+
status:
72+
session.closedAt != null
73+
? "CLOSED"
74+
: session.expiresAt != null && session.expiresAt.getTime() < Date.now()
75+
? "EXPIRED"
76+
: "ACTIVE",
77+
isCurrent: rootSessionRun?.sessionId === session.id,
78+
};
79+
}

‎apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ import {
105105
type RunQueueWaiting,
106106
} from "~/presenters/v3/RunQueueMetricsPresenter.server";
107107
import { type Span, SpanPresenter, type SpanRun } from "~/presenters/v3/SpanPresenter.server";
108+
import { type StartedSession } from "~/presenters/v3/startedSession.server";
108109
import { findProjectWithOrgFlagsBySlug } from "~/models/project.server";
109110
import { logger } from "~/services/logger.server";
110111
import { rbac } from "~/services/rbac.server";
@@ -1681,6 +1682,45 @@ function TraceExportMenuItems({ runParam }: { runParam: string }) {
16811682
);
16821683
}
16831684

1685+
function StartedSessionProperty({ session }: { session: StartedSession }) {
1686+
const organization = useOrganization();
1687+
const project = useProject();
1688+
const environment = useEnvironment();
1689+
1690+
const id = <CopyableText value={session.externalId} copyValue={session.externalId} asChild />;
1691+
const status = <SessionStatusCombo status={session.status} />;
1692+
1693+
return (
1694+
<Property.Item>
1695+
<Property.Label>Started session</Property.Label>
1696+
<Property.Value>
1697+
{session.isCurrent ? (
1698+
<span className="flex flex-wrap items-center gap-x-2 gap-y-0">
1699+
{id}
1700+
{status}
1701+
</span>
1702+
) : (
1703+
<SimpleTooltip
1704+
button={
1705+
<TextLink
1706+
to={v3SessionPath(organization, project, environment, {
1707+
friendlyId: session.friendlyId,
1708+
})}
1709+
className="group flex flex-wrap items-center gap-x-2 gap-y-0"
1710+
>
1711+
{id}
1712+
{status}
1713+
</TextLink>
1714+
}
1715+
content="Jump to session"
1716+
disableHoverableContent
1717+
/>
1718+
)}
1719+
</Property.Value>
1720+
</Property.Item>
1721+
);
1722+
}
1723+
16841724
function SpanEntity({ span }: { span: Span }) {
16851725
const isAdmin = useHasAdminAccess();
16861726

@@ -1726,6 +1766,7 @@ function SpanEntity({ span }: { span: Span }) {
17261766
</div>
17271767
)}
17281768
<Property.Table>
1769+
{span.startedSession && <StartedSessionProperty session={span.startedSession} />}
17291770
<Property.Item>
17301771
<Property.Label className="flex items-center justify-between">
17311772
<span>Message</span>

0 commit comments

Comments
 (0)