Skip to main content

Chrome Extension Collecting Empty Data? Same-Site Cookies with Different Formats Break ID Mapping

· 3 min read

Encountered this issue while building an e-commerce data collection system for a client. Here's the root cause and solution.

TL;DR

The 1688 platform sets two cookies (last_mid and unb) both containing user IDs, but in different formats (b2b-xxx vs plain digits). The database stores the b2b- prefix format. The original code iterated the cookie array in the outer loop, matching unb first — the strict equality check failed, and all collected data was written with zero values because it couldn't map to a shop.

Fix: Put the cookie key priority list in the outer loop and the cookie array in the inner loop, ensuring high-priority keys are checked first.

Problem

After collecting daily shop reports from 1688, all dashboard metrics in the database were 0, while inquiry data had values:

shop_id | report_date | reveal_cnt | uv | pay_amt | effective_inq_users
2 | 2026-05-13 | 0 | 0 | 0.00 | 56
2 | 2026-05-14 | 0 | 0 | 0.00 | 41

Server log: No shop found for memberId: 2214126315258

But the database stored platform_account_id as b2b-2214126315258ad300.

Root Cause

Two cookies with different formats on the same domain

unb=2214126315258                          # plain digits
last_mid=b2b-2214126315258ad300 # b2b- prefix + suffix

The shops.platform_account_id column stores the b2b- prefix format. The code used strict equality:

const match = mapping.find(m => m.platform_account_id === memberId);
// "2214126315258" !== "b2b-2214126315258ad300" → no match

Iteration order trap

The original code had the cookie array in the outer loop and the key list in the inner loop:

