- Add WorkerFingerprint interface with timezone, city, state, ip, locale - Store fingerprint in TaskWorker after preflight passes - Pass fingerprint through TaskContext to handlers - Apply timezone via CDP and locale via Accept-Language header - Ensures browser fingerprint matches proxy IP location This fixes anti-detect detection where timezone/locale mismatch with proxy IP was getting blocked by Cloudflare. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
131 lines
3.9 KiB
TypeScript
131 lines
3.9 KiB
TypeScript
/**
|
|
* Count Jane stores - v2: Try Algolia store search
|
|
* Usage: npx ts-node scripts/count-jane-stores-v2.ts
|
|
*/
|
|
|
|
import puppeteer from 'puppeteer-extra';
|
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
|
|
puppeteer.use(StealthPlugin());
|
|
|
|
const STATES = [
|
|
'AZ', 'CA', 'CO', 'FL', 'IL', 'MA', 'MI', 'NV', 'NJ', 'NY', 'OH', 'PA', 'WA', 'OR'
|
|
];
|
|
|
|
async function main() {
|
|
console.log('Counting Jane stores by exploring state pages...\n');
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
const allStores: Map<number, any> = new Map();
|
|
|
|
await page.setRequestInterception(true);
|
|
page.on('request', (req) => {
|
|
const type = req.resourceType();
|
|
if (['image', 'font', 'media', 'stylesheet'].includes(type)) {
|
|
req.abort();
|
|
} else {
|
|
req.continue();
|
|
}
|
|
});
|
|
|
|
page.on('response', async (response) => {
|
|
const url = response.url();
|
|
const contentType = response.headers()['content-type'] || '';
|
|
if (url.includes('iheartjane.com') && contentType.includes('json')) {
|
|
try {
|
|
const json = await response.json();
|
|
// Look for stores in any response
|
|
if (json.stores && Array.isArray(json.stores)) {
|
|
for (const s of json.stores) {
|
|
if (s.id) allStores.set(s.id, s);
|
|
}
|
|
}
|
|
// Also check hits (Algolia format)
|
|
if (json.hits && Array.isArray(json.hits)) {
|
|
for (const s of json.hits) {
|
|
if (s.id) allStores.set(s.id, s);
|
|
}
|
|
}
|
|
} catch {}
|
|
}
|
|
});
|
|
|
|
// First visit the main stores page
|
|
console.log('Visiting main stores page...');
|
|
await page.goto('https://www.iheartjane.com/stores', {
|
|
waitUntil: 'networkidle0',
|
|
timeout: 60000,
|
|
});
|
|
await new Promise(r => setTimeout(r, 3000));
|
|
|
|
// Try to scroll to load more stores
|
|
console.log('Scrolling to load more...');
|
|
for (let i = 0; i < 5; i++) {
|
|
await page.evaluate(() => window.scrollBy(0, 1000));
|
|
await new Promise(r => setTimeout(r, 1000));
|
|
}
|
|
|
|
// Try clicking "Load More" if it exists
|
|
try {
|
|
const loadMore = await page.$('button:has-text("Load More"), [class*="load-more"]');
|
|
if (loadMore) {
|
|
console.log('Clicking Load More...');
|
|
await loadMore.click();
|
|
await new Promise(r => setTimeout(r, 3000));
|
|
}
|
|
} catch {}
|
|
|
|
// Extract stores from DOM as fallback
|
|
const domStores = await page.evaluate(() => {
|
|
const storeElements = document.querySelectorAll('[data-store-id], [class*="StoreCard"], [class*="store-card"]');
|
|
return storeElements.length;
|
|
});
|
|
|
|
console.log(`\nStores from DOM elements: ${domStores}`);
|
|
|
|
await browser.close();
|
|
|
|
// Count by state
|
|
const byState: Record<string, number> = {};
|
|
for (const store of allStores.values()) {
|
|
const state = store.state || 'Unknown';
|
|
byState[state] = (byState[state] || 0) + 1;
|
|
}
|
|
|
|
console.log('\n=== JANE STORE COUNTS ===\n');
|
|
console.log(`Unique stores captured: ${allStores.size}`);
|
|
|
|
if (allStores.size > 0) {
|
|
console.log('\nBy State:');
|
|
const sorted = Object.entries(byState).sort((a, b) => b[1] - a[1]);
|
|
for (const [state, count] of sorted.slice(0, 20)) {
|
|
console.log(` ${state}: ${count}`);
|
|
}
|
|
|
|
// Check Arizona specifically
|
|
const azStores = Array.from(allStores.values()).filter(s =>
|
|
s.state === 'Arizona' || s.state === 'AZ'
|
|
);
|
|
console.log(`\nArizona stores: ${azStores.length}`);
|
|
if (azStores.length > 0) {
|
|
console.log('AZ stores:');
|
|
for (const s of azStores.slice(0, 10)) {
|
|
console.log(` - ${s.name} (ID: ${s.id}) - ${s.city}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note about total
|
|
console.log('\n--- Note ---');
|
|
console.log('Jane uses server-side rendering. To get full store count,');
|
|
console.log('you may need to check their public marketing materials or');
|
|
console.log('iterate through known store IDs.');
|
|
}
|
|
|
|
main().catch(console.error);
|