- 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>
225 lines
7.0 KiB
TypeScript
225 lines
7.0 KiB
TypeScript
/**
|
|
* Explore Jane API to understand data structure
|
|
* Usage: npx ts-node scripts/test-jane-api-explore.ts
|
|
*/
|
|
|
|
import puppeteer from 'puppeteer-extra';
|
|
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
|
|
|
|
puppeteer.use(StealthPlugin());
|
|
|
|
async function main() {
|
|
console.log('Exploring Jane API from browser context...\n');
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
|
|
// Intercept network requests to find store data API calls
|
|
const capturedResponses: Array<{ url: string; data: any }> = [];
|
|
|
|
await page.setRequestInterception(true);
|
|
page.on('request', (req) => req.continue());
|
|
|
|
page.on('response', async (response) => {
|
|
const url = response.url();
|
|
if (url.includes('iheartjane.com') &&
|
|
(url.includes('/stores') || url.includes('/search') || url.includes('algolia'))) {
|
|
try {
|
|
const text = await response.text();
|
|
if (text.startsWith('{') || text.startsWith('[')) {
|
|
const data = JSON.parse(text);
|
|
capturedResponses.push({ url, data });
|
|
console.log(`Captured: ${url.substring(0, 100)}...`);
|
|
}
|
|
} catch {
|
|
// Not JSON
|
|
}
|
|
}
|
|
});
|
|
|
|
// Visit Jane to establish session
|
|
console.log('Visiting Jane stores page to capture network requests...');
|
|
await page.goto('https://www.iheartjane.com/stores', {
|
|
waitUntil: 'networkidle2',
|
|
timeout: 60000,
|
|
});
|
|
|
|
console.log(`\nCaptured ${capturedResponses.length} API responses`);
|
|
|
|
for (const resp of capturedResponses) {
|
|
console.log(`\n--- ${resp.url.substring(0, 80)} ---`);
|
|
const keys = Object.keys(resp.data);
|
|
console.log('Keys:', keys);
|
|
|
|
// Check for stores array
|
|
if (resp.data.stores && Array.isArray(resp.data.stores)) {
|
|
console.log(`Stores count: ${resp.data.stores.length}`);
|
|
const firstStore = resp.data.stores[0];
|
|
if (firstStore) {
|
|
console.log('First store keys:', Object.keys(firstStore));
|
|
console.log('Sample:', JSON.stringify(firstStore, null, 2).substring(0, 500));
|
|
}
|
|
}
|
|
|
|
// Check for hits (Algolia)
|
|
if (resp.data.hits && Array.isArray(resp.data.hits)) {
|
|
console.log(`Hits count: ${resp.data.hits.length}`);
|
|
const firstHit = resp.data.hits[0];
|
|
if (firstHit) {
|
|
console.log('First hit keys:', Object.keys(firstHit));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Look for __NEXT_DATA__ or similar embedded data
|
|
console.log('\n--- Checking for embedded page data ---');
|
|
const pageData = await page.evaluate(() => {
|
|
// Check for Next.js data
|
|
const nextData = (window as any).__NEXT_DATA__;
|
|
if (nextData?.props?.pageProps?.stores) {
|
|
return {
|
|
source: '__NEXT_DATA__',
|
|
storeCount: nextData.props.pageProps.stores.length,
|
|
firstStore: nextData.props.pageProps.stores[0],
|
|
};
|
|
}
|
|
|
|
// Check for any global store data
|
|
const win = window as any;
|
|
if (win.stores) return { source: 'window.stores', data: win.stores };
|
|
if (win.__stores) return { source: 'window.__stores', data: win.__stores };
|
|
|
|
return null;
|
|
});
|
|
|
|
if (pageData) {
|
|
console.log('Found embedded data:', pageData.source);
|
|
console.log('Store count:', pageData.storeCount);
|
|
if (pageData.firstStore) {
|
|
console.log('First store keys:', Object.keys(pageData.firstStore));
|
|
console.log('Sample:', JSON.stringify({
|
|
id: pageData.firstStore.id,
|
|
name: pageData.firstStore.name,
|
|
city: pageData.firstStore.city,
|
|
state: pageData.firstStore.state,
|
|
}, null, 2));
|
|
}
|
|
} else {
|
|
console.log('No embedded page data found');
|
|
}
|
|
|
|
// Try alternative API endpoints from browser context
|
|
console.log('\n--- Testing alternative API endpoints ---');
|
|
|
|
// Try the map endpoint
|
|
const mapData = await page.evaluate(async () => {
|
|
try {
|
|
const res = await fetch('https://api.iheartjane.com/v1/stores/map?per_page=100');
|
|
if (res.ok) return await res.json();
|
|
} catch {}
|
|
return null;
|
|
});
|
|
|
|
if (mapData) {
|
|
console.log('\n/v1/stores/map response:');
|
|
console.log('Keys:', Object.keys(mapData));
|
|
if (mapData.stores?.[0]) {
|
|
console.log('First store keys:', Object.keys(mapData.stores[0]));
|
|
}
|
|
}
|
|
|
|
// Try index endpoint
|
|
const indexData = await page.evaluate(async () => {
|
|
try {
|
|
const res = await fetch('https://api.iheartjane.com/v1/stores/index?per_page=10');
|
|
if (res.ok) return await res.json();
|
|
} catch {}
|
|
return null;
|
|
});
|
|
|
|
if (indexData) {
|
|
console.log('\n/v1/stores/index response:');
|
|
console.log('Keys:', Object.keys(indexData));
|
|
if (indexData.stores?.[0]) {
|
|
console.log('First store keys:', Object.keys(indexData.stores[0]));
|
|
}
|
|
}
|
|
|
|
// Try with state parameter
|
|
const stateData = await page.evaluate(async () => {
|
|
try {
|
|
const res = await fetch('https://api.iheartjane.com/v1/stores?state=AZ&per_page=10');
|
|
if (res.ok) return await res.json();
|
|
} catch {}
|
|
return null;
|
|
});
|
|
|
|
if (stateData) {
|
|
console.log('\n/v1/stores?state=AZ response:');
|
|
console.log('Keys:', Object.keys(stateData));
|
|
console.log('Stores count:', stateData.stores?.length);
|
|
if (stateData.stores?.[0]) {
|
|
console.log('First store keys:', Object.keys(stateData.stores[0]));
|
|
console.log('Sample:', JSON.stringify(stateData.stores[0], null, 2).substring(0, 300));
|
|
}
|
|
}
|
|
|
|
// Try Algolia directly for stores
|
|
console.log('\n--- Testing Algolia for stores ---');
|
|
const algoliaStores = await page.evaluate(async () => {
|
|
try {
|
|
// Common Algolia search pattern
|
|
const res = await fetch('https://search.iheartjane.com/1/indexes/stores-production/query', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Algolia-Application-Id': 'HKXSXRD7RA',
|
|
'X-Algolia-API-Key': 'YjZhYjQxZjU4ZTNjMTRhYzExZTk2YjU2MzliMGE4ZTE5YjJkMmZkZTI2ODllYTY2MThlMzQ3Y2QxOTFkMjI5Y3RhZ0ZpbHRlcnM9',
|
|
},
|
|
body: JSON.stringify({
|
|
query: 'Arizona',
|
|
hitsPerPage: 20,
|
|
}),
|
|
});
|
|
if (res.ok) return await res.json();
|
|
} catch {}
|
|
return null;
|
|
});
|
|
|
|
if (algoliaStores) {
|
|
console.log('Algolia stores-production response:');
|
|
console.log('Keys:', Object.keys(algoliaStores));
|
|
console.log('Hits count:', algoliaStores.hits?.length);
|
|
if (algoliaStores.hits?.[0]) {
|
|
console.log('First hit keys:', Object.keys(algoliaStores.hits[0]));
|
|
console.log('Sample:', JSON.stringify(algoliaStores.hits[0], null, 2).substring(0, 500));
|
|
}
|
|
}
|
|
|
|
// Check if there's a /v2 endpoint
|
|
const v2Data = await page.evaluate(async () => {
|
|
try {
|
|
const res = await fetch('https://api.iheartjane.com/v2/stores?per_page=10');
|
|
if (res.ok) return await res.json();
|
|
} catch {}
|
|
return null;
|
|
});
|
|
|
|
if (v2Data) {
|
|
console.log('\n/v2/stores response:');
|
|
console.log('Keys:', Object.keys(v2Data));
|
|
if (v2Data.stores?.[0]) {
|
|
console.log('First store keys:', Object.keys(v2Data.stores[0]));
|
|
}
|
|
}
|
|
|
|
await browser.close();
|
|
console.log('\nDone!');
|
|
}
|
|
|
|
main().catch(console.error);
|