// ❌ Wrong: cookie array outer, key priority ineffective
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var i = 0; i < cookies.length; i++) { // outer: cookies
var pair = cookies[i].trim();
for (var k = 0; k < keys.length; k++) { // inner: keys
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

document.cookie order is not guaranteed. If unb appears before last_mid in the array, it gets matched first — returning the plain-digit format and making last_mid priority useless.

Solution

Swap loop levels: key priority list in the outer loop, cookie array in the inner loop:

// ✅ Correct: key priority list in outer loop
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var k = 0; k < keys.length; k++) { // outer: iterate keys by priority
for (var i = 0; i < cookies.length; i++) { // inner: search all cookies
var pair = cookies[i].trim();
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

The key list is iterated by priority in the outer loop, so last_mid is always checked first regardless of document.cookie order, guaranteeing the user ID format matches the database.

Verification

// Confirm both cookies exist in browser console
document.cookie.split(';')
.filter(c => /last_mid|unb/.test(c.trim()))
.map(c => c.trim())
// ['unb=2214126315258', 'last_mid=b2b-2214126315258ad300']

Note

This issue doesn't affect chrome.cookies.get() in extension pages — it queries by name directly. But when parsing document.cookie strings, always pay attention to loop levels. Also, if your extension passes data via postMessage, watch out for the targetOrigin wildcard security risk; and if messages are processed twice after hot reload, you'll need to manually manage listener lifecycle.


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.

Chrome Extension Processing Messages Twice After Hot Reload? WXT HMR Stacks Listeners

· 2 min read

Encountered this issue while building an e-commerce data collection Chrome extension for a client. Here's the root cause and solution.

TL;DR

WXT framework's HMR re-executes content scripts on file changes but doesn't clean up old window.addEventListener('message', ...) handlers. Each hot reload adds another listener — every postMessage fires all instances.

Fix: Before registering a new listener, retrieve the old one from a window variable and call removeEventListener.

Problem

Browser console showed each message caught by two different instances:

content.js:114 [CCL] CCL_SHOP_REPORT_DAILY daily caught - instance: nxctn6
content.js:2 [CCL] CCL_SHOP_REPORT_DAILY daily caught - instance: t6jce7

Every postMessage processed twice, causing duplicate requests to the backend.

Root Cause

WXT (a Vite-based Chrome extension framework) in dev mode triggers HMR on content script changes:

  1. The new content script module loads and executes
  2. A new window.addEventListener('message', messageListener) is registered
  3. The old listener function remains in memory — HMR doesn't clean up DOM event listeners

Result: multiple independent message listeners on window, each postMessage triggering all of them.

Solution

At the content script entry point, remove the old listener before registering the new one:

const instanceId = Math.random().toString(36).slice(2, 8);

// Retrieve old listener reference
const prevListener = (window as any).__cclMessageListener;
if (prevListener) {
window.removeEventListener('message', prevListener);
}

// Define new listener
const messageListener = (event: MessageEvent) => {
// ... handling logic
};

// Store current reference (for next HMR cycle)
(window as any).__cclMessageListener = messageListener;

// Register
window.addEventListener('message', messageListener);

Key insight: removeEventListener requires the exact same function reference as addEventListener. By storing the function on window, the next HMR cycle can retrieve and remove the old one correctly. This ensures only one active message listener exists at any time, regardless of how many hot reloads occur.

If you're also seeing postMessage data leaking to third-party iframes or empty collected data due to cookie format mismatch, check those issues too.

Note

  • This issue isn't limited to WXT — any framework that hot-reloads content scripts (Plasmo, CRXJS, etc.) can encounter it
  • window variables persist until page refresh; HMR only replaces script modules, not window properties
  • Production builds don't have this issue (content script loads once), but it causes hard-to-debug duplicate requests during development

Chrome Extension Writing Test Data to Production? Add a DRY-RUN Switch

· 3 min read

TL;DR

Chrome extensions submitting collected data via API write directly to the production database — even during development and testing. A 3-layer DRY-RUN switch solves this: set an env variable in .env.development → client reads it and adds an X-Dry-Run header → server intercepts the header and returns a data preview without writing. Production never sets the variable, so it's completely unaffected.

Chrome Extension Service Worker Can't Read Login Token? Cross-Context Token Sync

· 3 min read

TL;DR

Chrome extension uses a sidepanel as the UI. User logs in, token goes to localStorage. But the Service Worker (background script) has no localStorage — calling it throws ReferenceError. Fix: after login, send the token to the Service Worker via chrome.runtime.sendMessage, which writes it to chrome.storage.local. Sidepanel reads localStorage, Service Worker reads chrome.storage.local.

Two WSL2 + Docker Networking Pitfalls: Silently Occupied Ports & Host Mode localhost Unreachable

· 5 min read

TL;DR

Two common networking pitfalls with WSL2 + Docker Desktop:

  1. Silently occupied port: When a Docker container maps 5432, SSH tunnel localhost:5432 connects to the container's PostgreSQL instead of the remote server — the password is correct, but you're hitting the wrong instance
  2. Host mode localhost unreachable: network_mode: host shares the Docker utility VM's network, not WSL2's — curl localhost:8080 fails

Container Port Unreachable from WSL2? The Docker Desktop network_mode:host Trap

· 3 min read

Encountered this issue while building an AI data analytics platform (Airflow + PostgreSQL) for a client. Here's the root cause and solution.

TL;DR

On Docker Desktop for Windows (WSL2 backend), network_mode: host container ports are unreachable from the WSL2 host. The container shows the port listening, but curl localhost:PORT returns connection refused. The fix: use network_mode: !reset in your override file to remove host mode, then switch to bridge + external network + port mapping.

WordPress Block Theme Changes Not Taking Effect? FSE Development Troubleshooting Guide

· 7 min read

Encountered these five issues repeatedly while developing WordPress Block Themes for clients. Each one took significant debugging time. This guide covers the root causes and provides ready-to-use solutions.

TL;DR

Five issues ranked by frequency: file changes not applying (database cache overrides files), block nesting errors (unclosed comments), child theme content not rendering (missing post-content block), SVG icons disappearing (WP_Filesystem polluted by plugins), and WP-CLI mail failures (SMTP plugins don't hook in CLI). Each scenario includes copy-paste diagnostic commands.

WooCommerce Blocks Showing core/missing After Upgrade? Block Theme Troubleshooting Guide

· 7 min read

Ran into these four WooCommerce-specific issues while building Block Themes for clients. Each relates to how FSE architecture interacts with WooCommerce's block system. Documenting the troubleshooting process to help others working on WooCommerce theme development.

TL;DR

Four common issues in WooCommerce Block Theme development: block rename causing core/missing (block names gained -block suffix after upgrade), product archive 404 after shop slug change (rewrite cache not flushed), template HTML mismatch with Gutenberg save (dynamic block validation failure), and Cart/Checkout templates not auto-assigning (manual assignment required). Each scenario includes copy-paste fixes.

Embedding a Timeline in SITE123 Event Pages? First Dodge These 5 Platform Limits

· 5 min read

TL;DR

Embedding a self-hosted timeline into a SITE123 event page hits 5 platform walls: Custom Code can't target a page or position, scripts run before DOM is ready, selectors hit hidden elements, float layout pushes adjacent blocks out of place, and platform cache injects the script twice. Fix: JS DOM manipulation + horizontal fishbone layout + DOMContentLoaded wrapper.