survey.mjs 4.5 KB

123456789101112131415161718192021222324252627282930
  1. import {chromium} from 'playwright';
  2. import {readFileSync,writeFileSync,mkdirSync} from 'node:fs';
  3. const env={};for(const line of readFileSync('/Users/mac/.config/xiaoshu/legacy-sync-nightly.env','utf8').split('\n')){const m=line.match(/^(?:export\s+)?(XIAOSHU_[A-Z0-9_]+)=(.*)$/);if(m)env[m[1]]=m[2].replace(/^['"]|['"]$/g,'');}
  4. const token=env.XIAOSHU_SYNC_OPERATOR_TOKEN;if(!token)throw Error('Missing operator');
  5. const origin=process.env.ADMIN_ORIGIN||'http://127.0.0.1:4300',out='tmp/admin-data-2026-10-15';mkdirSync(out,{recursive:true,mode:0o700});
  6. const userR=await fetch('https://server.xiaoshu.pro/parse/users/me',{headers:{'X-Parse-Application-Id':env.XIAOSHU_PARSE_APP_ID,'X-Parse-Session-Token':token}}),me=await userR.json();if(!userR.ok)throw Error('Invalid operator');
  7. const paths=process.argv.slice(2).length?process.argv.slice(2):['dashboard','members','stores','schedule','coaches','payroll','lessons','learning-reports','support-requests','vocabulary','special-training','privacy-requests','operator-accounts'];
  8. const browser=await chromium.launch({headless:true,executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'}),results=[];
  9. try{for(const path of paths){
  10. const ctx=await browser.newContext({viewport:{width:1440,height:1000}});await ctx.addInitScript(auth=>localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth)),{objectId:me.objectId,sessionToken:token,username:'性能验收',roles:me.roles,operationsRole:'ops-manager',isSuperAdmin:true});
  11. const page=await ctx.newPage(),requests=[],pending=new Set(),errors=[];let start,lastFinished=0;
  12. await page.addInitScript(()=>{window.__perfLong=[];new PerformanceObserver(list=>window.__perfLong.push(...list.getEntries().map(e=>({start:e.startTime,duration:e.duration})))).observe({type:'longtask',buffered:true});});
  13. page.on('pageerror',e=>errors.push(e.message));
  14. page.on('request',r=>{if(!r.url().includes('/cloud-functions/'))return;const p=r.postDataJSON()?.params||{},entry={operation:p.operation||p.action,path:new URL(r.url()).pathname,startMs:Date.now()-start,params:p};requests.push(entry);r.__entry=entry;pending.add(r);});
  15. page.on('requestfinished',async r=>{if(!r.__entry)return;const entry=r.__entry;try{const resp=await r.response(),body=await resp.body(),data=JSON.parse(body.toString()),t=r.timing();Object.assign(entry,{elapsedMs:Date.now()-start-entry.startMs,ttfbMs:Math.round(t.responseStart-t.requestStart),downloadMs:Math.round(t.responseEnd-t.responseStart),bytes:body.length,status:resp.status(),success:data.success!==false&&data.retcode!==-1,total:data.data?.total??null});}catch(e){entry.error=String(e.message).slice(0,100);}finally{lastFinished=Date.now();pending.delete(r);}});
  16. page.on('requestfailed',r=>{if(r.__entry){r.__entry.failed=true;pending.delete(r);lastFinished=Date.now();}});
  17. start=Date.now();await page.goto(origin+'/admin/'+path,{waitUntil:'domcontentloaded'});
  18. while(Date.now()-start<35000){await page.waitForTimeout(100);if(requests.length&&pending.size===0&&Date.now()-lastFinished>300)break;}
  19. await page.evaluate(()=>new Promise(r=>requestAnimationFrame(()=>requestAnimationFrame(r))));
  20. const first={settledMs:Date.now()-start,requests:requests.map(r=>({...r})),pending:pending.size,errors:[...errors],longTasks:await page.evaluate(()=>window.__perfLong),resources:await page.evaluate(()=>performance.getEntriesByType('resource').map(r=>({name:new URL(r.name).pathname,duration:Math.round(r.duration),transfer:r.transferSize,decoded:r.decodedBodySize}))),alertCount:await page.getByRole('alert').count()};
  21. let back;
  22. if(path!=='dashboard' && !process.env.FIRST_ONLY){
  23. await page.locator('#admin-sidebar a[href="/admin/dashboard"]').last().click();await page.waitForTimeout(1200);
  24. while(pending.size&&Date.now()-lastFinished<22000)await page.waitForTimeout(100);
  25. const count=requests.length,begin=Date.now();await page.locator('#admin-sidebar a[href="/admin/'+path+'"]').last().click();await page.waitForTimeout(500);
  26. while(pending.size&&Date.now()-begin<25000)await page.waitForTimeout(100);
  27. back={settledMs:Date.now()-begin,requests:requests.slice(count)};
  28. }
  29. results.push({path,first,back});writeFileSync(out+'/survey.json',JSON.stringify({at:new Date().toISOString(),results},null,2),{mode:0o600});console.log(JSON.stringify({path,firstMs:first.settledMs,requests:first.requests.map(r=>({op:r.operation,ms:r.elapsedMs,ttfb:r.ttfbMs,bytes:r.bytes,status:r.status})),returnMs:back?.settledMs,returnRequests:back?.requests.map(r=>r.operation),errors}));await ctx.close();
  30. }}finally{await browser.close();}