Skip to content

Commit 8dc77c0

Browse files
authored
fix(webapp): only load env var values for displayed environments (#3903)
## Summary The environment variables page loaded every variable value in the project, unfiltered by environment. Archiving a preview branch does not delete its environment variable value rows, so projects that churn preview branches accumulate values forever, and every page view loaded all of them. On large projects this made the page loader take many seconds and stalled the server while deserializing the oversized result. ## Fix The presenter now loads the displayed environments first and filters the `values` relation to those environment IDs. That matches the display semantics exactly (per-user dev environments and active branch environments included), and the lookup is covered by the existing unique index on `(variableId, environmentId)`. Values in archived branch environments are no longer fetched at all. Covered by a new testcontainers test asserting that values from active environments (including branch environments) are returned while archived branch environments are excluded.
1 parent cc9eabd commit 8dc77c0

5 files changed

Lines changed: 142 additions & 17 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: fix
4+
---
5+
6+
Speed up the environment variables page for projects with many archived preview branches. The page now only loads variable values for the environments it displays instead of every value ever created, including those left behind by archived branches.

apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { PrismaClient, prisma } from "~/db.server";
1+
import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server";
22
import { Project } from "~/models/project.server";
33
import { User } from "~/models/user.server";
44
import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server";
@@ -15,13 +15,15 @@ export type EnvironmentVariableWithSetValues = Result["environmentVariables"][nu
1515

1616
export class EnvironmentVariablesPresenter {
1717
#prismaClient: PrismaClient;
18+
#replicaClient: PrismaReplicaClient;
1819

19-
constructor(prismaClient: PrismaClient = prisma) {
20+
constructor(prismaClient: PrismaClient = prisma, replicaClient: PrismaReplicaClient = $replica) {
2021
this.#prismaClient = prismaClient;
22+
this.#replicaClient = replicaClient;
2123
}
2224

2325
public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) {
24-
const project = await this.#prismaClient.project.findFirst({
26+
const project = await this.#replicaClient.project.findFirst({
2527
select: {
2628
id: true,
2729
},
@@ -41,7 +43,18 @@ export class EnvironmentVariablesPresenter {
4143
throw new Error("Project not found");
4244
}
4345

44-
const environmentVariables = await this.#prismaClient.environmentVariable.findMany({
46+
const { environments: sortedEnvironments, hasStaging } =
47+
await loadEnvironmentVariablesEnvironments(
48+
this.#replicaClient,
49+
{ userId, projectId: project.id },
50+
{ skipProjectAccessCheck: true }
51+
);
52+
53+
// Only load values for the environments we display. Projects can accumulate
54+
// values in archived branch environments, which would otherwise all be loaded here.
55+
const environmentIds = sortedEnvironments.map((env) => env.id);
56+
57+
const environmentVariables = await this.#replicaClient.environmentVariable.findMany({
4558
select: {
4659
id: true,
4760
key: true,
@@ -59,6 +72,11 @@ export class EnvironmentVariablesPresenter {
5972
},
6073
isSecret: true,
6174
},
75+
where: {
76+
environmentId: {
77+
in: environmentIds,
78+
},
79+
},
6280
},
6381
},
6482
where: {
@@ -84,7 +102,7 @@ export class EnvironmentVariablesPresenter {
84102

85103
const users =
86104
userIds.size > 0
87-
? await this.#prismaClient.user.findMany({
105+
? await this.#replicaClient.user.findMany({
88106
where: {
89107
id: {
90108
in: Array.from(userIds),
@@ -102,14 +120,7 @@ export class EnvironmentVariablesPresenter {
102120
const usersRecord: Record<string, { id: string; name: string | null; displayName: string | null; avatarUrl: string | null }> =
103121
Object.fromEntries(users.map((u) => [u.id, u]));
104122

105-
const { environments: sortedEnvironments, hasStaging } =
106-
await loadEnvironmentVariablesEnvironments(
107-
this.#prismaClient,
108-
{ userId, projectId: project.id },
109-
{ skipProjectAccessCheck: true }
110-
);
111-
112-
const repository = new EnvironmentVariablesRepository(this.#prismaClient);
123+
const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient);
113124

114125
const nonSecretItems: Array<{ environmentId: string; key: string }> = [];
115126
for (const environmentVariable of environmentVariables) {

apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { RuntimeEnvironmentType } from "@trigger.dev/database";
2-
import { type PrismaClient } from "~/db.server";
2+
import { type PrismaReplicaClient } from "~/db.server";
33
import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort";
44

55
export type EnvironmentVariablesEnvironment = {
@@ -15,7 +15,7 @@ export type EnvironmentVariablesEnvironmentsResult = {
1515
};
1616

1717
export async function loadEnvironmentVariablesEnvironments(
18-
prismaClient: PrismaClient,
18+
prismaClient: PrismaReplicaClient,
1919
{ userId, projectId }: { userId: string; projectId: string },
2020
options?: { skipProjectAccessCheck?: boolean }
2121
): Promise<EnvironmentVariablesEnvironmentsResult> {

apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -591,7 +591,7 @@ export class EnvironmentVariablesRepository implements Repository {
591591
}
592592

593593
const secretStore = getSecretStore("DATABASE", {
594-
prismaClient: this.prismaClient,
594+
prismaClient: this.replicaClient,
595595
});
596596

597597
const storeKeys = Array.from(uniqueItems.values()).map((item) =>

apps/webapp/test/EnvironmentVariablesPresenter.test.ts

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ describe("EnvironmentVariablesPresenter", () => {
5656
userId: user.id,
5757
});
5858

59-
const result = await new EnvironmentVariablesPresenter(prisma).call({
59+
const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({
6060
userId: user.id,
6161
projectSlug,
6262
});
@@ -69,4 +69,112 @@ describe("EnvironmentVariablesPresenter", () => {
6969
expect(secretVariable!.value).toBe("");
7070
expect(nonSecretVariable!.value).toBe("plain-value");
7171
});
72+
73+
postgresTest(
74+
"returns values for active environments (including branch environments) and excludes archived branch environments",
75+
async ({ prisma }) => {
76+
const { user, organization, project, projectSlug } =
77+
await createTestOrgProjectWithMember(prisma);
78+
79+
const prodEnvironment = await createRuntimeEnvironment(prisma, {
80+
projectId: project.id,
81+
organizationId: organization.id,
82+
type: "PRODUCTION",
83+
});
84+
85+
const parentPreviewEnvironment = await createRuntimeEnvironment(prisma, {
86+
projectId: project.id,
87+
organizationId: organization.id,
88+
type: "PREVIEW",
89+
});
90+
await prisma.runtimeEnvironment.update({
91+
where: { id: parentPreviewEnvironment.id },
92+
data: { isBranchableEnvironment: true },
93+
});
94+
95+
const activeBranchEnvironment = await createRuntimeEnvironment(prisma, {
96+
projectId: project.id,
97+
organizationId: organization.id,
98+
type: "PREVIEW",
99+
});
100+
await prisma.runtimeEnvironment.update({
101+
where: { id: activeBranchEnvironment.id },
102+
data: {
103+
parentEnvironmentId: parentPreviewEnvironment.id,
104+
branchName: "feature/active",
105+
},
106+
});
107+
108+
const archivedBranchEnvironment = await createRuntimeEnvironment(prisma, {
109+
projectId: project.id,
110+
organizationId: organization.id,
111+
type: "PREVIEW",
112+
});
113+
await prisma.runtimeEnvironment.update({
114+
where: { id: archivedBranchEnvironment.id },
115+
data: {
116+
parentEnvironmentId: parentPreviewEnvironment.id,
117+
branchName: "feature/archived",
118+
},
119+
});
120+
121+
const repository = new EnvironmentVariablesRepository(prisma, prisma);
122+
123+
await createEnvironmentVariable(repository, project.id, {
124+
environmentId: prodEnvironment.id,
125+
key: "MY_VAR",
126+
value: "prod-value",
127+
userId: user.id,
128+
});
129+
await createEnvironmentVariable(repository, project.id, {
130+
environmentId: activeBranchEnvironment.id,
131+
key: "MY_VAR",
132+
value: "active-branch-value",
133+
userId: user.id,
134+
});
135+
await createEnvironmentVariable(repository, project.id, {
136+
environmentId: archivedBranchEnvironment.id,
137+
key: "MY_VAR",
138+
value: "archived-branch-value",
139+
userId: user.id,
140+
});
141+
142+
// Archive the branch after it accumulated values (archiving does not
143+
// delete its EnvironmentVariableValue rows).
144+
await prisma.runtimeEnvironment.update({
145+
where: { id: archivedBranchEnvironment.id },
146+
data: { archivedAt: new Date() },
147+
});
148+
149+
const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({
150+
userId: user.id,
151+
projectSlug,
152+
});
153+
154+
const environmentIds = result.environments.map((environment) => environment.id);
155+
expect(environmentIds).toContain(prodEnvironment.id);
156+
expect(environmentIds).toContain(activeBranchEnvironment.id);
157+
expect(environmentIds).not.toContain(archivedBranchEnvironment.id);
158+
159+
const myVarValues = result.environmentVariables.filter(
160+
(variable) => variable.key === "MY_VAR"
161+
);
162+
expect(myVarValues).toHaveLength(2);
163+
164+
const prodValue = myVarValues.find(
165+
(variable) => variable.environment.id === prodEnvironment.id
166+
);
167+
expect(prodValue?.value).toBe("prod-value");
168+
169+
const activeBranchValue = myVarValues.find(
170+
(variable) => variable.environment.id === activeBranchEnvironment.id
171+
);
172+
expect(activeBranchValue?.value).toBe("active-branch-value");
173+
expect(activeBranchValue?.environment.branchName).toBe("feature/active");
174+
175+
expect(
176+
myVarValues.some((variable) => variable.environment.id === archivedBranchEnvironment.id)
177+
).toBe(false);
178+
}
179+
);
72180
});

0 commit comments

Comments
 (0)