Files
cannaiq/backend/scripts/test-treez-client.ts
Kelly a020e31a46 feat(treez): CDP interception client for Elasticsearch API capture
Rewrites Treez platform client to use CDP (Chrome DevTools Protocol)
interception instead of DOM scraping. Key changes:

- Uses Puppeteer Stealth plugin to bypass headless detection
- Intercepts Elasticsearch API responses via CDP Network.responseReceived
- Captures full product data including inventory levels (availableUnits)
- Adds comprehensive TypeScript types for all Treez data structures
- Updates queries.ts with automatic session management
- Fixes product-discovery-treez handler for new API shape

Tested with Best Dispensary: 142 products across 10 categories captured
with inventory data, pricing, and lab results.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-13 19:25:49 -07:00

179 lines
6.2 KiB
TypeScript

/**
* ============================================================
* TREEZ CLIENT TEST SCRIPT
* ============================================================
*
* Tests the Treez CDP interception client using Best Dispensary.
*
* This verifies:
* - Stealth plugin bypasses headless detection
* - CDP intercepts Elasticsearch API responses
* - Products are captured and normalized correctly
* - Inventory data is available
*
* Usage: npx ts-node scripts/test-treez-client.ts
*
* ============================================================
*/
import { fetchProductsFromUrl } from '../src/platforms/treez';
const TEST_URL = 'https://shop.bestdispensary.com/shop';
async function main() {
console.log('='.repeat(60));
console.log('TREEZ CLIENT TEST - CDP INTERCEPTION');
console.log('='.repeat(60));
console.log(`URL: ${TEST_URL}`);
console.log('Method: Puppeteer + Stealth + CDP response capture');
console.log('');
try {
console.log('[Starting] Launching browser with Stealth plugin...\n');
const result = await fetchProductsFromUrl(TEST_URL);
console.log('\n' + '='.repeat(60));
console.log('RESULTS');
console.log('='.repeat(60));
console.log(`Total products: ${result.totalCaptured}`);
console.log(`Store ID: ${result.storeId || 'N/A (custom domain)'}`);
console.log(`Source URL: ${result.sourceUrl}`);
console.log(`Fetched at: ${result.fetchedAt.toISOString()}`);
if (result.products.length === 0) {
console.log('\n[WARNING] No products captured!');
console.log('This could mean:');
console.log(' - Stealth plugin is not bypassing detection');
console.log(' - CDP is not intercepting the correct URLs');
console.log(' - Page structure has changed');
process.exit(1);
}
// Show sample raw product
console.log('\n' + '='.repeat(60));
console.log('SAMPLE RAW PRODUCT (from Elasticsearch)');
console.log('='.repeat(60));
const raw = result.products[0];
console.log(JSON.stringify({
id: raw.id,
name: raw.name,
menuTitle: raw.menuTitle,
brand: raw.brand,
category: raw.category,
subtype: raw.subtype,
status: raw.status,
availableUnits: raw.availableUnits,
customMinPrice: raw.customMinPrice,
customMaxPrice: raw.customMaxPrice,
isActive: raw.isActive,
isAboveThreshold: raw.isAboveThreshold,
}, null, 2));
// Show sample normalized product
console.log('\n' + '='.repeat(60));
console.log('SAMPLE NORMALIZED PRODUCT');
console.log('='.repeat(60));
const normalized = result.normalized[0];
console.log(JSON.stringify({
id: normalized.id,
name: normalized.name,
brand: normalized.brand,
category: normalized.category,
subtype: normalized.subtype,
price: normalized.price,
priceMin: normalized.priceMin,
priceMax: normalized.priceMax,
discountedPrice: normalized.discountedPrice,
discountPercent: normalized.discountPercent,
availableUnits: normalized.availableUnits,
inStock: normalized.inStock,
thcPercent: normalized.thcPercent,
cbdPercent: normalized.cbdPercent,
strainType: normalized.strainType,
effects: normalized.effects,
flavors: normalized.flavors,
imageUrl: normalized.imageUrl,
images: normalized.images?.slice(0, 2),
}, null, 2));
// Brand breakdown
console.log('\n' + '='.repeat(60));
console.log('BRANDS (top 15)');
console.log('='.repeat(60));
const brandCounts = new Map<string, number>();
for (const p of result.normalized) {
const brand = p.brand || 'Unknown';
brandCounts.set(brand, (brandCounts.get(brand) || 0) + 1);
}
const sorted = [...brandCounts.entries()].sort((a, b) => b[1] - a[1]);
console.log(`Total unique brands: ${sorted.length}\n`);
sorted.slice(0, 15).forEach(([brand, count]) => {
console.log(` ${brand}: ${count} products`);
});
// Category breakdown
console.log('\n' + '='.repeat(60));
console.log('CATEGORIES');
console.log('='.repeat(60));
const categoryCounts = new Map<string, number>();
for (const p of result.normalized) {
const cat = p.category || 'Unknown';
categoryCounts.set(cat, (categoryCounts.get(cat) || 0) + 1);
}
const catSorted = [...categoryCounts.entries()].sort((a, b) => b[1] - a[1]);
catSorted.forEach(([cat, count]) => {
console.log(` ${cat}: ${count} products`);
});
// Inventory stats
console.log('\n' + '='.repeat(60));
console.log('INVENTORY STATS');
console.log('='.repeat(60));
const inStock = result.normalized.filter(p => p.inStock).length;
const outOfStock = result.normalized.filter(p => !p.inStock).length;
const hasInventoryData = result.normalized.filter(p => p.availableUnits > 0).length;
console.log(`In stock: ${inStock}`);
console.log(`Out of stock: ${outOfStock}`);
console.log(`With inventory levels: ${hasInventoryData}`);
// Show inventory examples
if (hasInventoryData > 0) {
console.log('\nSample inventory levels:');
result.normalized
.filter(p => p.availableUnits > 0)
.slice(0, 5)
.forEach(p => {
console.log(` ${p.name}: ${p.availableUnits} units`);
});
}
// Check for THC/CBD data
const hasThc = result.normalized.filter(p => p.thcPercent !== null).length;
const hasCbd = result.normalized.filter(p => p.cbdPercent !== null).length;
console.log(`\nWith THC data: ${hasThc} (${Math.round(hasThc / result.totalCaptured * 100)}%)`);
console.log(`With CBD data: ${hasCbd} (${Math.round(hasCbd / result.totalCaptured * 100)}%)`);
// Check for images
const hasImages = result.normalized.filter(p => p.imageUrl).length;
console.log(`With images: ${hasImages} (${Math.round(hasImages / result.totalCaptured * 100)}%)`);
console.log('\n' + '='.repeat(60));
console.log('TEST PASSED');
console.log('='.repeat(60));
} catch (error: any) {
console.error('\n' + '='.repeat(60));
console.error('TEST FAILED');
console.error('='.repeat(60));
console.error(`Error: ${error.message}`);
console.error(error.stack);
process.exit(1);
}
}
main().catch(console.error);