feat: analyze_ats shows full pipeline — easy apply, applied, external ATS breakdown
This commit is contained in:
134
analyze_ats.mjs
134
analyze_ats.mjs
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* analyze_ats.mjs — ATS platform analysis
|
* analyze_ats.mjs — Full job pipeline breakdown
|
||||||
* Reads the applications log and ranks external ATS platforms by job count
|
* Shows Easy Apply vs external ATS platforms vs other skips
|
||||||
* Run: node analyze_ats.mjs
|
* Run: node analyze_ats.mjs
|
||||||
*/
|
*/
|
||||||
import { readFileSync, existsSync } from 'fs';
|
import { readFileSync, existsSync } from 'fs';
|
||||||
@@ -12,76 +12,90 @@ const __dir = dirname(fileURLToPath(import.meta.url));
|
|||||||
const logPath = resolve(__dir, 'data/applications_log.json');
|
const logPath = resolve(__dir, 'data/applications_log.json');
|
||||||
const queuePath = resolve(__dir, 'data/jobs_queue.json');
|
const queuePath = resolve(__dir, 'data/jobs_queue.json');
|
||||||
|
|
||||||
function analyze() {
|
function hostname(url) {
|
||||||
const atsCounts = {};
|
try { return new URL(url).hostname.replace('www.', '').split('.')[0]; } catch { return 'unknown'; }
|
||||||
const atsExamples = {};
|
}
|
||||||
let totalExternal = 0;
|
|
||||||
let unknownCount = 0;
|
|
||||||
|
|
||||||
// From applications log (processed jobs)
|
function analyze() {
|
||||||
|
const all = [];
|
||||||
|
|
||||||
|
if (existsSync(queuePath)) {
|
||||||
|
all.push(...JSON.parse(readFileSync(queuePath, 'utf8')));
|
||||||
|
}
|
||||||
if (existsSync(logPath)) {
|
if (existsSync(logPath)) {
|
||||||
const log = JSON.parse(readFileSync(logPath, 'utf8'));
|
all.push(...JSON.parse(readFileSync(logPath, 'utf8')));
|
||||||
for (const entry of log) {
|
|
||||||
if (entry.status === 'skipped_external_unsupported') {
|
|
||||||
totalExternal++;
|
|
||||||
const platform = entry.ats_platform || 'unknown';
|
|
||||||
atsCounts[platform] = (atsCounts[platform] || 0) + 1;
|
|
||||||
if (!atsExamples[platform]) atsExamples[platform] = [];
|
|
||||||
if (atsExamples[platform].length < 3) {
|
|
||||||
atsExamples[platform].push({ title: entry.title, company: entry.company, url: entry.ats_url });
|
|
||||||
}
|
|
||||||
if (platform === 'unknown') unknownCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// From queue (not yet processed)
|
if (all.length === 0) {
|
||||||
if (existsSync(queuePath)) {
|
console.log('No data yet. Run the searcher first.');
|
||||||
const queue = JSON.parse(readFileSync(queuePath, 'utf8'));
|
return;
|
||||||
for (const job of queue) {
|
}
|
||||||
if (job.status === 'skipped_external_unsupported') {
|
|
||||||
totalExternal++;
|
// Counters
|
||||||
const platform = job.ats_platform || 'unknown';
|
let easyApply = 0, applied = 0, failed = 0, needsAnswer = 0;
|
||||||
|
let recruiterOnly = 0, noEasyApply = 0;
|
||||||
|
const atsCounts = {};
|
||||||
|
const atsExamples = {};
|
||||||
|
let newJobs = 0;
|
||||||
|
|
||||||
|
for (const job of all) {
|
||||||
|
switch (job.status) {
|
||||||
|
case 'new': newJobs++; break;
|
||||||
|
case 'applied': applied++; break;
|
||||||
|
case 'failed': failed++; break;
|
||||||
|
case 'needs_answer': needsAnswer++; break;
|
||||||
|
case 'skipped_recruiter_only': recruiterOnly++; break;
|
||||||
|
case 'skipped_easy_apply_unsupported': noEasyApply++; break;
|
||||||
|
case 'skipped_external_unsupported': {
|
||||||
|
const platform = job.ats_platform || (job.ats_url ? hostname(job.ats_url) : 'unknown');
|
||||||
atsCounts[platform] = (atsCounts[platform] || 0) + 1;
|
atsCounts[platform] = (atsCounts[platform] || 0) + 1;
|
||||||
if (!atsExamples[platform]) atsExamples[platform] = [];
|
if (!atsExamples[platform]) atsExamples[platform] = [];
|
||||||
if (atsExamples[platform].length < 3) {
|
if (atsExamples[platform].length < 3) {
|
||||||
atsExamples[platform].push({ title: job.title, company: job.company, url: job.ats_url });
|
atsExamples[platform].push({ title: job.title, company: job.company, url: job.ats_url });
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Count easy apply eligible (status=new on linkedin with easy apply available)
|
||||||
|
if (job.status === 'new' && job.easy_apply) easyApply++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalExternal = Object.values(atsCounts).reduce((a, b) => a + b, 0);
|
||||||
|
const total = all.length;
|
||||||
|
|
||||||
|
console.log(`\n📊 Job Pipeline Breakdown — ${total} total jobs\n`);
|
||||||
|
console.log(` ✅ Applied: ${applied}`);
|
||||||
|
console.log(` 🔄 Pending (new): ${newJobs}`);
|
||||||
|
console.log(` 💬 Needs answer: ${needsAnswer}`);
|
||||||
|
console.log(` ❌ Failed: ${failed}`);
|
||||||
|
console.log(` 🚫 Recruiter-only: ${recruiterOnly}`);
|
||||||
|
console.log(` ⏭️ No Easy Apply btn: ${noEasyApply}`);
|
||||||
|
console.log(` 🌐 External ATS: ${totalExternal}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (totalExternal > 0) {
|
||||||
|
const sorted = Object.entries(atsCounts).sort((a, b) => b[1] - a[1]);
|
||||||
|
console.log(' External ATS breakdown:');
|
||||||
|
for (const [platform, count] of sorted) {
|
||||||
|
const pct = ((count / totalExternal) * 100).toFixed(0);
|
||||||
|
const bar = '█'.repeat(Math.max(1, Math.round(count / totalExternal * 15)));
|
||||||
|
console.log(` ${platform.padEnd(18)} ${String(count).padEnd(5)} (${pct}%) ${bar}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n Top ATS — examples:');
|
||||||
|
for (const [platform, count] of sorted.slice(0, 4)) {
|
||||||
|
console.log(`\n ${platform} (${count} jobs):`);
|
||||||
|
for (const ex of (atsExamples[platform] || [])) {
|
||||||
|
console.log(` • ${ex.title || '?'} @ ${ex.company || '?'}`);
|
||||||
|
if (ex.url) console.log(` ${ex.url}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`\n💡 Build support for: ${sorted.slice(0, 3).map(([p]) => p).join(', ')} first.`);
|
||||||
|
} else {
|
||||||
|
console.log(' No external ATS data yet — run the applier to classify jobs.');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalExternal === 0) {
|
|
||||||
console.log('No external ATS jobs found yet. Run the applier first.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sorted = Object.entries(atsCounts).sort((a, b) => b[1] - a[1]);
|
|
||||||
|
|
||||||
console.log(`\n📊 ATS Platform Analysis — ${totalExternal} external jobs\n`);
|
|
||||||
console.log('Platform Count % of external Build next?');
|
|
||||||
console.log('─'.repeat(60));
|
|
||||||
|
|
||||||
for (const [platform, count] of sorted) {
|
|
||||||
const pct = ((count / totalExternal) * 100).toFixed(1);
|
|
||||||
const bar = '█'.repeat(Math.round(count / totalExternal * 20));
|
|
||||||
console.log(`${platform.padEnd(16)} ${String(count).padEnd(7)} ${pct.padEnd(15)}% ${bar}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nTop platforms with examples:');
|
|
||||||
for (const [platform, count] of sorted.slice(0, 5)) {
|
|
||||||
console.log(`\n ${platform} (${count} jobs):`);
|
|
||||||
for (const ex of (atsExamples[platform] || [])) {
|
|
||||||
console.log(` • ${ex.title} @ ${ex.company}`);
|
|
||||||
if (ex.url) console.log(` ${ex.url}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unknownCount > 0) {
|
|
||||||
console.log(`\n⚠️ ${unknownCount} jobs with unknown ATS (URL capture may need improvement)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n💡 Recommendation: Build support for the top 2-3 platforms first.');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
analyze();
|
analyze();
|
||||||
|
|||||||
Reference in New Issue
Block a user