Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .cursor/rules/webapp.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ alwaysApply: false

The main trigger.dev webapp, which powers it's API and dashboard and makes up the docker image that is produced as an OSS image, is a Remix 2.1.0 app that uses an express server, written in TypeScript. The following subsystems are either included in the webapp or are used by the webapp in another part of the monorepo:

- `@trigger.dev/database` exports a Prisma 5.4.1 client that is used extensively in the webapp to access a PostgreSQL instance. The schema file is [schema.prisma](mdc:internal-packages/database/prisma/schema.prisma)
- `@trigger.dev/database` exports a Prisma 6.14.0 client that is used extensively in the webapp to access a PostgreSQL instance. The schema file is [schema.prisma](mdc:internal-packages/database/prisma/schema.prisma)
- `@trigger.dev/core` is a published package and is used to share code between the `@trigger.dev/sdk` and the webapp. It includes functionality but also a load of Zod schemas for data validation. When importing from `@trigger.dev/core` in the webapp, we never import the root `@trigger.dev/core` path, instead we favor one of the subpath exports that you can find in [package.json](mdc:packages/core/package.json)
- `@internal/run-engine` has all the code needed to trigger a run and take it through it's lifecycle to completion.
- `@trigger.dev/redis-worker` is a custom redis based background job/worker system that's used in the webapp and also used inside the run engine.
Expand All @@ -31,7 +31,10 @@ We originally the Trigger.dev "Run Engine" not as a single system, but just spre
- The batch trigger API endpoint is [api.v1.tasks.batch.ts](mdc:apps/webapp/app/routes/api.v1.tasks.batch.ts)
- Setup code for the prisma client is in [db.server.ts](mdc:apps/webapp/app/db.server.ts)
- The run engine is configured in [runEngine.server.ts](mdc:apps/webapp/app/v3/runEngine.server.ts)
- All the "services" that are found in app/v3/services/**/*.server.ts
- All the "services" that are found in app/v3/services/\*_/_.server.ts
- The code for the TaskEvent data, which is the otel data sent from tasks to our servers, is in both the [eventRepository.server.ts](mdc:apps/webapp/app/v3/eventRepository.server.ts) and also the [otlpExporter.server.ts](mdc:apps/webapp/app/v3/otlpExporter.server.ts). The otel endpoints which are hit from production and development otel exporters is [otel.v1.logs.ts](mdc:apps/webapp/app/routes/otel.v1.logs.ts) and [otel.v1.traces.ts](mdc:apps/webapp/app/routes/otel.v1.traces.ts)
- We use "presenters" to move more complex loader code into a class, and you can find those are app/v3/presenters/**/*.server.ts
- We use "presenters" to move more complex loader code into a class, and you can find those are app/v3/presenters/\*_/_.server.ts

- All the "services" that are found in app/v3/services/\*_/_.server.ts
- The code for the TaskEvent data, which is the otel data sent from tasks to our servers, is in both the [eventRepository.server.ts](mdc:apps/webapp/app/v3/eventRepository.server.ts) and also the [otlpExporter.server.ts](mdc:apps/webapp/app/v3/otlpExporter.server.ts). The otel endpoints which are hit from production and development otel exporters is [otel.v1.logs.ts](mdc:apps/webapp/app/routes/otel.v1.logs.ts) and [otel.v1.traces.ts](mdc:apps/webapp/app/routes/otel.v1.traces.ts)
- We use "presenters" to move more complex loader code into a class, and you can find those are app/v3/presenters/\*_/_.server.ts
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
**/dist
**/node_modules

**/generated/prisma

apps/webapp/build
apps/webapp/public/build

Expand Down
41 changes: 32 additions & 9 deletions apps/webapp/app/db.server.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {
Prisma,
PrismaClient,
PrismaClientOrTransaction,
PrismaReplicaClient,
PrismaTransactionClient,
PrismaTransactionOptions,
$transaction as transac,
type PrismaClientOrTransaction,
type PrismaReplicaClient,
type PrismaTransactionClient,
type PrismaTransactionOptions,
} from "@trigger.dev/database";
import invariant from "tiny-invariant";
import { z } from "zod";
import { env } from "./env.server";
import { logger } from "./services/logger.server";
import { isValidDatabaseUrl } from "./utils/db";
import { singleton } from "./utils/singleton";
import { $transaction as transac } from "@trigger.dev/database";
import { startActiveSpan } from "./v3/tracer.server";
import { Span } from "@opentelemetry/api";
import { queryPerformanceMonitor } from "./utils/queryPerformanceMonitor.server";

