← back to Tk10640 Redirect Prune

broken-redirect-probe.mjs

34 lines

// TK-10653 — VERIFIED broken-redirect classification. Sample N random redirects with a
// non-home /products/ TARGET. For each, probe BOTH source-liveness + target-liveness:
//   source LIVE(active+pub)        -> INERT (product served, redirect ignored)
//   source DEAD + target LIVE      -> GOOD migration redirect (fires -> live product) KEEP
//   source DEAD + target DEAD      -> BROKEN (fires -> 404) = customer-facing bug + prune cand
import { readFileSync, writeFileSync } from 'node:fs';
const txt=readFileSync(`${process.env.HOME}/Projects/secrets-manager/.env`,'utf8');
const env={}; for(const l of txt.split('\n')){const m=l.match(/^([A-Z0-9_]+)=(.*)$/); if(m) env[m[1]]=m[2].replace(/^["']|["']$/g,'');}
const STORE=env.SHOPIFY_STORE_DOMAIN, TOKEN=env.SHOPIFY_FULL_ACCESS_TOKEN, API='2024-10';
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
async function gql(q,v={}){for(let a=0;a<10;a++){try{const res=await fetch(`https://${STORE}/admin/api/${API}/graphql.json`,{method:'POST',headers:{'Content-Type':'application/json','X-Shopify-Access-Token':TOKEN},body:JSON.stringify({query:q,variables:v})});const tx=await res.text();let j;try{j=JSON.parse(tx);}catch{await sleep(1500*(a+1));continue;}if(j.errors){if(/THROTTLED/i.test(JSON.stringify(j.errors))){await sleep(1500*(a+1));continue;}throw new Error(JSON.stringify(j.errors));}return j.data;}catch(e){if(a<9){await sleep(1500*(a+1));continue;}throw e;}}}
async function liveHandle(h){const d=await gql(`query($h:String!){productByHandle(handle:$h){status onlineStoreUrl}}`,{h});const p=d.productByHandle;return !!(p&&p.status==='ACTIVE'&&p.onlineStoreUrl);}
const N=Number((process.argv.find(a=>a.startsWith('--n='))||'').split('=')[1]||400);
const norm=s=>(s||'').split('?')[0].split('#')[0];
const rows=readFileSync('data/redirects-dump.jsonl','utf8').trim().split('\n').map(JSON.parse)
  .filter(r=>{const t=norm(r.target);return /^\/products\//.test(t);});
for(let i=rows.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[rows[i],rows[j]]=[rows[j],rows[i]];}
let inert=0, good=0, broken=0, other=0; const brokenEx=[];
for(let i=0;i<N&&i<rows.length;i++){ const r=rows[i];
  const sh=r.path.startsWith('/products/')?r.path.replace(/^\/products\//,'').split('?')[0].split('#')[0]:null;
  const th=norm(r.target).replace(/^\/products\//,'');
  const srcLive= sh? await liveHandle(sh) : false;  // non-product sources can't be a live product
  if(srcLive){ inert++; continue; }
  const tgtLive= await liveHandle(th);
  if(tgtLive){ good++; } else { broken++; if(brokenEx.length<40) brokenEx.push({path:r.path,target:r.target}); }
  if(i%100===99) process.stderr.write(`  ${i+1}/${N}: inert=${inert} good=${good} broken=${broken}\n`);
}
const n=Math.min(N,rows.length);
writeFileSync('data/broken-redirect-sample.json', JSON.stringify(brokenEx,null,2));
console.log(JSON.stringify({sample:'random product-target redirects', probed:n,
  inert_source_live:inert, good_migration_source_dead_target_live:good, BROKEN_source_dead_target_dead:broken,
  broken_rate:+(broken/n).toFixed(3), est_broken_in_64730:Math.round(broken/n*64730),
  good_keep_rate:+(good/n).toFixed(3)},null,2));