(port: number = 3000)
| 8509 | } |
| 8510 | |
| 8511 | async start(port: number = 3000): Promise<void> { |
| 8512 | // Initialize OpenTelemetry logging for PostHog (all log levels) |
| 8513 | const { initOtelLogs, emitLog } = await import('./utils/otel-logs.js'); |
| 8514 | const { setLogHook } = await import('./logger.js'); |
| 8515 | if (initOtelLogs()) { |
| 8516 | setLogHook(emitLog); |
| 8517 | } |
| 8518 | |
| 8519 | // Initialize PostHog error tracking (captures all logger.error() calls as exceptions) |
| 8520 | const { initPostHogErrorTracking } = await import('./utils/posthog.js'); |
| 8521 | initPostHogErrorTracking(); |
| 8522 | |
| 8523 | // Initialize database |
| 8524 | const { initializeDatabase, onPoolError } = await import("./db/client.js"); |
| 8525 | const { runMigrations } = await import("./db/migrate.js"); |
| 8526 | const { getDatabaseConfig } = await import("./config.js"); |
| 8527 | const dbConfig = getDatabaseConfig(); |
| 8528 | if (!dbConfig) { |
| 8529 | throw new Error("DATABASE_URL or DATABASE_PRIVATE_URL environment variable is required"); |
| 8530 | } |
| 8531 | initializeDatabase(dbConfig); |
| 8532 | |
| 8533 | // Escalate pool-level errors to Slack |
| 8534 | onPoolError(() => { |
| 8535 | notifySystemError({ source: 'database-pool', errorMessage: 'Database pool error — check application logs' }); |
| 8536 | }); |
| 8537 | |
| 8538 | await runMigrations(); |
| 8539 | |
| 8540 | // Validate the idempotency backend can actually query its table — fails |
| 8541 | // fast on a stale pool, missing migration, or wrong-credentials boot |
| 8542 | // rather than silently passing every mutating call to a broken backend. |
| 8543 | // No-ops when the store falls back to memoryBackend. |
| 8544 | // |
| 8545 | // Bounded with a 10s deadline because the pg pool has connectionTimeoutMillis=5000 |
| 8546 | // but no statement_timeout — without this race, a hung query (e.g., DB starting |
| 8547 | // up, replica failover) would stall boot indefinitely and starve Fly's TCP healthcheck. |
| 8548 | const { getIdempotencyStore } = await import("./training-agent/idempotency.js"); |
| 8549 | const probe = getIdempotencyStore().probe?.(); |
| 8550 | if (probe) { |
| 8551 | await Promise.race([ |
| 8552 | probe, |
| 8553 | new Promise<never>((_, reject) => |
| 8554 | setTimeout( |
| 8555 | () => reject(new Error("Idempotency backend probe timed out after 10s — check DATABASE_URL and pool reachability")), |
| 8556 | 10_000, |
| 8557 | ), |
| 8558 | ), |
| 8559 | ]); |
| 8560 | } |
| 8561 | |
| 8562 | // Sync organizations from WorkOS and Stripe to local database (dev environment support) |
| 8563 | if (AUTH_ENABLED && workos) { |
| 8564 | const orgDb = new OrganizationDatabase(); |
| 8565 | |
| 8566 | // Sync WorkOS organizations first |
| 8567 | try { |
| 8568 | const result = await orgDb.syncFromWorkOS(workos); |
no test coverage detected