export type {
PrismaTransactionClient,
Expand Down Expand Up @@ -153,13 +154,19 @@ function getClient() {
},
]
: []) satisfies Prisma.LogDefinition[]),
// verbose
...((process.env.VERBOSE_PRISMA_LOGS === "1"
// Query performance monitoring
...((process.env.VERBOSE_PRISMA_LOGS === "1" ||
process.env.VERY_SLOW_QUERY_THRESHOLD_MS !== undefined
? [
{
emit: "event",
level: "query",
},
]
: []) satisfies Prisma.LogDefinition[]),
// verbose
...((process.env.VERBOSE_PRISMA_LOGS === "1"
? [
{
emit: "stdout",
level: "query",
Expand Down Expand Up @@ -206,6 +213,11 @@ function getClient() {
});
}

// Add query performance monitoring
client.$on("query", (log) => {
queryPerformanceMonitor.onQuery("writer", log);
});

// connect eagerly
client.$connect();

Expand Down Expand Up @@ -265,13 +277,19 @@ function getReplicaClient() {
},
]
: []) satisfies Prisma.LogDefinition[]),
// verbose
...((process.env.VERBOSE_PRISMA_LOGS === "1"
// Query performance monitoring
...((process.env.VERBOSE_PRISMA_LOGS === "1" ||
process.env.VERY_SLOW_QUERY_THRESHOLD_MS !== undefined
? [
{
emit: "event",
level: "query",
},
]
: []) satisfies Prisma.LogDefinition[]),
// verbose
...((process.env.VERBOSE_PRISMA_LOGS === "1"
? [
{
emit: "stdout",
level: "query",
Expand Down Expand Up @@ -317,6 +335,11 @@ function getReplicaClient() {
});
}

// Add query performance monitoring for replica client
replicaClient.$on("query", (log) => {
queryPerformanceMonitor.onQuery("replica", log);
});

// connect eagerly
replicaClient.$connect();

Expand Down
2 changes: 2 additions & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,8 @@ const EnvironmentSchema = z.object({
AI_RUN_FILTER_MODEL: z.string().optional(),

EVENT_LOOP_MONITOR_THRESHOLD_MS: z.coerce.number().int().default(100),

VERY_SLOW_QUERY_THRESHOLD_MS: z.coerce.number().int().optional(),
});

export type Environment = z.infer<typeof EnvironmentSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
});
}

function decompressContent(compressedBuffer: Buffer): string {
// First, we need to decode the base64 Buffer to get the actual compressed data
const decodedBuffer = Buffer.from(compressedBuffer.toString("utf-8"), "base64");
function decompressContent(compressedBuffer: Uint8Array): string {
// Convert Uint8Array to Buffer and decode base64 in one step
const decodedBuffer = Buffer.from(Buffer.from(compressedBuffer).toString("utf-8"), "base64");

// Decompress the data
const decompressedData = zlib.inflateSync(decodedBuffer);
Expand Down
64 changes: 64 additions & 0 deletions apps/webapp/app/utils/queryPerformanceMonitor.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { env } from "~/env.server";
import { logger } from "~/services/logger.server";

export interface QueryPerformanceConfig {
verySlowQueryThreshold?: number; // ms
maxQueryLogLength: number;
}

export class QueryPerformanceMonitor {
private config: QueryPerformanceConfig;

constructor(config: Partial<QueryPerformanceConfig> = {}) {
this.config = {
maxQueryLogLength: 1000,
...config,
};
}

onQuery(
clientType: "writer" | "replica",
log: {
duration: number;
query: string;
params: string;
target: string;
timestamp: Date;
}
) {
if (this.config.verySlowQueryThreshold === undefined) {
return;
}

const { duration, query, params, target, timestamp } = log;

// Only log very slow queries as errors
if (duration > this.config.verySlowQueryThreshold) {
// Truncate long queries for readability
const truncatedQuery =
query.length > this.config.maxQueryLogLength
? query.substring(0, this.config.maxQueryLogLength) + "..."
: query;

logger.error("Prisma: very slow database query", {
clientType,
durationMs: duration,
query: truncatedQuery,
target,
timestamp,
paramCount: this.countParams(query),
hasParams: params !== "[]" && params !== "",
});
}
}

private countParams(query: string): number {
// Count the number of $1, $2, etc. parameters in the query
const paramMatches = query.match(/\$\d+/g);
return paramMatches ? paramMatches.length : 0;
}
}

export const queryPerformanceMonitor = new QueryPerformanceMonitor({
verySlowQueryThreshold: env.VERY_SLOW_QUERY_THRESHOLD_MS,
});
5 changes: 3 additions & 2 deletions apps/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"@opentelemetry/sdk-trace-node": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"@popperjs/core": "^2.11.8",
"@prisma/instrumentation": "^5.11.0",
"@prisma/instrumentation": "^6.14.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

@prisma/instrumentation ^6.14.0 — version aligns with Prisma 6.14.0

Looks good. Please verify OTEL package versions don’t trigger peer warnings at runtime.


🏁 Script executed:

#!/bin/bash
# Check for mismatched OTEL deps that could cause peer warnings
jq -r '.dependencies, .devDependencies | to_entries[] | select(.key|test("^@opentelemetry/")).key + "@" + .value' apps/webapp/package.json
# Grep for Prisma instrumentation registration occurrences
rg -nP '@prisma/instrumentation|PrismaInstrumentation' -C2

Length of output: 7998


Ensure OpenTelemetry dependency versions are fully aligned

  • @prisma/instrumentation ^6.14.0 is correct and matches Prisma 6.14.0.
  • In apps/webapp/package.json we have mixed OTel versions that will trigger peer-dependency warnings:
    • @opentelemetry/[email protected] vs. core/resources/sdk packages at 1.25.1
    • @opentelemetry/instrumentation-express@^0.36.1 vs. @opentelemetry/instrumentation & [email protected]
  • These mismatches will surface at install/runtime. Please bump to a consistent set, for example:
    • “@opentelemetry/api”: “^1.25.1”
    • “@opentelemetry/instrumentation-express”: “^0.52.1”
  • After updating, reinstall (pnpm/yarn) and confirm no peer warnings appear.
🤖 Prompt for AI Agents
In apps/webapp/package.json around line 81 the OpenTelemetry packages are
version-mismatched (e.g. @opentelemetry/[email protected] vs other otel packages at
1.25.1 and @opentelemetry/instrumentation-express@^0.36.1 vs
[email protected]); update package.json to align versions (set
"@opentelemetry/api" to "^1.25.1" and "@opentelemetry/instrumentation-express"
to "^0.52.1", and ensure any other @opentelemetry/* deps use the same
1.25.x/0.52.x family), then reinstall dependencies (pnpm/yarn) and confirm no
peer-dependency warnings at install/runtime.

"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-alert-dialog": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.3",
Expand Down Expand Up @@ -137,6 +137,7 @@
"dotenv": "^16.4.5",
"effect": "^3.11.7",
"emails": "workspace:*",
"eventsource": "^4.0.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

New dependency: eventsource — confirm module format and Node support

Ensure it works with your CJS server build and Node engine (>=16). Validate bundling and SSR usage.


🏁 Script executed:

#!/bin/bash
# Where is eventsource used?
rg -nP '\beventsource\b' -C2 apps/webapp | sed -n '1,120p'

# Quick ESM/CJS check via package.json fields (requires network if not installed)
# If workspace is installed, peek installed metadata:
fd -a 'eventsource/package.json' node_modules | xargs -I{} jq '{name:.name,module:.module,main:.main,types:.types,type:.type,engines:.engines}' {}

Length of output: 484


🏁 Script executed:

#!/bin/bash
# Fetch module entrypoints and engine requirements for [email protected]
npm view [email protected] main module type engines --json

Length of output: 645


[email protected] requires Node >= 20 and is published as ESM (with a CJS entrypoint)—verify runtime upgrade or pin to v3

The new “eventsource” dependency ships with

  • "type": "module" (so all .js files are ESM),
  • a CJS entrypoint at main: "./dist/index.cjs",
  • an ESM entrypoint at module: "./dist/index.js",
  • and declares "engines": { "node": ">=20.0.0" }.

This means:

  • At install time, Node versions < 20 will be rejected by npm/yarn.
  • Your CJS-based server can still require('eventsource') (it’ll load the .cjs build), but bundlers/SSR pipelines may need explicit handling of .cjs modules in an ESM package.

Please address:

  • Whether you’re ready to upgrade your Node engine requirement to >= 20.0.0 to satisfy “eventsource”’s engines field.
  • If not, consider pinning to the latest v3.x release (which supports Node >= 10 and is CJS).
  • Confirm your bundler/SSR config correctly includes .cjs files from ESM packages.

• File: apps/webapp/package.json (line 140)
• Dependency: "eventsource": "^4.0.0"

🤖 Prompt for AI Agents
In apps/webapp/package.json around line 140, the dependency "eventsource":
"^4.0.0" requires Node >=20 and ships as ESM with a CJS entrypoint; either
update the project's engine to "node": ">=20.0.0" (and bump CI/build images,
update lockfile and reinstall) if you are ready to run Node 20+, or pin
eventsource to the latest v3.x release (e.g. replace with "^3.x") to retain Node
<20 compatibility; additionally, if you keep v4, ensure your bundler/SSR config
explicitly allows importing .cjs files from ESM packages (adjust module/resolver
rules or transpile/include node_modules .cjs entries) and run a full build/test
to verify SSR and server-side require/import behavior.

"evt": "^2.4.13",
"express": "4.20.0",
"framer-motion": "^10.12.11",
Expand Down Expand Up @@ -214,9 +215,9 @@
"@remix-run/dev": "2.1.0",
"@remix-run/eslint-config": "2.1.0",
"@remix-run/testing": "^2.1.0",
"@sentry/cli": "2.50.2",
"@swc/core": "^1.3.4",
"@swc/helpers": "^0.4.11",
"@sentry/cli": "2.50.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.9",
"@total-typescript/ts-reset": "^0.4.2",
Expand Down
4 changes: 2 additions & 2 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ RUN corepack enable
ENV NODE_ENV production
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpm install --prod --no-frozen-lockfile
COPY --from=pruner --chown=node:node /triggerdotdev/internal-packages/database/prisma/schema.prisma /triggerdotdev/internal-packages/database/prisma/schema.prisma
# RUN pnpm add @prisma/client@5.1.1 -w
# RUN pnpm add @prisma/client@6.14.0 -w
ENV NPM_CONFIG_IGNORE_WORKSPACE_ROOT_CHECK true
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpx prisma@5.4.1 generate --schema /triggerdotdev/internal-packages/database/prisma/schema.prisma
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store pnpx prisma@6.14.0 generate --schema /triggerdotdev/internal-packages/database/prisma/schema.prisma

## Builder (builds the webapp)
FROM base AS builder
Expand Down
1 change: 1 addition & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ services:

clickhouse:
image: bitnami/clickhouse:latest
restart: always
container_name: clickhouse
environment:
CLICKHOUSE_ADMIN_USER: default
Expand Down
2 changes: 2 additions & 0 deletions internal-packages/database/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
node_modules
# Ensure the .env symlink is not removed by accident
!.env

generated/prisma
6 changes: 4 additions & 2 deletions internal-packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"dependencies": {
"@prisma/client": "5.4.1"
"@prisma/client": "6.14.0",
"decimal.js": "^10.6.0"
},
"devDependencies": {
"prisma": "5.4.1",
"@types/decimal.js": "^7.4.3",
"prisma": "6.14.0",
"rimraf": "6.0.1"
},
"scripts": {
Expand Down
3 changes: 2 additions & 1 deletion internal-packages/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ datasource db {

generator client {
provider = "prisma-client-js"
output = "../generated/prisma"
binaryTargets = ["native", "debian-openssl-1.1.x"]
previewFeatures = ["tracing", "metrics"]
previewFeatures = ["metrics"]
}

model User {
Expand Down
2 changes: 1 addition & 1 deletion internal-packages/database/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * from "@prisma/client";
export * from "../generated/prisma";
export * from "./transaction";
19 changes: 14 additions & 5 deletions internal-packages/database/src/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Prisma, PrismaClient } from "@prisma/client";
import { PrismaClient } from "../generated/prisma";
import { Decimal } from "decimal.js";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";

// Define the isolation levels manually
type TransactionIsolationLevel =
| "ReadUncommitted"
| "ReadCommitted"
| "RepeatableRead"
| "Serializable";

export type PrismaTransactionClient = Omit<
PrismaClient,
Expand All @@ -9,13 +18,13 @@ export type PrismaClientOrTransaction = PrismaClient | PrismaTransactionClient;

export type PrismaReplicaClient = Omit<PrismaClient, "$transaction">;

export const Decimal = Prisma.Decimal;
export { Decimal };

function isTransactionClient(prisma: PrismaClientOrTransaction): prisma is PrismaTransactionClient {
return !("$transaction" in prisma);
}

export function isPrismaKnownError(error: unknown): error is Prisma.PrismaClientKnownRequestError {
export function isPrismaKnownError(error: unknown): error is PrismaClientKnownRequestError {
return (
typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
);
Expand Down Expand Up @@ -55,7 +64,7 @@ export type PrismaTransactionOptions = {
timeout?: number;

/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
isolationLevel?: TransactionIsolationLevel;

swallowPrismaErrors?: boolean;

Expand All @@ -70,7 +79,7 @@ export type PrismaTransactionOptions = {
export async function $transaction<R>(
prisma: PrismaClientOrTransaction,
fn: (prisma: PrismaTransactionClient) => Promise<R>,
prismaError: (error: Prisma.PrismaClientKnownRequestError) => void,
prismaError: (error: PrismaClientKnownRequestError) => void,
options?: PrismaTransactionOptions,
attempt = 0
): Promise<R | undefined> {
Expand Down
Loading
Loading