Skip to main content

4 posts tagged with "ecommerce"

View all tags

Old Browser Extension Stops Syncing Mid-Month? Version Skew from a Re-purposed Sync Flag

Β· 5 min read

After the "crowd asset" collection feature of our browser extension shipped, support started hearing the same story: some users open crowd assets mid-month and see "already up to date" β€” while their data stops at the 1st. The un-upgraded old extension had stopped collecting, and would only resume on the first day of the next month.

Encountered this while building e-commerce data collection tooling β€” a one-stop e-commerce operations solution from data collection to smart analytics; the collection flag is served by the backend, and old and new extension versions share the same server-side table.

TL;DR​

The new release gave the existing boolean flag is_current_month a new meaning ("this month's collection window is covered"); un-upgraded clients read it with the old meaning ("data is current"), their idempotency check short-circuited, and collection stopped mid-month. This is not a bug to fix β€” it's version skew: one field, two interpretations. The impact has a natural self-healing boundary (the flag flips false next month); the response is upgrade prompts + idempotent upsert (no rollback), and the prevention is: semantic changes must come with a new field.

Symptoms​

New extension collects: writes the current-month row, is_current_month = true

Old extension syncs: reads that row β†’ hits the "up-to-date" check β†’ stops collecting
User's view: mid-month, "crowd assets" says already current; data frozen at month start
Next month, day 1: is_current_month flips false β†’ old version resumes (self-heals)

The eerie part: server data is perfectly correct, the new extension works, the old extension's code never changed β€” the only failure mode is "old version reading rows written by the new version".

Root Cause​

Textbook version skew. The flag's name stayed, its meaning moved: to the new release it means "current-month window covered"; the old release, following its own historical semantics, reads the same row as "data is current" β€” and its perfectly-correct idempotency check ("already current β†’ skip re-collection") short-circuits the whole collection. Both sides' logic is right; the wrong part is letting two semantics share one field.

Client (and every client-side) version distribution is outside the server's control: after a release, old and new versions coexist for weeks as the norm. Any change to a server-side field's meaning is read by every historical version β€” the same lesson as the classic feature-flag mistake: re-purpose an old flag to carry a new meaning, and readers act on the old meaning.

Solution​

Immediate response: prompt upgrades + idempotent writes​

When releasing the version with the new collection logic, explicitly ask users to upgrade β€” the old version will not recover on its own within the month. Server and data need no rollback: collection writes are idempotent upserts, so interleaved old/new writes produce no dirty data, and everything realigns when the flag flips next month.

Long-term prevention: new semantics, new field​

Move the "window semantics" off the boolean onto a new field with an explicit window; old clients that don't know the field can't misread it:

// anti-pattern: old flag re-purposed for new semantics
{ "is_current_month": true }

// correct: new semantics in a new field, explicit and comparable
{ "collected_window": "2026-08", "source_version": "2.3.0" }

The idempotency key decouples from business semantics: old versions judge by the old field, new versions by the new one, no cross-contamination.

Design principle: give every flag a self-healing boundary​

Prefer binding flags to natural time boundaries (month, day) rather than absolute semantics like "current". The month boundary here capped the worst case at one month; without such a boundary, skew is permanent and only a release can fix it.

Notes

  • You don't control the client version distribution. Treat any semantic change to a server-side field as a compatibility change: enumerate all readers and verify each interpretation path.
  • A self-healing boundary is a safety net, not a plan β€” "it'll be fine next month" is not an acceptable long-term state; make the business call explicitly.
  • Idempotent writes (upsert) are the precondition for a safe overlap period; collection pipelines without idempotency will produce duplicates or conflicts the moment versions coexist.
  • When debugging collection pipelines, isolate production data first β€” see adding a DRY-RUN mode to your Chrome extension collector.

FAQ​

What are the best practices for feature flags?​

One flag, one meaning. New semantics get a new field, not a re-purposed old flag. Bind flags to explicit time windows or versions. Before changing any meaning, enumerate every reader β€” especially un-upgraded clients β€” and confirm they won't interpret new values with old semantics. The mid-month collection stop here is the direct consequence of re-using an old flag.

How do I keep server-side fields backward compatible?​

Add, never mutate: existing fields keep their meaning, type, and behavior; new semantics go into new fields; old clients ignore what they don't recognize. Keeping a field's name while changing its meaning is a breaking change to every existing reader.

What should old clients do when they read data with new semantics?​

Three steps: bound the impact to a self-healing window (a calendar-month flip restores collection automatically); prompt upgrades at release to shorten the overlap window; keep server writes idempotent (upsert) so interleaved old/new writes blend safely β€” no rollback, no data cleanup.

CCLEE

Independent developer, 24 years in e-commerce, focused on grounding AI in real business scenarios.

Work with me

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.


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

Fix FSE Block Theme Style Preview Single Color Block and Front Page Blank Canvas

Β· 4 min read

Encountered these two issues while developing a WordPress FSE Block Theme for a client. Here are the root causes and solutions.

TL;DR​

  1. Style variation palette/gradients replace rather than merge -- declaring only 1 color drops all others. You must include the complete list and only change what differs.
  2. Hardcoding patterns in front-page.html causes Site Editor blank canvas and prevents users from editing the layout -- switch to content-driven architecture.