Skip to main content

11 posts tagged with "saas-development"

View all tags

Python task marked failed but no error? try/except swallowed the exception

· 5 min read

Debugging a silent failure where a document sync task marked everything failed in a RAG knowledge base project — full writeup below.

TL;DR

A shared method was refactored with a new parameter signature, but one caller was missed. The caller passed arguments under the old contract and threw TypeError — except the call sat inside a try/except that quietly funneled the exception into a failed counter. No crash, no ERROR in the logs, just a number ticking up. These "silent failures" are the hardest bugs to track down. Two fixes: grep all callers after a signature refactor; and make except blocks log or re-raise, never swallow silently.

Node.js ESM Dynamic Import Can't Find Module? Check the File Extension

· 3 min read

Encountered this issue while building a SaaS analytics platform for a client. Here's the root cause and solution.

TL;DR

In Node.js ESM mode, import('./path/to/module') doesn't auto-resolve ./path/to/module.js. If the TypeScript build output is missing .js extensions, the module throws ERR_MODULE_NOT_FOUND. When this import is inside deferred logic (timers, conditionals), the app starts fine but crashes later — PM2 shows a climbing restart count.

Fix: Ensure all ESM dynamic imports include .js extensions, and automate this in the build pipeline.

Problem

PM2 showed the app restarting continuously:

│ name             │ ↺    │ status │ uptime │
│ analytics-api │ 9 │ online │ 28m │

Error log repeated every few minutes:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/dist/domains/video/cleanup'
imported from /app/dist/server.js

But the file clearly exists:

$ ls dist/domains/video/
cleanup.js executor.js queue.js

Root Cause

ESM doesn't auto-resolve file extensions

Node.js CommonJS (require()) automatically tries .js, .json, and other extensions. ESM (import) does not.

// ❌ ESM can't find the module
import('./domains/video/cleanup')
// Node.js looks for: ./domains/video/cleanup (exact path, no extension)
// Actual file: ./domains/video/cleanup.js

// ✅ Must include .js extension
import('./domains/video/cleanup.js')

Deferred imports hide the problem

The import was inside a deferred execution:

// server.ts — doesn't execute immediately on startup
import('./domains/video/cleanup.js').then(({ startCleanupScheduler }) => {
startCleanupScheduler(); // triggers seconds later
});

The app starts successfully (DB connection, port binding all fine). When the timer fires, the import fails → process crashes → PM2 restarts → starts fine again → timer fires again → crashes again. This creates a crash loop.

Why did it work before?

Previous deployment used a build script that included a post-build step to fix import paths. One deployment skipped this step and deployed raw tsc output — tsc doesn't modify import paths in output files.

Solution

1. Write .js extensions in TypeScript source

TypeScript officially recommends writing .js extensions even in .ts files:

// ✅ Write .js even in .ts source files
import('./domains/video/cleanup.js').then(({ startCleanupScheduler }) => {
startCleanupScheduler();
});

2. Automated post-build fix (recommended)

Add an import-fixing script to the build pipeline:

{
"scripts": {
"build": "tsc && node fix-imports.js"
}
}

Core logic of fix-imports.js:

import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';

function fixImports(dir) {
for (const file of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, file.name);
if (file.isDirectory()) {
fixImports(fullPath);
} else if (file.name.endsWith('.js')) {
let content = readFileSync(fullPath, 'utf8');
// Fix dynamic imports
const fixed = content.replace(
/import\(['"](\.[^'"]+)['"]\)/g,
(match, path) => path.endsWith('.js') ? match : match.replace(path, path + '.js')
);
// Fix static imports
const fixed2 = fixed.replace(
/from\s+['"](\.[^'"]+)['"]/g,
(match, path) => path.endsWith('.js') ? match : match.replace(path, path + '.js')
);
if (fixed2 !== content) {
writeFileSync(fullPath, fixed2);
}
}
}
}

The build pipeline automatically appends .js to all relative import paths, keeping TypeScript source extension-free while preventing module-not-found errors in ESM deployments.

Note

  • This only affects Node.js ESM mode ("type": "module" or .mjs files). CommonJS is unaffected.
  • Static imports (import ... from './foo') have the same limitation, not just dynamic import(); import hoisting also causes another common issue — dotenv runs after the import chain, leaving env vars undefined
  • Using tsx or ts-node in development won't show this error (they auto-resolve extensions), but node dist/server.js in production will fail.
  • PM2 crash loop signature: restart count (↺) keeps growing, uptime never exceeds a few minutes.

Save 70% of Your AI Coding Plan Quota by Replacing ZhiPu Search with Tavily MCP

· 3 min read

When using ZhiPu GLM Coding Plan with Claude Code, I noticed my monthly quota draining faster than expected. The culprit? Built-in MCP tools — web-search-prime and web-reader — were consuming coding conversation quota for every web search and page fetch.

TL;DR

Replace ZhiPu's built-in web-search-prime and web-reader MCP services with Tavily MCP. Result: free 1000 searches/month with zero impact on coding quota.

Build a Custom MCP Toolkit with Python FastMCP to Connect Any AI Model

· 8 min read

While building AI Agent systems for clients, we found that different tasks require vastly different model capabilities and costs: vision models for image analysis, lightweight models for text completion, and local models for internal data queries. MCP (Model Context Protocol) turns each capability into an independent tool that AI clients invoke on demand.

TL;DR

Build a custom MCP Server in 30 minutes with Python FastMCP, connecting any OpenAI-compatible API based on scenario and cost. This article demonstrates the full workflow using the Doubao vision model, with extension templates for text generation, image generation, TTS, and more.

Skip Login in Playwright Tests with Custom Fixtures

· 3 min read

Encountered this issue while building an AI Agent platform. Here's the root cause and solution.

TL;DR

Use Playwright's test.extend() to create a custom fixture that injects auth token into localStorage via page.addInitScript() before page load. Tests use authenticatedPage instead of page, automatically getting logged-in state without repeating login in each test.

Bypass Supabase Auth for Playwright E2E Testing Without Login

· 4 min read

Encountered this issue while building an AI Agent SaaS platform for a client. Here's the root cause and solution.

TL;DR

E2E tests shouldn't depend on real OAuth login flows. By detecting localStorage test markers in the useAuth hook, you can inject mock auth state directly and skip Supabase initialization. Also change the Zustand store's loading default to false to prevent AuthGuard from showing an infinite spinner.