Files
triggerdotdev--trigger.dev/apps/webapp/app/v3/environmentVariables/environmentVariablesRepository.server.ts
Eric Allam 8dc77c0ccd 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.
2026-06-11 19:30:59 +01:00

1400 lines
40 KiB
TypeScript

import { Prisma, type PrismaClient, type RuntimeEnvironmentType } from "@trigger.dev/database";
import type { AuthenticatedEnvironment } from "@trigger.dev/core/v3/auth/environment";
import { z } from "zod";
import { environmentFullTitle } from "~/components/environments/EnvironmentLabel";
import { $replica, $transaction, prisma, type PrismaReplicaClient } from "~/db.server";
import { env } from "~/env.server";
import { getSecretStore } from "~/services/secrets/secretStore.server";
import { generateFriendlyId } from "../friendlyIdentifiers";
import {
type CreateEnvironmentVariables,
type CreateResult,
type DeleteEnvironmentVariable,
type DeleteEnvironmentVariableValue,
type EditEnvironmentVariable,
type EditEnvironmentVariableValue,
type EnvironmentVariable,
type EnvironmentVariableWithSecret,
type ProjectEnvironmentVariable,
type Repository,
type Result,
} from "./repository";
import { removeBlacklistedVariables } from "../environmentVariableRules.server";
import { deduplicateVariableArray } from "../deduplicateVariableArray.server";
import { logger } from "~/services/logger.server";
function secretKeyProjectPrefix(projectId: string) {
return `environmentvariable:${projectId}:`;
}
function secretKeyEnvironmentPrefix(projectId: string, environmentId: string) {
return `${secretKeyProjectPrefix(projectId)}${environmentId}:`;
}
function secretKey(projectId: string, environmentId: string, key: string) {
return `${secretKeyEnvironmentPrefix(projectId, environmentId)}${key}`;
}
function parseSecretKey(key: string) {
const parts = key.split(":");
return {
projectId: parts[1],
environmentId: parts[2],
key: parts[3],
};
}
const SecretValue = z.object({ secret: z.string() });
export class EnvironmentVariablesRepository implements Repository {
constructor(
private prismaClient: PrismaClient = prisma,
private replicaClient: PrismaReplicaClient = $replica
) {}
async create(projectId: string, options: CreateEnvironmentVariables): Promise<CreateResult> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
environmentVariables: {
select: {
key: true,
values: {
select: {
environment: {
select: { id: true, type: true },
},
},
},
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
if (options.environmentIds.every((v) => !project.environments.some((e) => e.id === v))) {
return { success: false as const, error: `Environment not found` };
}
// Remove `TRIGGER_SECRET_KEY` or `TRIGGER_API_URL`
let values = removeBlacklistedVariables(options.variables);
const removedBlacklisted = values.length !== options.variables.length;
//get rid of empty variables
values = values.filter((v) => v.key.trim() !== "" && v.value.trim() !== "");
if (values.length === 0) {
return {
success: false as const,
error: `You must set at least one valid variable.${
removedBlacklisted ? " All the variables submitted are not allowed." : ""
}`,
};
}
//check if any of them exist in an environment we're setting
if (!options.override) {
const existingVariableKeys: { key: string; environments: RuntimeEnvironmentType[] }[] = [];
for (const variable of values) {
const existingVariable = project.environmentVariables.find((v) => v.key === variable.key);
if (
existingVariable &&
existingVariable.values.some((v) => options.environmentIds.includes(v.environment.id))
) {
existingVariableKeys.push({
key: variable.key,
environments: existingVariable.values
.filter((v) => options.environmentIds.includes(v.environment.id))
.map((v) => v.environment.type),
});
}
}
if (existingVariableKeys.length > 0) {
return {
success: false as const,
error: `Some of the variables are already set for these environments. Set override to true to override them.`,
variableErrors: existingVariableKeys.map((val) => ({
key: val.key,
error: `Variable already set in ${val.environments
.map((e) => environmentFullTitle({ type: e }))
.join(", ")}.`,
})),
};
}
}
try {
for (const variable of values) {
const result = await $transaction(this.prismaClient, "create env var", async (tx) => {
const environmentVariable = await tx.environmentVariable.upsert({
where: {
projectId_key: {
key: variable.key,
projectId,
},
},
create: {
key: variable.key,
friendlyId: generateFriendlyId("envvar"),
project: {
connect: {
id: projectId,
},
},
},
update: {},
});
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
// If parentEnvironmentId is provided and isSecret is not explicitly set,
// look up if the parent has this variable marked as secret
let inheritedIsSecret: boolean | undefined = undefined;
if (options.isSecret === undefined && options.parentEnvironmentId) {
const parentVariableValue = await tx.environmentVariableValue.findFirst({
where: {
variableId: environmentVariable.id,
environmentId: options.parentEnvironmentId,
},
select: {
isSecret: true,
},
});
if (parentVariableValue?.isSecret) {
inheritedIsSecret = true;
}
}
const effectiveIsSecret = options.isSecret ?? inheritedIsSecret;
//set the secret values and references
for (const environmentId of options.environmentIds) {
const key = secretKey(projectId, environmentId, variable.key);
const existingValueRecord = await tx.environmentVariableValue.findFirst({
where: {
variableId: environmentVariable.id,
environmentId,
},
});
// Check if value already exists and is the same, and no metadata change (e.g. isSecret toggle)
const existingSecret = await secretStore.getSecret(SecretValue, key);
const canSkip =
existingSecret &&
existingSecret.secret === variable.value &&
existingValueRecord &&
(options.isSecret === undefined ||
existingValueRecord.isSecret === options.isSecret);
if (canSkip) {
continue;
}
//create the secret reference
const secretReference = await tx.secretReference.upsert({
where: {
key,
},
create: {
key,
provider: "DATABASE",
},
update: {},
});
if (existingValueRecord) {
await tx.environmentVariableValue.update({
where: {
id: existingValueRecord.id,
},
data: {
version: {
increment: 1,
},
...(options.lastUpdatedBy ? { lastUpdatedBy: options.lastUpdatedBy } : {}),
valueReferenceId: secretReference.id,
...(options.isSecret !== undefined
? {
isSecret: options.isSecret,
}
: {}),
},
});
} else {
await tx.environmentVariableValue.create({
data: {
variableId: environmentVariable.id,
environmentId: environmentId,
valueReferenceId: secretReference.id,
isSecret: effectiveIsSecret,
version: 1,
lastUpdatedBy: options.lastUpdatedBy ? options.lastUpdatedBy : Prisma.JsonNull,
},
});
}
await secretStore.setSecret<{ secret: string }>(key, {
secret: variable.value,
});
}
});
}
return {
success: true as const,
};
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// The error code for unique constraint violation in Prisma is P2002
if (error.code === "P2002") {
return {
success: false as const,
error: `There was already an existing field`,
};
}
}
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async edit(projectId: string, options: EditEnvironmentVariable): Promise<Result> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
if (options.values.every((v) => !project.environments.some((e) => e.id === v.environmentId))) {
return { success: false as const, error: `Environment not found` };
}
//get rid of empty strings
let values = options.values.filter((v) => v.value.trim() !== "");
//add in empty values for environments that don't have a value
const environmentIds = project.environments.map((e) => e.id);
if (!options.keepEmptyValues) {
for (const environmentId of environmentIds) {
if (!values.some((v) => v.environmentId === environmentId)) {
values.push({
environmentId,
value: "",
});
}
}
}
const environmentVariable = await this.prismaClient.environmentVariable.findFirst({
select: {
id: true,
key: true,
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
try {
await $transaction(this.prismaClient, "edit env var", async (tx) => {
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//create the secret values and references
for (const value of values) {
const key = secretKey(projectId, value.environmentId, environmentVariable.key);
const existingValue = await tx.environmentVariableValue.findFirst({
where: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
},
});
if (existingValue && existingValue.valueReferenceId) {
if (value.value === "") {
//delete the value
await secretStore.deleteSecret(key);
await tx.secretReference.delete({
where: {
id: existingValue.valueReferenceId,
},
});
await tx.environmentVariableValue.delete({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
},
},
});
} else {
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
await tx.environmentVariableValue.update({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
},
},
data: {
version: {
increment: 1,
},
lastUpdatedBy: options.lastUpdatedBy ? options.lastUpdatedBy : undefined,
},
});
}
continue;
}
//create the secret reference
const secretReference = await tx.secretReference.create({
data: {
key,
provider: "DATABASE",
},
});
const variableValue = await tx.environmentVariableValue.create({
data: {
variableId: environmentVariable.id,
environmentId: value.environmentId,
valueReferenceId: secretReference.id,
version: 1,
lastUpdatedBy: options.lastUpdatedBy ? options.lastUpdatedBy : Prisma.JsonNull,
},
});
await secretStore.setSecret<{ secret: string }>(key, {
secret: value.value,
});
}
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async editValue(projectId: string, options: EditEnvironmentVariableValue): Promise<Result> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
if (!project.environments.some((e) => e.id === options.environmentId)) {
return { success: false as const, error: "Environment not found" };
}
const environmentVariable = await this.prismaClient.environmentVariable.findFirst({
select: {
id: true,
key: true,
values: {
where: {
environmentId: options.environmentId,
},
select: {
valueReferenceId: true,
},
},
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
if (environmentVariable.values.length === 0) {
return { success: false as const, error: "Environment variable value not found" };
}
try {
await $transaction(this.prismaClient, "edit env var value", async (tx) => {
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
const key = secretKey(projectId, options.environmentId, environmentVariable.key);
await secretStore.setSecret<{ secret: string }>(key, {
secret: options.value,
});
await tx.environmentVariableValue.update({
where: {
variableId_environmentId: {
variableId: environmentVariable.id,
environmentId: options.environmentId,
},
},
data: {
version: {
increment: 1,
},
lastUpdatedBy: options.lastUpdatedBy ? options.lastUpdatedBy : undefined,
},
});
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async getProject(projectId: string): Promise<ProjectEnvironmentVariable[]> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
type: true,
},
},
},
});
if (!project) {
return [];
}
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
});
const secrets = await secretStore.getSecrets(SecretValue, secretKeyProjectPrefix(projectId));
const values = secrets.map((secret) => {
const { projectId, environmentId, key } = parseSecretKey(secret.key);
return {
projectId,
environmentId,
key,
value: secret.value.secret,
};
});
//now group the values together by key and environment ID into ProjectEnvironmentVariable[]
//and add the type of environment to the result
const results: ProjectEnvironmentVariable[] = [];
for (const value of values) {
const environment = project.environments.find((e) => e.id === value.environmentId);
if (!environment) {
throw new Error("Environment not found");
}
const existing = results.find((r) => r.key === value.key);
if (existing) {
existing.values.push({
value: value.value,
environment: {
id: value.environmentId,
type: environment.type,
},
});
} else {
results.push({
key: value.key,
values: [
{
value: value.value,
environment: {
id: value.environmentId,
type: environment.type,
},
},
],
});
}
}
return results;
}
async getVariableValuesForKeys(
projectId: string,
items: Array<{ environmentId: string; key: string }>
): Promise<Map<string, string>> {
if (items.length === 0) {
return new Map();
}
const uniqueItems = new Map<string, { environmentId: string; key: string }>();
for (const item of items) {
uniqueItems.set(`${item.environmentId}:${item.key}`, item);
}
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.replicaClient,
});
const storeKeys = Array.from(uniqueItems.values()).map((item) =>
secretKey(projectId, item.environmentId, item.key)
);
const secrets = await secretStore.getSecretsByKeys(SecretValue, storeKeys);
const secretsByStoreKey = new Map(secrets.map((secret) => [secret.key, secret.value.secret]));
const values = new Map<string, string>();
for (const item of uniqueItems.values()) {
const storeKey = secretKey(projectId, item.environmentId, item.key);
const value = secretsByStoreKey.get(storeKey);
if (value !== undefined) {
values.set(`${item.environmentId}:${item.key}`, value);
}
}
return values;
}
async getEnvironmentWithRedactedSecrets(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
): Promise<EnvironmentVariableWithSecret[]> {
const variables = await this.getEnvironment(projectId, environmentId, parentEnvironmentId);
// Get the keys of all secret variables
const secretValues = await this.replicaClient.environmentVariableValue.findMany({
where: {
environmentId: parentEnvironmentId
? { in: [environmentId, parentEnvironmentId] }
: environmentId,
isSecret: true,
},
select: {
variable: {
select: {
key: true,
},
},
},
});
const secretVarKeys = secretValues.map((r) => r.variable.key);
// Filter out secret variables
return variables.map((v) => {
if (secretVarKeys.includes(v.key)) {
return {
key: v.key,
value: "<redacted>",
isSecret: true,
};
}
return { key: v.key, value: v.value, isSecret: false };
});
}
async getEnvironment(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
): Promise<EnvironmentVariable[]> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
where: {
id: environmentId,
},
},
},
});
if (!project || project.environments.length === 0) {
return [];
}
return this.getEnvironmentVariables(projectId, environmentId, parentEnvironmentId);
}
async #getSecretEnvironmentVariables(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
): Promise<EnvironmentVariable[]> {
const secretStore = getSecretStore("DATABASE", {
prismaClient: this.prismaClient,
});
const parentSecrets = parentEnvironmentId
? await secretStore.getSecrets(
SecretValue,
secretKeyEnvironmentPrefix(projectId, parentEnvironmentId)
)
: [];
const childSecrets = await secretStore.getSecrets(
SecretValue,
secretKeyEnvironmentPrefix(projectId, environmentId)
);
// Merge the secrets, we want child ones to override parent ones
const mergedSecrets = new Map<string, string>();
for (const secret of parentSecrets) {
const { key: parsedKey } = parseSecretKey(secret.key);
mergedSecrets.set(parsedKey, secret.value.secret);
}
for (const secret of childSecrets) {
const { key: parsedKey } = parseSecretKey(secret.key);
mergedSecrets.set(parsedKey, secret.value.secret);
}
const merged = Array.from(mergedSecrets.entries()).map(([key, value]) => {
return {
key,
value,
};
});
return removeBlacklistedVariables(merged);
}
async getEnvironmentVariables(
projectId: string,
environmentId: string,
parentEnvironmentId?: string
): Promise<EnvironmentVariable[]> {
return this.#getSecretEnvironmentVariables(projectId, environmentId, parentEnvironmentId);
}
async delete(projectId: string, options: DeleteEnvironmentVariable): Promise<Result> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
const environmentVariable = await this.prismaClient.environmentVariable.findFirst({
select: {
id: true,
key: true,
values: {
select: {
id: true,
environmentId: true,
valueReference: {
select: {
key: true,
},
},
},
},
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
try {
await $transaction(this.prismaClient, "delete env var", async (tx) => {
await tx.environmentVariable.delete({
where: {
id: options.id,
},
});
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
//delete the secret values and references
for (const value of environmentVariable.values) {
const key = secretKey(projectId, value.environmentId, environmentVariable.key);
await secretStore.deleteSecret(key);
if (value.valueReference) {
await tx.secretReference.delete({
where: {
key: value.valueReference.key,
},
});
}
}
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
async deleteValue(projectId: string, options: DeleteEnvironmentVariableValue): Promise<Result> {
const project = await this.prismaClient.project.findFirst({
where: {
id: projectId,
deletedAt: null,
},
select: {
environments: {
select: {
id: true,
},
},
},
});
if (!project) {
return { success: false as const, error: "Project not found" };
}
const environmentVariable = await this.prismaClient.environmentVariable.findFirst({
select: {
id: true,
key: true,
values: {
select: {
id: true,
environmentId: true,
valueReference: {
select: {
key: true,
},
},
},
},
},
where: {
id: options.id,
},
});
if (!environmentVariable) {
return { success: false as const, error: "Environment variable not found" };
}
const value = environmentVariable.values.find((v) => v.environmentId === options.environmentId);
if (!value) {
return { success: false as const, error: "Environment variable value not found" };
}
// If this is the last value, delete the whole variable
if (environmentVariable.values.length === 1) {
return this.delete(projectId, { id: options.id });
}
try {
await $transaction(this.prismaClient, "delete env var value", async (tx) => {
const secretStore = getSecretStore("DATABASE", {
prismaClient: tx,
});
const key = secretKey(projectId, options.environmentId, environmentVariable.key);
await secretStore.deleteSecret(key);
if (value.valueReference) {
await tx.secretReference.delete({
where: {
key: value.valueReference.key,
},
});
}
await tx.environmentVariableValue.delete({
where: {
id: value.id,
},
});
});
return {
success: true as const,
};
} catch (error) {
return {
success: false as const,
error: error instanceof Error ? error.message : "Something went wrong",
};
}
}
}
export const RuntimeEnvironmentForEnvRepoPayload = {
select: {
id: true,
slug: true,
type: true,
projectId: true,
apiKey: true,
organizationId: true,
branchName: true,
builtInEnvironmentVariableOverrides: true,
},
} as const;
// Derived from the slim AuthenticatedEnvironment so a full AE satisfies
// this type — the legacy Prisma payload had `builtInEnvironmentVariableOverrides`
// as Prisma's JsonValue, which is a subtype of `unknown` in the slim
// shape, causing assignability errors in the JWT/queue paths that pass
// AE values straight through. Using Pick<AE, ...> aligns them.
export type RuntimeEnvironmentForEnvRepo = Pick<
AuthenticatedEnvironment,
| "id"
| "slug"
| "type"
| "projectId"
| "apiKey"
| "organizationId"
| "branchName"
| "builtInEnvironmentVariableOverrides"
>;
export const environmentVariablesRepository = new EnvironmentVariablesRepository();
export async function resolveVariablesForEnvironment(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo,
parentEnvironment?: RuntimeEnvironmentForEnvRepo
) {
let projectSecrets = await environmentVariablesRepository.getEnvironmentVariables(
runtimeEnvironment.projectId,
runtimeEnvironment.id,
parentEnvironment?.id
);
projectSecrets = renameVariables(projectSecrets, {
OTEL_RESOURCE_ATTRIBUTES: "CUSTOM_OTEL_RESOURCE_ATTRIBUTES",
});
const overridableTriggerVariables = await resolveOverridableTriggerVariables(runtimeEnvironment);
const builtInVariables =
runtimeEnvironment.type === "DEVELOPMENT"
? await resolveBuiltInDevVariables(runtimeEnvironment)
: await resolveBuiltInProdVariables(runtimeEnvironment, parentEnvironment);
const overridableOtelVariables =
runtimeEnvironment.type === "DEVELOPMENT"
? await resolveOverridableOtelDevVariables(runtimeEnvironment)
: [];
const result = deduplicateVariableArray([
...overridableTriggerVariables,
...overridableOtelVariables,
...projectSecrets,
...builtInVariables,
]);
return result;
}
function renameVariables(variables: EnvironmentVariable[], renameMap: Record<string, string>) {
return variables.map((variable) => {
return {
...variable,
key: renameMap[variable.key] ?? variable.key,
};
});
}
async function resolveOverridableTriggerVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo
) {
let result: Array<EnvironmentVariable> = [
{
key: "TRIGGER_REALTIME_STREAM_VERSION",
value: env.REALTIME_STREAM_VERSION,
},
];
return result;
}
async function resolveBuiltInDevVariables(runtimeEnvironment: RuntimeEnvironmentForEnvRepo) {
let result: Array<EnvironmentVariable> = [
{
key: "TRIGGER_OTEL_EXPORTER_OTLP_ENDPOINT",
value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? `${env.APP_ORIGIN.replace(/\/$/, "")}/otel`,
},
{
key: "TRIGGER_API_URL",
value: env.API_ORIGIN ?? env.APP_ORIGIN,
},
{
key: "TRIGGER_STREAM_URL",
value: env.STREAM_ORIGIN ?? env.API_ORIGIN ?? env.APP_ORIGIN,
},
];
if (env.DEV_OTEL_METRICS_ENDPOINT) {
result.push({
key: "TRIGGER_OTEL_METRICS_ENDPOINT",
value: env.DEV_OTEL_METRICS_ENDPOINT,
});
}
if (env.DEV_OTEL_METRICS_EXPORT_INTERVAL_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_EXPORT_INTERVAL_MILLIS",
value: env.DEV_OTEL_METRICS_EXPORT_INTERVAL_MILLIS,
});
}
if (env.DEV_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS,
});
}
if (env.DEV_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS",
value: env.DEV_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS,
});
}
if (env.DEV_OTEL_BATCH_PROCESSING_ENABLED === "1") {
result = result.concat([
{
key: "TRIGGER_OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "TRIGGER_OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "TRIGGER_OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "TRIGGER_OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "TRIGGER_OTEL_LOG_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_LOG_MAX_QUEUE_SIZE,
},
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.DEV_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.DEV_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.DEV_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.DEV_OTEL_LOG_MAX_QUEUE_SIZE,
},
]);
}
const commonVariables = await resolveCommonBuiltInVariables(runtimeEnvironment);
return [...result, ...commonVariables];
}
async function resolveOverridableOtelDevVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo
) {
let result: Array<EnvironmentVariable> = [
{
key: "OTEL_EXPORTER_OTLP_ENDPOINT",
value: env.DEV_OTEL_EXPORTER_OTLP_ENDPOINT ?? `${env.APP_ORIGIN.replace(/\/$/, "")}/otel`,
},
];
return result;
}
async function resolveBuiltInProdVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo,
parentEnvironment?: RuntimeEnvironmentForEnvRepo
) {
let result: Array<EnvironmentVariable> = [
{
key: "TRIGGER_SECRET_KEY",
value: parentEnvironment?.apiKey ?? runtimeEnvironment.apiKey,
},
{
key: "TRIGGER_API_URL",
value: env.API_ORIGIN ?? env.APP_ORIGIN,
},
{
key: "TRIGGER_STREAM_URL",
value: env.STREAM_ORIGIN ?? env.API_ORIGIN ?? env.APP_ORIGIN,
},
{
key: "TRIGGER_RUNTIME_WAIT_THRESHOLD_IN_MS",
value: String(env.CHECKPOINT_THRESHOLD_IN_MS),
},
{
key: "TRIGGER_ORG_ID",
value: runtimeEnvironment.organizationId,
},
];
if (runtimeEnvironment.branchName) {
result = result.concat([
{
key: "TRIGGER_PREVIEW_BRANCH",
value: runtimeEnvironment.branchName,
},
]);
}
if (env.PROD_OTEL_METRICS_EXPORT_INTERVAL_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_EXPORT_INTERVAL_MILLIS",
value: env.PROD_OTEL_METRICS_EXPORT_INTERVAL_MILLIS,
});
}
if (env.PROD_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_METRICS_EXPORT_TIMEOUT_MILLIS,
});
}
if (env.PROD_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS) {
result.push({
key: "TRIGGER_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS",
value: env.PROD_OTEL_METRICS_COLLECTION_INTERVAL_MILLIS,
});
}
if (env.PROD_OTEL_BATCH_PROCESSING_ENABLED === "1") {
result = result.concat([
{
key: "TRIGGER_OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "TRIGGER_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "TRIGGER_OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "TRIGGER_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "TRIGGER_OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "TRIGGER_OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "TRIGGER_OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "TRIGGER_OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "TRIGGER_OTEL_LOG_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_LOG_MAX_QUEUE_SIZE,
},
{
key: "OTEL_BATCH_PROCESSING_ENABLED",
value: "1",
},
{
key: "OTEL_SPAN_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_SPAN_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_SPAN_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_SPAN_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_SPAN_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_SPAN_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_SPAN_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_SPAN_MAX_QUEUE_SIZE,
},
{
key: "OTEL_LOG_MAX_EXPORT_BATCH_SIZE",
value: env.PROD_OTEL_LOG_MAX_EXPORT_BATCH_SIZE,
},
{
key: "OTEL_LOG_SCHEDULED_DELAY_MILLIS",
value: env.PROD_OTEL_LOG_SCHEDULED_DELAY_MILLIS,
},
{
key: "OTEL_LOG_EXPORT_TIMEOUT_MILLIS",
value: env.PROD_OTEL_LOG_EXPORT_TIMEOUT_MILLIS,
},
{
key: "OTEL_LOG_MAX_QUEUE_SIZE",
value: env.PROD_OTEL_LOG_MAX_QUEUE_SIZE,
},
]);
}
if (env.PROD_USAGE_HEARTBEAT_INTERVAL_MS && env.USAGE_EVENT_URL) {
result = result.concat([
{
key: "USAGE_HEARTBEAT_INTERVAL_MS",
value: String(env.PROD_USAGE_HEARTBEAT_INTERVAL_MS),
},
{
key: "USAGE_EVENT_URL",
value: env.USAGE_EVENT_URL,
},
]);
}
if (env.PROD_TASK_HEARTBEAT_INTERVAL_MS) {
result = result.concat([
{
key: "HEARTBEAT_INTERVAL_MS",
value: String(env.PROD_TASK_HEARTBEAT_INTERVAL_MS),
},
]);
}
const commonVariables = await resolveCommonBuiltInVariables(runtimeEnvironment);
return [...result, ...commonVariables];
}
async function resolveCommonBuiltInVariables(
runtimeEnvironment: RuntimeEnvironmentForEnvRepo
): Promise<Array<EnvironmentVariable>> {
return [
{
key: "TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT)
),
},
{
key: "TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_LOG_ATTRIBUTE_COUNT_LIMIT)
),
},
{
key: "TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT)
),
},
{
key: "TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_LOG_ATTRIBUTE_VALUE_LENGTH_LIMIT)
),
},
{
key: "TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_SPAN_EVENT_COUNT_LIMIT)
),
},
{
key: "TRIGGER_OTEL_LINK_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_LINK_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_LINK_COUNT_LIMIT)
),
},
{
key: "TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_ATTRIBUTE_PER_LINK_COUNT_LIMIT)
),
},
{
key: "TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT",
runtimeEnvironment,
String(env.TRIGGER_OTEL_ATTRIBUTE_PER_EVENT_COUNT_LIMIT)
),
},
{
key: "TRIGGER_WAIT_UNTIL_TIMEOUT_MS",
value: resolveBuiltInEnvironmentVariableOverrides(
"TRIGGER_WAIT_UNTIL_TIMEOUT_MS",
runtimeEnvironment,
String(env.WAIT_UNTIL_TIMEOUT_MS)
),
},
];
}
function resolveBuiltInEnvironmentVariableOverrides(
key: string,
runtimeEnvironment: RuntimeEnvironmentForEnvRepo,
defaultValue: string
) {
const overrides = runtimeEnvironment.builtInEnvironmentVariableOverrides;
if (!overrides) {
return defaultValue;
}
if (
!Array.isArray(overrides) &&
typeof overrides === "object" &&
overrides !== null &&
key in overrides
) {
const value = (overrides as Record<string, unknown>)[key];
if (typeof value === "string") {
return value;
}
}
return defaultValue;
}