diff --git a/.server-changes/env-vars-page-scope-values-to-visible-environments.md b/.server-changes/env-vars-page-scope-values-to-visible-environments.md new file mode 100644 index 00000000000..067c04661b7 --- /dev/null +++ b/.server-changes/env-vars-page-scope-values-to-visible-environments.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: fix +--- + +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. diff --git a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts index 2ab7f1f5a1b..720bfda8db7 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentVariablesPresenter.server.ts @@ -1,4 +1,4 @@ -import { PrismaClient, prisma } from "~/db.server"; +import { $replica, PrismaClient, PrismaReplicaClient, prisma } from "~/db.server"; import { Project } from "~/models/project.server"; import { User } from "~/models/user.server"; import { EnvironmentVariablesRepository } from "~/v3/environmentVariables/environmentVariablesRepository.server"; @@ -15,13 +15,15 @@ export type EnvironmentVariableWithSetValues = Result["environmentVariables"][nu export class EnvironmentVariablesPresenter { #prismaClient: PrismaClient; + #replicaClient: PrismaReplicaClient; - constructor(prismaClient: PrismaClient = prisma) { + constructor(prismaClient: PrismaClient = prisma, replicaClient: PrismaReplicaClient = $replica) { this.#prismaClient = prismaClient; + this.#replicaClient = replicaClient; } public async call({ userId, projectSlug }: { userId: User["id"]; projectSlug: Project["slug"] }) { - const project = await this.#prismaClient.project.findFirst({ + const project = await this.#replicaClient.project.findFirst({ select: { id: true, }, @@ -41,7 +43,18 @@ export class EnvironmentVariablesPresenter { throw new Error("Project not found"); } - const environmentVariables = await this.#prismaClient.environmentVariable.findMany({ + const { environments: sortedEnvironments, hasStaging } = + await loadEnvironmentVariablesEnvironments( + this.#replicaClient, + { userId, projectId: project.id }, + { skipProjectAccessCheck: true } + ); + + // Only load values for the environments we display. Projects can accumulate + // values in archived branch environments, which would otherwise all be loaded here. + const environmentIds = sortedEnvironments.map((env) => env.id); + + const environmentVariables = await this.#replicaClient.environmentVariable.findMany({ select: { id: true, key: true, @@ -59,6 +72,11 @@ export class EnvironmentVariablesPresenter { }, isSecret: true, }, + where: { + environmentId: { + in: environmentIds, + }, + }, }, }, where: { @@ -84,7 +102,7 @@ export class EnvironmentVariablesPresenter { const users = userIds.size > 0 - ? await this.#prismaClient.user.findMany({ + ? await this.#replicaClient.user.findMany({ where: { id: { in: Array.from(userIds), @@ -102,14 +120,7 @@ export class EnvironmentVariablesPresenter { const usersRecord: Record = Object.fromEntries(users.map((u) => [u.id, u])); - const { environments: sortedEnvironments, hasStaging } = - await loadEnvironmentVariablesEnvironments( - this.#prismaClient, - { userId, projectId: project.id }, - { skipProjectAccessCheck: true } - ); - - const repository = new EnvironmentVariablesRepository(this.#prismaClient); + const repository = new EnvironmentVariablesRepository(this.#prismaClient, this.#replicaClient); const nonSecretItems: Array<{ environmentId: string; key: string }> = []; for (const environmentVariable of environmentVariables) { diff --git a/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts b/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts index 218d0be7eb6..9473127df05 100644 --- a/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts +++ b/apps/webapp/app/presenters/v3/environmentVariablesEnvironments.server.ts @@ -1,5 +1,5 @@ import type { RuntimeEnvironmentType } from "@trigger.dev/database"; -import { type PrismaClient } from "~/db.server"; +import { type PrismaReplicaClient } from "~/db.server"; import { filterOrphanedEnvironments, sortEnvironments } from "~/utils/environmentSort"; export type EnvironmentVariablesEnvironment = { @@ -15,7 +15,7 @@ export type EnvironmentVariablesEnvironmentsResult = { }; export async function loadEnvironmentVariablesEnvironments( - prismaClient: PrismaClient, + prismaClient: PrismaReplicaClient, { userId, projectId }: { userId: string; projectId: string }, options?: { skipProjectAccessCheck?: boolean } ): Promise { diff --git a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts index 4a8e0e36c0a..68f9aa0f3b3 100644 --- a/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts +++ b/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts @@ -591,7 +591,7 @@ export class EnvironmentVariablesRepository implements Repository { } const secretStore = getSecretStore("DATABASE", { - prismaClient: this.prismaClient, + prismaClient: this.replicaClient, }); const storeKeys = Array.from(uniqueItems.values()).map((item) => diff --git a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts index 1fd2c9e9619..b5172bd5948 100644 --- a/apps/webapp/test/EnvironmentVariablesPresenter.test.ts +++ b/apps/webapp/test/EnvironmentVariablesPresenter.test.ts @@ -56,7 +56,7 @@ describe("EnvironmentVariablesPresenter", () => { userId: user.id, }); - const result = await new EnvironmentVariablesPresenter(prisma).call({ + const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ userId: user.id, projectSlug, }); @@ -69,4 +69,112 @@ describe("EnvironmentVariablesPresenter", () => { expect(secretVariable!.value).toBe(""); expect(nonSecretVariable!.value).toBe("plain-value"); }); + + postgresTest( + "returns values for active environments (including branch environments) and excludes archived branch environments", + async ({ prisma }) => { + const { user, organization, project, projectSlug } = + await createTestOrgProjectWithMember(prisma); + + const prodEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PRODUCTION", + }); + + const parentPreviewEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: parentPreviewEnvironment.id }, + data: { isBranchableEnvironment: true }, + }); + + const activeBranchEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: activeBranchEnvironment.id }, + data: { + parentEnvironmentId: parentPreviewEnvironment.id, + branchName: "feature/active", + }, + }); + + const archivedBranchEnvironment = await createRuntimeEnvironment(prisma, { + projectId: project.id, + organizationId: organization.id, + type: "PREVIEW", + }); + await prisma.runtimeEnvironment.update({ + where: { id: archivedBranchEnvironment.id }, + data: { + parentEnvironmentId: parentPreviewEnvironment.id, + branchName: "feature/archived", + }, + }); + + const repository = new EnvironmentVariablesRepository(prisma, prisma); + + await createEnvironmentVariable(repository, project.id, { + environmentId: prodEnvironment.id, + key: "MY_VAR", + value: "prod-value", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: activeBranchEnvironment.id, + key: "MY_VAR", + value: "active-branch-value", + userId: user.id, + }); + await createEnvironmentVariable(repository, project.id, { + environmentId: archivedBranchEnvironment.id, + key: "MY_VAR", + value: "archived-branch-value", + userId: user.id, + }); + + // Archive the branch after it accumulated values (archiving does not + // delete its EnvironmentVariableValue rows). + await prisma.runtimeEnvironment.update({ + where: { id: archivedBranchEnvironment.id }, + data: { archivedAt: new Date() }, + }); + + const result = await new EnvironmentVariablesPresenter(prisma, prisma).call({ + userId: user.id, + projectSlug, + }); + + const environmentIds = result.environments.map((environment) => environment.id); + expect(environmentIds).toContain(prodEnvironment.id); + expect(environmentIds).toContain(activeBranchEnvironment.id); + expect(environmentIds).not.toContain(archivedBranchEnvironment.id); + + const myVarValues = result.environmentVariables.filter( + (variable) => variable.key === "MY_VAR" + ); + expect(myVarValues).toHaveLength(2); + + const prodValue = myVarValues.find( + (variable) => variable.environment.id === prodEnvironment.id + ); + expect(prodValue?.value).toBe("prod-value"); + + const activeBranchValue = myVarValues.find( + (variable) => variable.environment.id === activeBranchEnvironment.id + ); + expect(activeBranchValue?.value).toBe("active-branch-value"); + expect(activeBranchValue?.environment.branchName).toBe("feature/active"); + + expect( + myVarValues.some((variable) => variable.environment.id === archivedBranchEnvironment.id) + ).toBe(false); + } + ); });