-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(webapp): fetch run-scoped trace subtrees for large traces #4024
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| Fix run-scoped trace subtree fetching so ancestor spans are loaded regardless of the anchor run's time window. Ancestors start before the anchor run and are fetched by explicit span IDs, so applying the anchor's `startCreatedAt` filter wrongly excluded them — which meant cancellation/error overrides from an ancestor never propagated down to the anchor subtree (e.g. a child span stayed PARTIAL when its parent run was cancelled). Ancestor fetches now skip the time window. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| area: webapp | ||
| type: fix | ||
| --- | ||
|
|
||
| Fix empty trace views for child and nested runs in very large traces. The dashboard and retrieve-trace API now return the requested run's span subtree. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,21 @@ | ||
| import { type Prisma, prisma } from "~/db.server"; | ||
| import type { Organization, OrgMember, Project } from "@trigger.dev/database"; | ||
| import { Prisma as PrismaNamespace, type Prisma, prisma } from "~/db.server"; | ||
| import { createEnvironment } from "./organization.server"; | ||
| import { customAlphabet } from "nanoid"; | ||
| import { logger } from "~/services/logger.server"; | ||
| import { rbac } from "~/services/rbac.server"; | ||
|
|
||
| export const INVITE_NOT_FOUND = "Invite not found"; | ||
| export const ENV_SETUP_INCOMPLETE = | ||
| "You joined the organization, but we couldn't finish setting up your development environments. Accept the invitation again to retry, or contact support if this persists."; | ||
|
|
||
| export function isAcceptInviteFormError(error: unknown): error is Error { | ||
| return ( | ||
| error instanceof Error && | ||
| (error.message === INVITE_NOT_FOUND || error.message === ENV_SETUP_INCOMPLETE) | ||
| ); | ||
| } | ||
|
|
||
| const tokenValueLength = 40; | ||
| const tokenGenerator = customAlphabet("123456789abcdefghijkmnopqrstuvwxyz", tokenValueLength); | ||
|
|
||
|
|
@@ -177,92 +189,181 @@ export async function getUsersInvites({ email }: { email: string }) { | |
| }); | ||
| } | ||
|
|
||
| export async function acceptInvite({ | ||
| user, | ||
| export async function provisionMemberDevelopmentEnvironments({ | ||
| inviteId, | ||
| user, | ||
| member, | ||
| organization, | ||
| projects, | ||
| }: { | ||
| user: { id: string; email: string }; | ||
| inviteId: string; | ||
| user: { id: string; email: string }; | ||
| member: OrgMember; | ||
| organization: Pick<Organization, "id" | "maximumConcurrencyLimit">; | ||
| projects: Pick<Project, "id">[]; | ||
| }) { | ||
| const result = await prisma.$transaction(async (tx) => { | ||
| // 1. Delete the invite and get the invite details | ||
| const invite = await tx.orgMemberInvite.delete({ | ||
| where: { | ||
| id: inviteId, | ||
| email: user.email, | ||
| }, | ||
| include: { | ||
| organization: { | ||
| include: { | ||
| projects: true, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| const projectIds = projects.map((p) => p.id); | ||
| const existingDevEnvs = await prisma.runtimeEnvironment.findMany({ | ||
| where: { | ||
| orgMemberId: member.id, | ||
| type: "DEVELOPMENT", | ||
| projectId: { in: projectIds }, | ||
| }, | ||
| select: { projectId: true }, | ||
| }); | ||
| const existingProjectIds = new Set(existingDevEnvs.map((env) => env.projectId)); | ||
| const projectsToProvision = projects.filter((project) => !existingProjectIds.has(project.id)); | ||
|
|
||
| // 2. Join the organization | ||
| const member = await tx.orgMember.create({ | ||
| data: { | ||
| organizationId: invite.organizationId, | ||
| userId: user.id, | ||
| role: invite.role, | ||
| }, | ||
| }); | ||
| const createdProjectIds: string[] = []; | ||
| let failedProjectId: string | undefined; | ||
| let failedProjectIndex: number | undefined; | ||
|
|
||
| try { | ||
| for (const [index, project] of projectsToProvision.entries()) { | ||
| failedProjectId = project.id; | ||
| failedProjectIndex = index; | ||
|
|
||
| // 3. Create an environment for each project | ||
| for (const project of invite.organization.projects) { | ||
| await createEnvironment({ | ||
| organization: invite.organization, | ||
| organization, | ||
| project, | ||
| type: "DEVELOPMENT", | ||
| // We set this true but no backfill (yet!?) so never used | ||
| // for dev environments | ||
| isBranchableEnvironment: true, | ||
| member, | ||
| prismaClient: tx, | ||
| }); | ||
|
|
||
| createdProjectIds.push(project.id); | ||
| failedProjectId = undefined; | ||
| failedProjectIndex = undefined; | ||
| } | ||
| } catch (error) { | ||
| logger.error("acceptInvite: development environment creation failed after membership created", { | ||
| inviteId, | ||
| userId: user.id, | ||
| organizationId: organization.id, | ||
| orgMemberId: member.id, | ||
| projectIds, | ||
| failedProjectId, | ||
| failedProjectIndex, | ||
| totalProjects: projects.length, | ||
| skippedProjectIds: [...existingProjectIds], | ||
| createdProjectIds, | ||
| error: | ||
| error instanceof Error | ||
| ? { name: error.name, message: error.message, stack: error.stack } | ||
| : String(error), | ||
| }); | ||
|
|
||
| // 4. Check for other invites | ||
| const remainingInvites = await tx.orgMemberInvite.findMany({ | ||
| throw new Error(ENV_SETUP_INCOMPLETE); | ||
| } | ||
| } | ||
|
|
||
| export async function acceptInvite({ | ||
| user, | ||
| inviteId, | ||
| }: { | ||
| user: { id: string; email: string }; | ||
| inviteId: string; | ||
| }) { | ||
| const invite = await prisma.orgMemberInvite.findFirst({ | ||
| where: { id: inviteId, email: user.email }, | ||
| include: { | ||
| organization: { | ||
| include: { | ||
| projects: { where: { deletedAt: null } }, | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| if (!invite) { | ||
| throw new Error(INVITE_NOT_FOUND); | ||
| } | ||
|
|
||
| let member = await prisma.orgMember.findFirst({ | ||
| where: { userId: user.id, organizationId: invite.organizationId }, | ||
| }); | ||
| if (!member) { | ||
| try { | ||
| member = await prisma.orgMember.create({ | ||
| data: { | ||
| organizationId: invite.organizationId, | ||
| userId: user.id, | ||
| role: invite.role, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| if ( | ||
| error instanceof PrismaNamespace.PrismaClientKnownRequestError && | ||
| error.code === "P2002" | ||
| ) { | ||
| member = await prisma.orgMember.findFirst({ | ||
| where: { userId: user.id, organizationId: invite.organizationId }, | ||
| }); | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (!member) { | ||
| throw new Error(ENV_SETUP_INCOMPLETE); | ||
| } | ||
|
|
||
| await provisionMemberDevelopmentEnvironments({ | ||
| inviteId, | ||
| user, | ||
| member, | ||
| organization: invite.organization, | ||
| projects: invite.organization.projects, | ||
| }); | ||
|
|
||
| try { | ||
| await prisma.orgMemberInvite.delete({ | ||
| where: { | ||
| id: inviteId, | ||
| email: user.email, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| if (error instanceof PrismaNamespace.PrismaClientKnownRequestError && error.code === "P2025") { | ||
| // Another concurrent accept finished first — membership and envs are ready. | ||
| } else { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
|
Comment on lines
+262
to
335
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 acceptInvite no longer uses a transaction — concurrent accepts could create duplicate environments The old Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| return { | ||
| remainingInvites, | ||
| organization: invite.organization, | ||
| inviteRole: invite.role, | ||
| rbacRoleId: invite.rbacRoleId, | ||
| }; | ||
| const remainingInvites = await prisma.orgMemberInvite.findMany({ | ||
| where: { | ||
| email: user.email, | ||
| }, | ||
| }); | ||
|
|
||
| // If the invite carried an explicit RBAC role, assign it. Best-effort: the | ||
| // invite is already consumed and membership created above, so a failure here | ||
| // — a returned {ok:false} or a thrown error from the plugin — must not block | ||
| // joining the org. Swallow and log either way; without the catch a plugin | ||
| // throw escapes and turns the whole invite-accept into a 400. | ||
| if (result.rbacRoleId) { | ||
| if (invite.rbacRoleId) { | ||
| try { | ||
| const roleResult = await rbac.setUserRole({ | ||
| userId: user.id, | ||
| organizationId: result.organization.id, | ||
| roleId: result.rbacRoleId, | ||
| organizationId: invite.organization.id, | ||
| roleId: invite.rbacRoleId, | ||
| }); | ||
| if (!roleResult.ok) { | ||
| logger.error("acceptInvite: skipped RBAC role assignment", { | ||
| organizationId: result.organization.id, | ||
| organizationId: invite.organization.id, | ||
| userId: user.id, | ||
| rbacRoleId: result.rbacRoleId, | ||
| rbacRoleId: invite.rbacRoleId, | ||
| reason: roleResult.error, | ||
| }); | ||
| } | ||
| } catch (error) { | ||
| logger.error("acceptInvite: RBAC role assignment threw", { | ||
| organizationId: result.organization.id, | ||
| organizationId: invite.organization.id, | ||
| userId: user.id, | ||
| rbacRoleId: result.rbacRoleId, | ||
| rbacRoleId: invite.rbacRoleId, | ||
| error: | ||
| error instanceof Error | ||
| ? { name: error.name, message: error.message, stack: error.stack } | ||
|
|
@@ -271,7 +372,7 @@ export async function acceptInvite({ | |
| } | ||
| } | ||
|
|
||
| return { remainingInvites: result.remainingInvites, organization: result.organization }; | ||
| return { remainingInvites, organization: invite.organization }; | ||
| } | ||
|
|
||
| export async function declineInvite({ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Make duplicate dev-env creation idempotent under concurrent accepts.
Two concurrent
acceptInvitecalls can both see the same project as missing, then onecreateEnvironmentsucceeds and the other hits the runtime-environment unique constraint for the same member/project. That second request currently throwsENV_SETUP_INCOMPLETEeven though the other request may have completed setup. Treat PrismaP2002for the per-project env create as “already created” by re-checking that project/member env and continuing.