diff --git a/apps/webapp/.gitignore b/apps/webapp/.gitignore index 8b81451ead..24213b447e 100644 --- a/apps/webapp/.gitignore +++ b/apps/webapp/.gitignore @@ -18,4 +18,5 @@ build-storybook.log storybook-static /prisma/seed.js -/prisma/populate.js \ No newline at end of file +/prisma/populate.js +.memory-snapshots \ No newline at end of file diff --git a/apps/webapp/app/routes/admin.api.v1.gc.ts b/apps/webapp/app/routes/admin.api.v1.gc.ts index 1f85307b76..ea63a264ac 100644 --- a/apps/webapp/app/routes/admin.api.v1.gc.ts +++ b/apps/webapp/app/routes/admin.api.v1.gc.ts @@ -2,7 +2,8 @@ import { type DataFunctionArgs } from "@remix-run/node"; import { PerformanceObserver } from "node:perf_hooks"; import { runInNewContext } from "node:vm"; import v8 from "v8"; -import { requireUser } from "~/services/session.server"; +import { prisma } from "~/db.server"; +import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; async function waitTillGcFinishes() { let resolver: (value: PerformanceEntry) => void; @@ -35,9 +36,19 @@ async function waitTillGcFinishes() { } export async function loader({ request }: DataFunctionArgs) { - const user = await requireUser(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!user.admin) { + if (!authenticationResult) { + throw new Response("You must be an admin to perform this action", { status: 403 }); + } + + const user = await prisma.user.findFirst({ + where: { + id: authenticationResult.userId, + }, + }); + + if (!user?.admin) { throw new Response("You must be an admin to perform this action", { status: 403 }); } diff --git a/apps/webapp/app/routes/admin.api.v1.snapshot.ts b/apps/webapp/app/routes/admin.api.v1.snapshot.ts index c8fa7cee40..3b345978f5 100644 --- a/apps/webapp/app/routes/admin.api.v1.snapshot.ts +++ b/apps/webapp/app/routes/admin.api.v1.snapshot.ts @@ -4,7 +4,8 @@ import os from "os"; import path from "path"; import { PassThrough } from "stream"; import v8 from "v8"; -import { requireUser } from "~/services/session.server"; +import { prisma } from "~/db.server"; +import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server"; // Format date as yyyy-MM-dd HH_mm_ss_SSS function formatDate(date: Date) { @@ -24,9 +25,19 @@ function formatDate(date: Date) { } export async function loader({ request }: DataFunctionArgs) { - const user = await requireUser(request); + const authenticationResult = await authenticateApiRequestWithPersonalAccessToken(request); - if (!user.admin) { + if (!authenticationResult) { + throw new Response("You must be an admin to perform this action", { status: 403 }); + } + + const user = await prisma.user.findFirst({ + where: { + id: authenticationResult.userId, + }, + }); + + if (!user?.admin) { throw new Response("You must be an admin to perform this action", { status: 403 }); } diff --git a/apps/webapp/app/routes/api.v1.mock.ts b/apps/webapp/app/routes/api.v1.mock.ts new file mode 100644 index 0000000000..1ec47cb3b7 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.mock.ts @@ -0,0 +1,20 @@ +import { ActionFunctionArgs } from "@remix-run/server-runtime"; + +export async function action({ request }: ActionFunctionArgs) { + if (process.env.NODE_ENV === "production") { + return new Response("Not found", { status: 404 }); + } + + return new Response( + JSON.stringify({ + data: { + id: "123", + type: "mock", + attributes: { + name: "Mock", + }, + }, + }), + { status: 200 } + ); +} diff --git a/apps/webapp/memory-leak-detector.js b/apps/webapp/memory-leak-detector.js new file mode 100644 index 0000000000..3b8695465e --- /dev/null +++ b/apps/webapp/memory-leak-detector.js @@ -0,0 +1,977 @@ +#!/usr/bin/env node + +/** + * Memory Leak Detector for Trigger.dev Webapp + * + * This script starts the server, performs memory snapshots, executes API requests, + * and analyzes memory usage patterns to detect potential memory leaks. + * + * Usage: node memory-leak-detector.js [options] + */ + +const { spawn, exec } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const http = require("http"); +const { performance } = require("perf_hooks"); + +class MemoryLeakDetector { + constructor(options = {}) { + // Create timestamped directory for this run + const runTimestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const baseDir = options.baseDir || "./.memory-snapshots"; + const runDir = `${baseDir}/${runTimestamp}-${options.label || "memory-leak-detector"}`; + + this.options = { + // Server configuration + serverPort: options.serverPort || 3030, + serverStartTimeout: options.serverStartTimeout || 30000, + + // Sentry configuration + sentryDsn: options.sentryDsn || "", + + // Memory snapshot configuration + heapSnapshotPath: `${runDir}/snapshots`, + adminToken: options.adminToken, // Bearer token for admin API access + + apiKey: options.apiKey, + + // Testing configuration + warmupRequests: options.warmupRequests || 10, + testRequests: options.testRequests || 100, + requestDelay: options.requestDelay || 50, // ms between requests + requestTimeout: options.requestTimeout || 5000, + + // API endpoints to test (configurable) + apiEndpoints: options.apiEndpoints || ["/api/v1/runs"], + postApiEndpoints: options.postApiEndpoints || ["/api/v1/mock"], + + // Memory analysis thresholds + memoryLeakThreshold: options.memoryLeakThreshold || 50, // MB increase + heapGrowthThreshold: options.heapGrowthThreshold || 0.2, // 20% growth + + // Output configuration + verbose: options.verbose || false, + outputFile: `${runDir}/memory-leak-report.json`, + runDir: runDir, + runTimestamp: runTimestamp, + + ...options, + }; + + this.serverProcess = null; + this.results = { + startTime: new Date().toISOString(), + runTimestamp: runTimestamp, + runDirectory: runDir, + serverInfo: {}, + snapshots: [], + testPhases: [], // Track each phase of testing + totalRequests: { + warmup: 0, + phase1: 0, + phase2: 0, + successful: 0, + failed: 0, + errors: [], + }, + memoryAnalysis: {}, + recommendations: [], + }; + } + + log(message, level = "info") { + const timestamp = new Date().toISOString(); + const prefix = `[${timestamp}] [${level.toUpperCase()}]`; + + if (level === "error") { + console.error(`${prefix} ${message}`); + } else if (this.options.verbose || level === "info") { + console.log(`${prefix} ${message}`); + } + } + + async ensureSnapshotDir() { + if (!fs.existsSync(this.options.runDir)) { + fs.mkdirSync(this.options.runDir, { recursive: true }); + this.log(`Created run directory: ${this.options.runDir}`); + } + if (!fs.existsSync(this.options.heapSnapshotPath)) { + fs.mkdirSync(this.options.heapSnapshotPath, { recursive: true }); + this.log(`Created snapshot directory: ${this.options.heapSnapshotPath}`); + } + } + + async takeHeapSnapshot(label) { + const timestamp = Date.now(); + const filename = `heap-${label}-${timestamp}.heapsnapshot`; + const filepath = path.join(this.options.heapSnapshotPath, filename); + + try { + let snapshotData; + + if (this.options.adminToken) { + this.log(`Running GC before snapshot...`); + await this.runGc(); + + // Use the admin API endpoint to get actual V8 heap snapshot + this.log(`Taking V8 heap snapshot via admin API: ${label}...`); + + snapshotData = await this.takeV8HeapSnapshot(label, filepath, timestamp); + } else { + // Fallback to basic memory usage info + this.log(`Taking basic memory snapshot: ${label} (no admin token provided)...`); + + const memUsage = process.memoryUsage(); + snapshotData = { + label, + timestamp, + filename: filename.replace(".heapsnapshot", ".json"), + filepath: filepath.replace(".heapsnapshot", ".json"), + processMemory: memUsage, + type: "basic", + }; + + fs.writeFileSync(snapshotData.filepath, JSON.stringify(snapshotData, null, 2)); + } + + this.results.snapshots.push(snapshotData); + this.log(`Snapshot completed: ${label}`); + + return snapshotData; + } catch (error) { + this.log(`Failed to take heap snapshot: ${error.message}`, "error"); + throw error; + } + } + + async takeV8HeapSnapshot(label, filepath, timestamp) { + return new Promise((resolve, reject) => { + const requestOptions = { + hostname: "localhost", + port: this.options.serverPort, + path: "/admin/api/v1/snapshot", + method: "GET", + timeout: 120000, // Heap snapshots can take a while + headers: { + Authorization: `Bearer ${this.options.adminToken}`, + "User-Agent": "memory-leak-detector/1.0", + }, + }; + + const req = http.request(requestOptions, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`Admin API returned ${res.statusCode}: ${res.statusMessage}`)); + return; + } + + const writeStream = fs.createWriteStream(filepath); + let downloadedBytes = 0; + + res.on("data", (chunk) => { + downloadedBytes += chunk.length; + this.log(`Downloaded ${downloadedBytes} bytes`); + writeStream.write(chunk); + }); + + res.on("end", () => { + writeStream.end(); + + const snapshotData = { + label, + timestamp, + filename: path.basename(filepath), + filepath, + size: downloadedBytes, + type: "v8-heapsnapshot", + }; + + this.log(`V8 heap snapshot saved: ${Math.round(downloadedBytes / 1024 / 1024)}MB`); + resolve(snapshotData); + }); + + res.on("error", (error) => { + writeStream.destroy(); + fs.unlink(filepath, () => {}); // Clean up partial file + reject(error); + }); + + writeStream.on("error", (error) => { + res.destroy(); + reject(error); + }); + }); + + req.on("error", (error) => { + reject(new Error(`Failed to connect to admin API: ${error.message}`)); + }); + + req.on("timeout", () => { + req.destroy(); + reject(new Error("Admin API request timeout")); + }); + + req.end(); + }); + } + + async runGc() { + return new Promise((resolve, reject) => { + const requestOptions = { + hostname: "localhost", + port: this.options.serverPort, + path: "/admin/api/v1/gc", + method: "GET", + timeout: 120000, // GC can take a while + headers: { + Authorization: `Bearer ${this.options.adminToken}`, + "User-Agent": "memory-leak-detector/1.0", + }, + }; + + const req = http.request(requestOptions, (res) => { + if (res.statusCode !== 200) { + reject(new Error(`Admin API returned ${res.statusCode}: ${res.statusMessage}`)); + return; + } + + res.on("data", (chunk) => { + this.log(`GC run completed`); + }); + + res.on("end", () => { + resolve(); + }); + + res.on("error", (error) => { + reject(error); + }); + }); + + req.on("error", (error) => { + reject(new Error(`Failed to connect to admin API: ${error.message}`)); + }); + + req.on("timeout", () => { + req.destroy(); + reject(new Error("Admin API request timeout")); + }); + + req.end(); + }); + } + + async startServer() { + return new Promise((resolve, reject) => { + this.log("Starting server..."); + + // First, build the project + const buildProcess = spawn("npm", ["run", "build"], { + cwd: process.cwd(), + stdio: this.options.verbose ? "inherit" : "pipe", + }); + + buildProcess.on("close", (code) => { + if (code !== 0) { + reject(new Error(`Build failed with code ${code}`)); + return; + } + + this.log("Build completed, starting server..."); + + const nodePath = path.resolve(process.cwd(), "../../node_modules/.pnpm/node_modules"); + + this.log(`Using NODE_PATH: ${nodePath}`); + + // Start the server with memory inspection flags + this.serverProcess = spawn( + "node", + ["--max-old-space-size=16384", "--expose-gc", "./build/server.js"], + { + cwd: process.cwd(), + stdio: ["ignore", "pipe", "pipe"], + env: { + ...process.env, + NODE_ENV: "production", + NODE_PATH: nodePath, + PORT: this.options.serverPort, + // Disable Sentry to prevent memory leaks from it + SENTRY_DSN: this.options.sentryDsn, + }, + } + ); + + let serverReady = false; + const timeout = setTimeout(() => { + if (!serverReady) { + this.serverProcess.kill(); + reject(new Error("Server failed to start within timeout")); + } + }, this.options.serverStartTimeout); + + this.serverProcess.stdout.on("data", (data) => { + const output = data.toString(); + if (this.options.verbose) { + console.log("SERVER:", output.trim()); + } + + if (output.includes("server ready") && !serverReady) { + serverReady = true; + clearTimeout(timeout); + + this.results.serverInfo = { + port: this.options.serverPort, + startTime: Date.now(), + nodeVersion: process.version, + }; + + this.log(`Server started on port ${this.options.serverPort}`); + + // Wait a bit more for server to fully initialize + setTimeout(() => resolve(), 2000); + } + }); + + this.serverProcess.stderr.on("data", (data) => { + const error = data.toString(); + if (this.options.verbose || error.includes("Error")) { + console.error("SERVER ERROR:", error.trim()); + } + }); + + this.serverProcess.on("close", (code) => { + if (!serverReady && code !== 0) { + clearTimeout(timeout); + reject(new Error(`Server process exited with code ${code}`)); + } + }); + }); + }); + } + + async makeRequest(endpoint, options = {}) { + return new Promise((resolve, reject) => { + const requestOptions = { + hostname: "localhost", + port: this.options.serverPort, + path: endpoint, + method: options.method || "GET", + timeout: this.options.requestTimeout, + headers: { + "User-Agent": "memory-leak-detector/1.0", + Accept: "application/json", + Authorization: `Bearer ${this.options.apiKey}`, + ...options.headers, + }, + }; + + const req = http.request(requestOptions, (res) => { + let data = ""; + + res.on("data", (chunk) => { + data += chunk; + }); + + res.on("end", () => { + resolve({ + statusCode: res.statusCode, + headers: res.headers, + body: data, + endpoint: endpoint, + }); + }); + }); + + req.on("error", (error) => { + reject({ error, endpoint }); + }); + + req.on("timeout", () => { + req.destroy(); + reject({ error: new Error("Request timeout"), endpoint }); + }); + + if (options.body) { + req.write(options.body); + } + + req.end(); + }); + } + + async performRequestPhase(phaseName, numRequests) { + this.log(`Starting ${phaseName} phase with ${numRequests} GET requests...`); + + const startTime = performance.now(); + let successfulRequests = 0; + let failedRequests = 0; + const errors = []; + + for (let i = 0; i < numRequests; i++) { + const endpoint = this.options.apiEndpoints[i % this.options.apiEndpoints.length]; + + try { + const response = await this.makeRequest(endpoint); + + if (response.statusCode >= 200 && response.statusCode < 300) { + successfulRequests++; + } else { + failedRequests++; + errors.push({ + endpoint, + statusCode: response.statusCode, + error: `HTTP ${response.statusCode}`, + phase: phaseName, + }); + } + } catch (error) { + failedRequests++; + errors.push({ + endpoint: error.endpoint, + error: error.error?.message || "Unknown error", + phase: phaseName, + }); + } + + // Add delay between requests + if (i < numRequests - 1) { + await this.delay(this.options.requestDelay); + } + + // Progress reporting + if (this.options.verbose && numRequests > 25 && (i + 1) % 25 === 0) { + this.log(`${phaseName}: ${i + 1}/${numRequests} requests completed`); + } + } + + this.log(`Continuing with ${phaseName} phase with ${numRequests} POST requests...`); + + for (let i = 0; i < numRequests; i++) { + const endpoint = this.options.postApiEndpoints[i % this.options.postApiEndpoints.length]; + + try { + // Send a LARGE body to try and trigger a memory leak + const response = await this.makeRequest(endpoint, { + method: "POST", + body: JSON.stringify( + Array.from({ length: 1000 }, (_, index) => ({ + id: index, + name: `Mock ${index}`, + description: `Mock ${index} description`, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + uuid: crypto.randomUUID(), + })) + ), + }); + + if (response.statusCode >= 200 && response.statusCode < 300) { + successfulRequests++; + } else { + failedRequests++; + errors.push({ + endpoint, + statusCode: response.statusCode, + error: `HTTP ${response.statusCode}`, + phase: phaseName, + }); + } + } catch (error) { + failedRequests++; + errors.push({ + endpoint: error.endpoint, + error: error.error?.message || "Unknown error", + phase: phaseName, + }); + } + + // Add delay between requests + if (i < numRequests - 1) { + await this.delay(this.options.requestDelay); + } + + // Progress reporting + if (this.options.verbose && numRequests > 25 && (i + 1) % 25 === 0) { + this.log(`${phaseName}: ${i + 1}/${numRequests} requests completed`); + } + } + + const endTime = performance.now(); + const duration = endTime - startTime; + + const phaseResults = { + phase: phaseName, + total: numRequests, + successful: successfulRequests, + failed: failedRequests, + errors: errors.slice(0, 5), // Keep only first 5 errors per phase + duration: Math.round(duration), + requestsPerSecond: Math.round((numRequests / duration) * 1000), + }; + + this.results.testPhases.push(phaseResults); + + // Update totals + this.results.totalRequests.successful += successfulRequests; + this.results.totalRequests.failed += failedRequests; + this.results.totalRequests.errors.push(...errors.slice(0, 3)); + + if (phaseName === "warmup") { + this.results.totalRequests.warmup = numRequests; + } else if (phaseName === "load-test-1") { + this.results.totalRequests.phase1 = numRequests; + } else if (phaseName === "load-test-2") { + this.results.totalRequests.phase2 = numRequests; + } + + this.log( + `${phaseName} completed: ${successfulRequests}/${numRequests} successful (${Math.round( + duration + )}ms total)` + ); + + return phaseResults; + } + + analyzeMemoryUsage() { + if (this.results.snapshots.length < 3) { + this.log("Need at least 3 snapshots to analyze memory usage", "warn"); + return; + } + + const snapshot1 = this.results.snapshots[0]; // after warmup + const snapshot2 = this.results.snapshots[1]; // after first load test + const snapshot3 = this.results.snapshots[2]; // after second load test + + let analysis = {}; + + // Handle different snapshot types + if ( + snapshot1.type === "v8-heapsnapshot" && + snapshot2.type === "v8-heapsnapshot" && + snapshot3.type === "v8-heapsnapshot" + ) { + const size1 = snapshot1.size || 0; + const size2 = snapshot2.size || 0; + const size3 = snapshot3.size || 0; + + const growth1to2 = size2 - size1; + const growth2to3 = size3 - size2; + const totalGrowth = size3 - size1; + + const growth1to2Percent = size1 > 0 ? (growth1to2 / size1) * 100 : 0; + const growth2to3Percent = size2 > 0 ? (growth2to3 / size2) * 100 : 0; + const totalGrowthPercent = size1 > 0 ? (totalGrowth / size1) * 100 : 0; + + analysis = { + snapshotAnalysis: { + type: "v8-heapsnapshot", + phase1: { + size: Math.round(size1 / 1024 / 1024), + file: snapshot1.filename, + }, + phase2: { + size: Math.round(size2 / 1024 / 1024), + file: snapshot2.filename, + growthFromPhase1: Math.round(growth1to2 / 1024 / 1024), + growthPercentFromPhase1: Math.round(growth1to2Percent * 100) / 100, + }, + phase3: { + size: Math.round(size3 / 1024 / 1024), + file: snapshot3.filename, + growthFromPhase2: Math.round(growth2to3 / 1024 / 1024), + growthPercentFromPhase2: Math.round(growth2to3Percent * 100) / 100, + }, + total: { + growth: Math.round(totalGrowth / 1024 / 1024), + growthPercent: Math.round(totalGrowthPercent * 100) / 100, + }, + }, + snapshots: this.results.snapshots.length, + snapshotPaths: this.results.snapshots.map((s) => s.filepath), + }; + + // Use total growth for recommendations + var heapGrowth = totalGrowth; + var heapGrowthPercent = totalGrowthPercent; + } else if (snapshot1.processMemory && snapshot2.processMemory && snapshot3.processMemory) { + // Traditional process memory analysis with 3 snapshots + const heap1 = snapshot1.processMemory.heapUsed; + const heap2 = snapshot2.processMemory.heapUsed; + const heap3 = snapshot3.processMemory.heapUsed; + const rss1 = snapshot1.processMemory.rss; + const rss2 = snapshot2.processMemory.rss; + const rss3 = snapshot3.processMemory.rss; + + const heapGrowth1to2 = heap2 - heap1; + const heapGrowth2to3 = heap3 - heap2; + const totalHeapGrowth = heap3 - heap1; + const rssGrowth1to2 = rss2 - rss1; + const rssGrowth2to3 = rss3 - rss2; + const totalRssGrowth = rss3 - rss1; + + analysis = { + heapUsage: { + phase1: Math.round(heap1 / 1024 / 1024), + phase2: Math.round(heap2 / 1024 / 1024), + phase3: Math.round(heap3 / 1024 / 1024), + growth1to2: Math.round(heapGrowth1to2 / 1024 / 1024), + growth2to3: Math.round(heapGrowth2to3 / 1024 / 1024), + totalGrowth: Math.round(totalHeapGrowth / 1024 / 1024), + totalGrowthPercent: Math.round((totalHeapGrowth / heap1) * 100 * 100) / 100, + }, + rssUsage: { + phase1: Math.round(rss1 / 1024 / 1024), + phase2: Math.round(rss2 / 1024 / 1024), + phase3: Math.round(rss3 / 1024 / 1024), + growth1to2: Math.round(rssGrowth1to2 / 1024 / 1024), + growth2to3: Math.round(rssGrowth2to3 / 1024 / 1024), + totalGrowth: Math.round(totalRssGrowth / 1024 / 1024), + totalGrowthPercent: Math.round((totalRssGrowth / rss1) * 100 * 100) / 100, + }, + snapshots: this.results.snapshots.length, + }; + + var heapGrowth = totalHeapGrowth; + var heapGrowthPercent = (totalHeapGrowth / heap1) * 100; + } else { + this.log("Mixed or incompatible snapshot types - cannot analyze memory growth", "warn"); + analysis = { + error: "Incompatible snapshot types", + snapshots: this.results.snapshots.length, + snapshotTypes: this.results.snapshots.map((s) => ({ label: s.label, type: s.type })), + }; + this.results.memoryAnalysis = analysis; + return; + } + + this.results.memoryAnalysis = analysis; + + // Generate recommendations + const recommendations = []; + + if (Math.abs(heapGrowth) > this.options.memoryLeakThreshold * 1024 * 1024) { + recommendations.push({ + type: "warning", + message: `Significant heap growth detected: ${Math.round( + heapGrowth / 1024 / 1024 + )}MB increase`, + suggestion: "Consider investigating object retention and event listener cleanup", + }); + } + + if (Math.abs(heapGrowthPercent) > this.options.heapGrowthThreshold * 100) { + recommendations.push({ + type: "warning", + message: `High heap growth percentage: ${heapGrowthPercent.toFixed(1)}%`, + suggestion: "Review memory allocation patterns and garbage collection behavior", + }); + } + + if ( + this.results.totalRequests.failed > + (this.results.totalRequests.phase1 + this.results.totalRequests.phase2) * 0.1 + ) { + recommendations.push({ + type: "error", + message: `High request failure rate: ${this.results.totalRequests.failed}/${ + this.results.totalRequests.phase1 + this.results.totalRequests.phase2 + }`, + suggestion: "Fix failing endpoints before analyzing memory patterns", + }); + } + + if (recommendations.length === 0) { + recommendations.push({ + type: "info", + message: "No obvious memory leaks detected in this test run", + suggestion: "Consider running longer tests or testing different API endpoints", + }); + } + + this.results.recommendations = recommendations; + + // Log analysis results + this.log("=== Memory Analysis Results ==="); + + if (this.results.memoryAnalysis.snapshotAnalysis) { + const analysis = this.results.memoryAnalysis.snapshotAnalysis; + this.log(`V8 Heap Snapshot Analysis:`); + this.log(` Phase 1 (after warmup): ${analysis.phase1.size}MB`); + this.log( + ` Phase 2 (after load test 1): ${analysis.phase2.size}MB (${ + analysis.phase2.growthPercentFromPhase1 > 0 ? "+" : "" + }${analysis.phase2.growthPercentFromPhase1}%)` + ); + this.log( + ` Phase 3 (after load test 2): ${analysis.phase3.size}MB (${ + analysis.phase3.growthPercentFromPhase2 > 0 ? "+" : "" + }${analysis.phase3.growthPercentFromPhase2}%)` + ); + this.log( + ` Total Growth: ${analysis.total.growthPercent > 0 ? "+" : ""}${ + analysis.total.growth + }MB (${analysis.total.growthPercent}%)` + ); + + this.log(`\nSnapshot files saved:`); + this.log(` 1. ${analysis.phase1.file}`); + this.log(` 2. ${analysis.phase2.file}`); + this.log(` 3. ${analysis.phase3.file}`); + this.log(`\nšŸ’” Analyze snapshots in Chrome DevTools:`); + this.log(` 1. Open Chrome DevTools → Memory tab`); + this.log(` 2. Load all 3 .heapsnapshot files`); + this.log(` 3. Compare snapshots to identify memory leaks`); + this.log(` 4. Focus on comparing snapshot 1 vs 3 for overall growth`); + } else if (this.results.memoryAnalysis.heapUsage) { + const heap = this.results.memoryAnalysis.heapUsage; + const rss = this.results.memoryAnalysis.rssUsage; + this.log(`Heap Usage Analysis:`); + this.log(` Phase 1: ${heap.phase1}MB`); + this.log( + ` Phase 2: ${heap.phase2}MB (${heap.growth1to2 > 0 ? "+" : ""}${heap.growth1to2}MB)` + ); + this.log( + ` Phase 3: ${heap.phase3}MB (${heap.growth2to3 > 0 ? "+" : ""}${heap.growth2to3}MB)` + ); + this.log( + ` Total Growth: ${heap.totalGrowthPercent > 0 ? "+" : ""}${heap.totalGrowth}MB (${ + heap.totalGrowthPercent + }%)` + ); + + this.log(`RSS Usage Analysis:`); + this.log(` Phase 1: ${rss.phase1}MB`); + this.log(` Phase 2: ${rss.phase2}MB (${rss.growth1to2 > 0 ? "+" : ""}${rss.growth1to2}MB)`); + this.log(` Phase 3: ${rss.phase3}MB (${rss.growth2to3 > 0 ? "+" : ""}${rss.growth2to3}MB)`); + this.log( + ` Total Growth: ${rss.totalGrowthPercent > 0 ? "+" : ""}${rss.totalGrowth}MB (${ + rss.totalGrowthPercent + }%)` + ); + } + + recommendations.forEach((rec, i) => { + this.log( + `${i + 1}. [${rec.type.toUpperCase()}] ${rec.message}`, + rec.type === "error" ? "error" : "info" + ); + this.log(` Suggestion: ${rec.suggestion}`, "info"); + }); + } + + async generateReport() { + const reportData = { + ...this.results, + endTime: new Date().toISOString(), + configuration: { + serverPort: this.options.serverPort, + testRequests: this.options.testRequests, + warmupRequests: this.options.warmupRequests, + apiEndpoints: this.options.apiEndpoints, + memoryLeakThreshold: this.options.memoryLeakThreshold, + heapGrowthThreshold: this.options.heapGrowthThreshold, + adminToken: this.options.adminToken ? "[REDACTED]" : null, + runDirectory: this.options.runDir, + }, + }; + + try { + fs.writeFileSync(this.options.outputFile, JSON.stringify(reportData, null, 2)); + this.log(`Report saved to: ${this.options.outputFile}`); + } catch (error) { + this.log(`Failed to save report: ${error.message}`, "error"); + } + + return reportData; + } + + async delay(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async cleanup() { + if (this.serverProcess && !this.serverProcess.killed) { + this.log("Stopping server..."); + this.serverProcess.kill("SIGTERM"); + + // Wait a bit for graceful shutdown + await this.delay(2000); + + if (!this.serverProcess.killed) { + this.serverProcess.kill("SIGKILL"); + } + } + } + + async run() { + try { + this.log("=== Memory Leak Detection Started ==="); + this.log(`Run directory: ${this.options.runDir}`); + + await this.ensureSnapshotDir(); + await this.startServer(); + + // Wait for server to fully initialize + await this.delay(3000); + + // Phase 1: Warmup requests + first snapshot + this.log("\n=== Phase 1: Warmup ==="); + await this.performRequestPhase("warmup", this.options.warmupRequests); + + // Force GC and wait before snapshot + if (global.gc) { + global.gc(); + await this.delay(1000); + } + await this.takeHeapSnapshot("after-warmup"); + + // Phase 2: First load test + second snapshot + this.log("\n=== Phase 2: Load Test 1 ==="); + await this.performRequestPhase("load-test-1", this.options.testRequests); + + // Force GC and wait before snapshot + if (global.gc) { + global.gc(); + await this.delay(1000); + } + await this.takeHeapSnapshot("after-load-test-1"); + + // Phase 3: Second load test + third snapshot + this.log("\n=== Phase 3: Load Test 2 ==="); + await this.performRequestPhase("load-test-2", this.options.testRequests); + + // Force GC and wait before final snapshot + if (global.gc) { + global.gc(); + await this.delay(2000); + } + await this.takeHeapSnapshot("after-load-test-2"); + + // Analyze results + this.analyzeMemoryUsage(); + + // Generate report + await this.generateReport(); + + this.log("\n=== Memory Leak Detection Completed ==="); + this.log(`šŸ“ All results saved to: ${this.options.runDir}`); + this.log(`šŸ“Š Report: ${this.options.outputFile}`); + this.log(`šŸ“ˆ Snapshots: ${this.options.heapSnapshotPath}`); + + if ( + this.results.snapshots.length === 3 && + this.results.snapshots[0].type === "v8-heapsnapshot" + ) { + this.log(`\nšŸ” Next steps:`); + this.log(`1. Analyze snapshots in Chrome DevTools Memory tab`); + this.log(`2. Compare snapshot 1 vs 3 to identify memory leaks`); + this.log(`3. Look for growing Sentry-related objects`); + this.log(`4. Run with SENTRY_DSN enabled to compare results`); + } + } catch (error) { + this.log(`Detection failed: ${error.message}`, "error"); + throw error; + } finally { + await this.cleanup(); + } + } +} + +// CLI interface +function parseArgs() { + const args = process.argv.slice(2); + const options = {}; + + for (let i = 0; i < args.length; i += 2) { + const arg = args[i]; + const value = args[i + 1]; + + switch (arg) { + case "--port": + options.serverPort = parseInt(value); + break; + case "--requests": + options.testRequests = parseInt(value); + break; + case "--delay": + options.requestDelay = parseInt(value); + break; + case "--endpoints": + options.apiEndpoints = value.split(","); + break; + case "--threshold": + options.memoryLeakThreshold = parseFloat(value); + break; + case "--output": + options.outputFile = value; + break; + case "--token": + options.adminToken = value; + break; + case "--api-key": + options.apiKey = value; + break; + case "--sentry-dsn": + options.sentryDsn = value; + break; + case "--label": + options.label = value; + break; + case "--verbose": + options.verbose = true; + i--; // No value for this flag + break; + case "--help": + console.log(` +Memory Leak Detector Usage: + +node memory-leak-detector.js [options] + +Options: + --port Server port (default: 3030) + --requests Number of test requests (default: 100) + --delay Delay between requests (default: 50) + --endpoints Comma-separated API endpoints to test + --label Label for the run + --threshold Memory leak threshold in MB (default: 50) + --token Admin Bearer token for V8 heap snapshots + --api-key API key for API requests + --sentry-dsn Sentry DSN for memory leak detection + --output Output report file (default: memory-leak-report.json) + --verbose Enable verbose logging + --help Show this help + +Examples: + node memory-leak-detector.js --verbose --requests 200 + node memory-leak-detector.js --token "your-admin-token" --api-key "your-api-key" --verbose + node memory-leak-detector.js --endpoints "/api/v1/whoami,/api/v1/projects" --threshold 25 + `); + process.exit(0); + } + } + + return options; +} + +// Main execution +if (require.main === module) { + const options = parseArgs(); + const detector = new MemoryLeakDetector(options); + + detector + .run() + .then(() => { + console.log("\nāœ… Memory leak detection completed successfully!"); + console.log(`šŸ“ Results directory: ${detector.options.runDir}`); + console.log(`šŸ“Š Report: ${detector.options.outputFile}`); + console.log(`šŸ“ˆ Snapshots: ${detector.options.heapSnapshotPath}`); + process.exit(0); + }) + .catch((error) => { + console.error("\nāŒ Memory leak detection failed:", error.message); + process.exit(1); + }); +} + +module.exports = MemoryLeakDetector; diff --git a/apps/webapp/package.json b/apps/webapp/package.json index 69473b78b9..d01407d17d 100644 --- a/apps/webapp/package.json +++ b/apps/webapp/package.json @@ -101,7 +101,7 @@ "@remix-run/serve": "2.1.0", "@remix-run/server-runtime": "2.1.0", "@remix-run/v1-meta": "^0.1.3", - "@sentry/remix": "^9.40.0", + "@sentry/remix": "9.46.0", "@slack/web-api": "7.9.1", "@socket.io/redis-adapter": "^8.3.0", "@splinetool/react-spline": "^2.2.6", diff --git a/apps/webapp/sentry.server.ts b/apps/webapp/sentry.server.ts index e752574732..a50efcfe85 100644 --- a/apps/webapp/sentry.server.ts +++ b/apps/webapp/sentry.server.ts @@ -22,5 +22,6 @@ if (process.env.SENTRY_DSN) { environment: process.env.APP_ENV, ignoreErrors: ["queryRoute() call aborted"], + includeLocalVariables: false, }); } diff --git a/package.json b/package.json index 877660e12f..c7d5330fbd 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,8 @@ "engine.io-parser@5.2.2": "patches/engine.io-parser@5.2.2.patch", "graphile-worker@0.16.6": "patches/graphile-worker@0.16.6.patch", "redlock@5.0.0-beta.2": "patches/redlock@5.0.0-beta.2.patch", - "@kubernetes/client-node@1.0.0": "patches/@kubernetes__client-node@1.0.0.patch" + "@kubernetes/client-node@1.0.0": "patches/@kubernetes__client-node@1.0.0.patch", + "@sentry/remix@9.46.0": "patches/@sentry__remix@9.46.0.patch" }, "overrides": { "express@^4>body-parser": "1.20.3", diff --git a/patches/@sentry__remix@9.46.0.patch b/patches/@sentry__remix@9.46.0.patch new file mode 100644 index 0000000000..e30781631b --- /dev/null +++ b/patches/@sentry__remix@9.46.0.patch @@ -0,0 +1,39 @@ +diff --git a/build/cjs/vendor/instrumentation.js b/build/cjs/vendor/instrumentation.js +index 84e18d1051f57d5807e65c8b8ce858ceee7d4557..640a5253d565650338fe33e0ea52c8dffc63e4e7 100644 +--- a/build/cjs/vendor/instrumentation.js ++++ b/build/cjs/vendor/instrumentation.js +@@ -238,7 +238,7 @@ class RemixInstrumentation extends instrumentation.InstrumentationBase { + return function callRouteAction(original) { + return async function patchCallRouteAction( ...args) { + const [params] = args; +- const clonedRequest = params.request.clone(); ++ const clonedRequest = params.request; + const span = plugin.tracer.startSpan( + `ACTION ${params.routeId}`, + { attributes: { [semanticConventions.SemanticAttributes.CODE_FUNCTION]: 'action' } }, +@@ -257,25 +257,6 @@ class RemixInstrumentation extends instrumentation.InstrumentationBase { + .then(async response => { + addResponseAttributesToSpan(span, response); + +- try { +- const formData = await clonedRequest.formData(); +- const { actionFormDataAttributes: actionFormAttributes } = plugin.getConfig(); +- +- formData.forEach((value, key) => { +- if ( +- actionFormAttributes?.[key] && +- actionFormAttributes[key] !== false && +- typeof value === 'string' +- ) { +- const keyName = actionFormAttributes[key] === true ? key : actionFormAttributes[key]; +- span.setAttribute(`formData.${keyName}`, value.toString()); +- } +- }); +- } catch { +- // Silently continue on any error. Typically happens because the action body cannot be processed +- // into FormData, in which case we should just continue. +- } +- + return response; + }) + .catch(async error => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9da304279c..15490dbedd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,6 +19,9 @@ patchedDependencies: '@kubernetes/client-node@1.0.0': hash: s75bgwaoixupmywtvgoy5ruszq path: patches/@kubernetes__client-node@1.0.0.patch + '@sentry/remix@9.46.0': + hash: biuxdxyvvwd3otdrxnv2y3covi + path: patches/@sentry__remix@9.46.0.patch engine.io-parser@5.2.2: hash: e6nctogrhpxoivwiwy37ersfu4 path: patches/engine.io-parser@5.2.2.patch @@ -405,8 +408,8 @@ importers: specifier: ^0.1.3 version: 0.1.3(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0) '@sentry/remix': - specifier: ^9.40.0 - version: 9.40.0(@remix-run/node@2.1.0)(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0)(react@18.2.0) + specifier: 9.46.0 + version: 9.46.0(patch_hash=biuxdxyvvwd3otdrxnv2y3covi)(@remix-run/node@2.1.0)(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0)(react@18.2.0) '@slack/web-api': specifier: 7.9.1 version: 7.9.1 @@ -1265,13 +1268,13 @@ importers: dependencies: '@clack/prompts': specifier: ^0.10.0 - version: 0.10.0 + version: 0.10.1 '@depot/cli': specifier: 0.0.1-cli.2.80.0 version: 0.0.1-cli.2.80.0 '@modelcontextprotocol/sdk': specifier: ^1.6.1 - version: 1.6.1(supports-color@10.0.0) + version: 1.17.1(supports-color@10.0.0) '@opentelemetry/api': specifier: 1.9.0 version: 1.9.0 @@ -5808,17 +5811,17 @@ packages: resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} dev: true - /@clack/core@0.4.1: - resolution: {integrity: sha512-Pxhij4UXg8KSr7rPek6Zowm+5M22rbd2g1nfojHJkxp5YkFqiZ2+YLEM/XGVIzvGOcM0nqjIFxrpDwWRZYWYjA==} + /@clack/core@0.4.2: + resolution: {integrity: sha512-NYQfcEy8MWIxrT5Fj8nIVchfRFA26yYKJcvBS7WlUIlw2OmQOY9DhGGXMovyI5J5PpxrCPGkgUi207EBrjpBvg==} dependencies: picocolors: 1.1.1 sisteransi: 1.0.5 dev: false - /@clack/prompts@0.10.0: - resolution: {integrity: sha512-H3rCl6CwW1NdQt9rE3n373t7o5cthPv7yUoxF2ytZvyvlJv89C5RYMJu83Hed8ODgys5vpBU0GKxIRG83jd8NQ==} + /@clack/prompts@0.10.1: + resolution: {integrity: sha512-Q0T02vx8ZM9XSv9/Yde0jTmmBQufZhPJfYAg2XrrrxWWaZgq1rr8nU8Hv710BQ1dhoP8rtY7YUdpGej2Qza/cw==} dependencies: - '@clack/core': 0.4.1 + '@clack/core': 0.4.2 picocolors: 1.1.1 sisteransi: 1.0.5 dev: false @@ -9284,19 +9287,22 @@ packages: - supports-color dev: true - /@modelcontextprotocol/sdk@1.6.1(supports-color@10.0.0): - resolution: {integrity: sha512-oxzMzYCkZHMntzuyerehK3fV6A2Kwh5BD6CGEJSVDU2QNEhfLOptf2X7esQgaHZXHZY0oHmMsOtIDLP71UJXgA==} + /@modelcontextprotocol/sdk@1.17.1(supports-color@10.0.0): + resolution: {integrity: sha512-CPle1OQehbWqd25La9Ack5B07StKIxh4+Bf19qnpZKJC1oI22Y0czZHbifjw1UoczIfKBwBDAp/dFxvHG13B5A==} engines: {node: '>=18'} dependencies: + ajv: 6.12.6 content-type: 1.0.5 cors: 2.8.5 + cross-spawn: 7.0.6 eventsource: 3.0.5 + eventsource-parser: 3.0.0 express: 5.0.1(supports-color@10.0.0) express-rate-limit: 7.5.0(express@5.0.1) - pkce-challenge: 4.1.0 + pkce-challenge: 5.0.0 raw-body: 3.0.0 zod: 3.25.76 - zod-to-json-schema: 3.24.3(zod@3.25.76) + zod-to-json-schema: 3.24.5(zod@3.25.76) transitivePeerDependencies: - supports-color dev: false @@ -11349,7 +11355,7 @@ packages: resolution: {integrity: sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dependencies: - cross-spawn: 7.0.3 + cross-spawn: 7.0.6 is-glob: 4.0.3 open: 8.4.0 picocolors: 1.1.1 @@ -17601,34 +17607,34 @@ packages: selderee: 0.11.0 dev: false - /@sentry-internal/browser-utils@9.40.0: - resolution: {integrity: sha512-Ajvz6jN+EEMKrOHcUv2+HlhbRUh69uXhhRoBjJw8sc61uqA2vv3QWyBSmTRoHdTnLGboT5bKEhHIkzVXb+YgEw==} + /@sentry-internal/browser-utils@9.46.0: + resolution: {integrity: sha512-Q0CeHym9wysku8mYkORXmhtlBE0IrafAI+NiPSqxOBKXGOCWKVCvowHuAF56GwPFic2rSrRnub5fWYv7T1jfEQ==} engines: {node: '>=18'} dependencies: - '@sentry/core': 9.40.0 + '@sentry/core': 9.46.0 dev: false - /@sentry-internal/feedback@9.40.0: - resolution: {integrity: sha512-39UbLdGWGvSJ7bAzRnkv91cBdd6fLbdkLVVvqE2ZUfegm7+rH1mRPglmEhw4VE4mQfKZM1zWr/xus2+XPqJcYw==} + /@sentry-internal/feedback@9.46.0: + resolution: {integrity: sha512-KLRy3OolDkGdPItQ3obtBU2RqDt9+KE8z7r7Gsu7c6A6A89m8ZVlrxee3hPQt6qp0YY0P8WazpedU3DYTtaT8w==} engines: {node: '>=18'} dependencies: - '@sentry/core': 9.40.0 + '@sentry/core': 9.46.0 dev: false - /@sentry-internal/replay-canvas@9.40.0: - resolution: {integrity: sha512-GLoJ4R4Uipd7Vb+0LzSJA2qCyN1J6YalQIoDuOJTfYyykHvKltds5D8a/5S3Q6d8PcL/nxTn93fynauGEZt2Ow==} + /@sentry-internal/replay-canvas@9.46.0: + resolution: {integrity: sha512-QcBjrdRWFJrrrjbmrr2bbrp2R9RYj1KMEbhHNT2Lm1XplIQw+tULEKOHxNtkUFSLR1RNje7JQbxhzM1j95FxVQ==} engines: {node: '>=18'} dependencies: - '@sentry-internal/replay': 9.40.0 - '@sentry/core': 9.40.0 + '@sentry-internal/replay': 9.46.0 + '@sentry/core': 9.46.0 dev: false - /@sentry-internal/replay@9.40.0: - resolution: {integrity: sha512-WrmCvqbLJQC45IFRVN3k0J5pU5NkdX0e9o6XxjcmDiATKk00RHnW4yajnCJ8J1cPR4918yqiJHPX5xpG08BZNA==} + /@sentry-internal/replay@9.46.0: + resolution: {integrity: sha512-+8JUblxSSnN0FXcmOewbN+wIc1dt6/zaSeAvt2xshrfrLooVullcGsuLAiPhY0d/e++Fk06q1SAl9g4V0V13gg==} engines: {node: '>=18'} dependencies: - '@sentry-internal/browser-utils': 9.40.0 - '@sentry/core': 9.40.0 + '@sentry-internal/browser-utils': 9.46.0 + '@sentry/core': 9.46.0 dev: false /@sentry/babel-plugin-component-annotate@2.22.2: @@ -17636,15 +17642,15 @@ packages: engines: {node: '>= 14'} dev: false - /@sentry/browser@9.40.0: - resolution: {integrity: sha512-qz/1Go817vcsbcIwgrz4/T34vi3oQ4UIqikosuaCTI9wjZvK0HyW3QmLvTbAnsE7G7h6+UZsVkpO5R16IQvQhQ==} + /@sentry/browser@9.46.0: + resolution: {integrity: sha512-NOnCTQCM0NFuwbyt4DYWDNO2zOTj1mCf43hJqGDFb1XM9F++7zAmSNnCx4UrEoBTiFOy40McJwBBk9D1blSktA==} engines: {node: '>=18'} dependencies: - '@sentry-internal/browser-utils': 9.40.0 - '@sentry-internal/feedback': 9.40.0 - '@sentry-internal/replay': 9.40.0 - '@sentry-internal/replay-canvas': 9.40.0 - '@sentry/core': 9.40.0 + '@sentry-internal/browser-utils': 9.46.0 + '@sentry-internal/feedback': 9.46.0 + '@sentry-internal/replay': 9.46.0 + '@sentry-internal/replay-canvas': 9.46.0 + '@sentry/core': 9.46.0 dev: false /@sentry/bundler-plugin-core@2.22.2: @@ -17751,8 +17757,8 @@ packages: - encoding - supports-color - /@sentry/core@9.40.0: - resolution: {integrity: sha512-cZkuz6BDna6VXSqvlWnrRsaDx4QBKq1PcfQrqhVz8ljs0M7Gcl+Mtj8dCzUxx12fkYM62hQXG72DEGNlAQpH/Q==} + /@sentry/core@9.46.0: + resolution: {integrity: sha512-it7JMFqxVproAgEtbLgCVBYtQ9fIb+Bu0JD+cEplTN/Ukpe6GaolyYib5geZqslVxhp2sQgT+58aGvfd/k0N8Q==} engines: {node: '>=18'} dev: false @@ -17768,8 +17774,8 @@ packages: - supports-color dev: false - /@sentry/node-core@9.40.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/instrumentation@0.57.2)(@opentelemetry/resources@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0): - resolution: {integrity: sha512-97JONDa8NxItX0Cz5WQPMd1gQjzodt38qQ0OzZNFvYg2Cpvxob8rxwsNA08Liu7B97rlvsvqMt+Wbgw8SAMfgQ==} + /@sentry/node-core@9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/instrumentation@0.57.2)(@opentelemetry/resources@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0): + resolution: {integrity: sha512-XRVu5pqoklZeh4wqhxCLZkz/ipoKhitctgEFXX9Yh1e1BoHM2pIxT52wf+W6hHM676TFmFXW3uKBjsmRM3AjgA==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -17787,13 +17793,13 @@ packages: '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.36.0 - '@sentry/core': 9.40.0 - '@sentry/opentelemetry': 9.40.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/core': 9.46.0 + '@sentry/opentelemetry': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) import-in-the-middle: 1.14.2 dev: false - /@sentry/node@9.40.0: - resolution: {integrity: sha512-8bVWChXzGH4QmbVw+H/yiJ6zxqPDhnx11fEAP+vpL1UBm1cAV67CoB4eS7OqQdPC8gF/BQb2sqF0TvY/12NPpA==} + /@sentry/node@9.46.0: + resolution: {integrity: sha512-pRLqAcd7GTGvN8gex5FtkQR5Mcol8gOy1WlyZZFq4rBbVtMbqKOQRhohwqnb+YrnmtFpj7IZ7KNDo077MvNeOQ==} engines: {node: '>=18'} dependencies: '@opentelemetry/api': 1.9.0 @@ -17826,17 +17832,17 @@ packages: '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.36.0 '@prisma/instrumentation': 6.11.1(@opentelemetry/api@1.9.0) - '@sentry/core': 9.40.0 - '@sentry/node-core': 9.40.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/instrumentation@0.57.2)(@opentelemetry/resources@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) - '@sentry/opentelemetry': 9.40.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/core': 9.46.0 + '@sentry/node-core': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/instrumentation@0.57.2)(@opentelemetry/resources@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) + '@sentry/opentelemetry': 9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0) import-in-the-middle: 1.14.2 minimatch: 9.0.5 transitivePeerDependencies: - supports-color dev: false - /@sentry/opentelemetry@9.40.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0): - resolution: {integrity: sha512-POQ/ZFmBbi15z3EO9gmTExpxCfW0Ug+WooA8QZPJaizo24gcF5AMOgwuGFwT2YLw/2HdPWjPUPujNNGdCWM6hw==} + /@sentry/opentelemetry@9.46.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1)(@opentelemetry/core@1.30.1)(@opentelemetry/sdk-trace-base@1.30.1)(@opentelemetry/semantic-conventions@1.36.0): + resolution: {integrity: sha512-w2zTxqrdmwRok0cXBoh+ksXdGRUHUZhlpfL/H2kfTodOL+Mk8rW72qUmfqQceXoqgbz8UyK8YgJbyt+XS5H4Qg==} engines: {node: '>=18'} peerDependencies: '@opentelemetry/api': ^1.9.0 @@ -17850,23 +17856,23 @@ packages: '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) '@opentelemetry/semantic-conventions': 1.36.0 - '@sentry/core': 9.40.0 + '@sentry/core': 9.46.0 dev: false - /@sentry/react@9.40.0(react@18.2.0): - resolution: {integrity: sha512-y00d33qozmQAKroQ4Kk2jxhznprPBOb55SL4LOpNPRHGEomxZCUeM3geltczrf14JsGowCr5+xlT+cZQ2XcNlA==} + /@sentry/react@9.46.0(react@18.2.0): + resolution: {integrity: sha512-2NTlke1rKAJO2JIY1RCrv8EjfXXkLc+AC61PpgF1QjH/cz0NuCZ6gpQi6M5qS7anAGPjaOE1t3QdLeOEI/Q3kA==} engines: {node: '>=18'} peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x dependencies: - '@sentry/browser': 9.40.0 - '@sentry/core': 9.40.0 + '@sentry/browser': 9.46.0 + '@sentry/core': 9.46.0 hoist-non-react-statics: 3.3.2 react: 18.2.0 dev: false - /@sentry/remix@9.40.0(@remix-run/node@2.1.0)(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0)(react@18.2.0): - resolution: {integrity: sha512-8cXI06jqCqzwH/132Z1K1hKBid4HKoC8LfE1sBa6hJGu+/dosbYIy7M8wCYrD5gIAcIacEIWlGyB4xOBjyXTpA==} + /@sentry/remix@9.46.0(patch_hash=biuxdxyvvwd3otdrxnv2y3covi)(@remix-run/node@2.1.0)(@remix-run/react@2.1.0)(@remix-run/server-runtime@2.1.0)(react@18.2.0): + resolution: {integrity: sha512-U4wcghcPaK1+HZvxAvDBOLz4Wbfn9fElfCkq4SFZUG/T1hxH4rg6rVmySoYR2DEbucyV147/xVloVceJ/EQpHQ==} engines: {node: '>=18'} hasBin: true peerDependencies: @@ -17883,9 +17889,9 @@ packages: '@remix-run/router': 1.15.3 '@remix-run/server-runtime': 2.1.0(typescript@5.5.4) '@sentry/cli': 2.50.2 - '@sentry/core': 9.40.0 - '@sentry/node': 9.40.0 - '@sentry/react': 9.40.0(react@18.2.0) + '@sentry/core': 9.46.0 + '@sentry/node': 9.46.0 + '@sentry/react': 9.46.0(react@18.2.0) glob: 10.4.5 react: 18.2.0 yargs: 17.7.2 @@ -17893,6 +17899,7 @@ packages: - encoding - supports-color dev: false + patched: true /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==} @@ -22616,7 +22623,7 @@ packages: optional: true dependencies: chokidar: 3.6.0 - confbox: 0.1.7 + confbox: 0.1.8 defu: 6.1.4 dotenv: 16.4.5 giget: 1.2.3 @@ -23325,12 +23332,8 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - /confbox@0.1.7: - resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} - /confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - dev: false /config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} @@ -23601,6 +23604,14 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + /crypto-js@4.1.1: resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==} dev: false @@ -31774,8 +31785,8 @@ packages: resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} engines: {node: '>= 6'} - /pkce-challenge@4.1.0: - resolution: {integrity: sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==} + /pkce-challenge@5.0.0: + resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} engines: {node: '>=16.20.0'} dev: false @@ -31788,7 +31799,7 @@ packages: /pkg-types@1.1.3: resolution: {integrity: sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==} dependencies: - confbox: 0.1.7 + confbox: 0.1.8 mlly: 1.7.1 pathe: 1.1.2 @@ -38411,6 +38422,7 @@ packages: zod: ^3.24.1 dependencies: zod: 3.25.76 + dev: true /zod-to-json-schema@3.24.5(zod@3.25.76): resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}