Open a public LinkedIn Learning instructor profile or the authorized instructor analytics portal and extract visible data. Public-profile requests return course titles, canonical course URLs, and the best visible LinkedIn-hosted instructor image. Analytics requests return rendered headers, rows, visible dashboard text, and all accessible pagination results while preserving per-cell text so Learners, Shares, Likes, revenue, and other columns are not mislabelled. This is read-only.
Use Cases
Use for public LinkedIn Learning instructor course/profile-image discovery or for an authorized instructor-portal analytics harvest. Analytics requires the caller's approved authenticated browser profile. If an instructor name is supplied without a canonical profile URL, resolve the opaque instructor slug from LinkedIn Learning search results rather than guessing it. Never enter credentials or bypass checkpoints.
Automation Flow
- For a public instructor profile, use a supplied canonical URL directly after normalizing it to
https://www.linkedin.com/learning/instructors/{instructor-slug}. If only{instructor-name}is supplied, navigate directly tohttps://www.linkedin.com/learning/search?keywords={encodeURIComponent(instructor-name)}, select an exact or unambiguous visible result whose href matches/learning/instructors/, read its canonical href, and navigate there. Never invent the slug. - For analytics, navigate directly to
https://www.linkedin.com/learning/instructor-portal/analyticswithwaitUntil: "domcontentloaded"; do not visit the homepage or use the UI to reach the dashboard. An optional direct form ishttps://www.linkedin.com/learning/instructor-portal/analytics?u={encodeURIComponent(instructor-user-id)}only when the caller explicitly supplies the opaque ID. - In the same browser-agent call, wait for rendering. If the approved session visibly presents an account chooser, activate only a visible control whose exact text is
Personal accountwhen that account context is explicitly authorized, then wait for portal evidence such asMy Courses,Analytics, or a rendered table. Do not submit credentials. - For analytics, run the evaluator below after navigation and settling. It extracts the current table, clicks only visible enabled
Nextcontrols, waits for newly rendered content, and unions rows across pages. Do not fabricate a page parameter.
Analytics evaluator:
(async()=>{
const clean=s=>(s||'').replace(/\s+/g,' ').trim();
const visible=e=>!!e&&(e.offsetWidth||e.offsetHeight||e.getClientRects().length)>0;
const sleep=ms=>new Promise(r=>setTimeout(r,ms));
const snapshot=()=>{
const u=new URL(location.href), body=clean(document.body?.innerText||'').slice(0,12000);
const loginFields=!!document.querySelector('input#username,input#password,input[name="session_key"],input[name="session_password"]');
const challenge=/\/(?:checkpoint|challenge)(?:\/|$)/i.test(u.pathname)||/captcha|security verification|verify your identity|unusual activity|one-time code/i.test(body);
const login=/\/(?:uas\/login|login)(?:\/|$|\?)/i.test(u.href)||(loginFields&&/sign in|join linkedin|email|password/i.test(body));
const portal=/instructor portal|analytics|my courses|learning/i.test(body)&&/\/learning\/instructor-portal\/analytics/i.test(u.pathname);
const consent=/cookie|consent|privacy settings|agree and continue/i.test(body)&&!!document.querySelector('button,[role="button"]');
const tables=[...document.querySelectorAll('main table,table,[role="table"],[role="grid"]')].filter(visible), table=tables[0]||null;
const head=table?.querySelector('thead tr')||table?.querySelector('tr')||table?.querySelector('[role="row"]');
const headers=[...(head?.querySelectorAll(':scope>th,:scope>td,:scope>[role="columnheader"],:scope>[role="gridcell"]')||[])].map(e=>clean(e.innerText||e.textContent));
const rows=[],seen=new Set();
for(const row of [...document.querySelectorAll('main table tr,main [role="row"],table tr,[role="row"]')].filter(visible)){
const cells=[...row.querySelectorAll(':scope>th,:scope>td,:scope>[role="columnheader"],:scope>[role="gridcell"]')].map(e=>clean(e.innerText||e.textContent));
const text=clean(cells.length?cells.join(' | '):row.innerText||row.textContent);
if(!text||seen.has(text)||row.matches('thead tr')||row.querySelector(':scope>th,[role="columnheader"]'))continue;
seen.add(text); rows.push({text,cells,values:headers.length?Object.fromEntries(headers.map((h,i)=>[h,cells[i]??null]).filter(([h])=>h)):null});
}
return {url:u.href.split('#')[0],path:u.pathname,body,loginFields,challenge,login,portal,consent,headers,rows,tableFound:!!table};
};
await sleep(700);
// The caller must explicitly authorize which account's analytics to read; default is no selection.
const accountAuthorized = false;
const chooser=[...document.querySelectorAll('a,button,[role="button"]')].find(e=>visible(e)&&clean(e.innerText||e.textContent)==='Personal account');
if(chooser&&!accountAuthorized){return {status:'account-selection-required',success:false,url:location.href,note:'An account chooser appeared; set accountAuthorized only for an explicitly authorized account before selecting.'};}
if(chooser&&accountAuthorized){chooser.click();await sleep(2500);}
const pages=[];let prior='';
for(let i=0;i<20;i++){
const p=snapshot(),sig=p.rows.map(r=>r.text).join('\n');
if(sig!==prior||!pages.length)pages.push(p); prior=sig;
if(p.challenge||p.login||!p.portal)break;
const next=[...document.querySelectorAll('button,[role="button"],a')].find(e=>visible(e)&&!e.disabled&&e.getAttribute('aria-disabled')!=='true'&&/^next$/i.test(clean(e.innerText||e.getAttribute('aria-label'))));
if(!next)break;
next.click();
let changed=false;
for(let w=0;w<10;w++){await sleep(600);if(snapshot().rows.map(r=>r.text).join('\n')!==sig){changed=true;break;}}
if(!changed)break;
}
const first=pages[0]||snapshot(), last=pages[pages.length-1]||first, rows=[],seen=new Set();
for(const p of pages)for(const r of p.rows)if(!seen.has(r.text)){seen.add(r.text);rows.push(r);}
let status='unknown';
if(first.challenge||last.challenge)status='challenge';else if(first.login||last.login)status='login-required';else if(first.consent&&!first.portal)status='consent-required';else if(first.portal||last.portal)status='accessible';
return {status,success:status==='accessible',url:first.url,finalUrl:last.url,finalPath:last.path,instructorUserId:new URL(last.url).searchParams.get('u'),headers:last.headers.length?last.headers:first.headers,tableFound:pages.some(p=>p.tableFound),rowCount:rows.length,rows,pagesVisited:pages.length,courseTableText:rows.map(r=>r.text).join('\n'),visibleText:first.body,hasLoginFields:first.loginFields,challengeDetected:pages.some(p=>p.challenge),consentDetected:first.consent&&!first.portal,portalEvidence:pages.some(p=>p.portal),message:(first.challenge||first.login||(first.consent&&!first.portal))?first.body.slice(0,1000):null};
})()- For public profiles, run this evaluator on the resolved profile page:
(()=>{
const clean=s=>(s||'').replace(/\s+/g,' ').trim(),visible=e=>!!e&&(e.offsetWidth||e.offsetHeight||e.getClientRects().length)>0;
const abs=h=>{try{const u=new URL(h,location.href);u.search='';u.hash='';return u.href}catch{return null}};
const root=document.querySelector('main')||document.body,courses=[],seen=new Set();
for(const a of [...root.querySelectorAll('a[href*="/learning/"]')].filter(visible)){
const p=new URL(a.href,location.href).pathname;if(!/\/learning\/[^/?#]+/.test(p)||/\/learning\/(?:instructors|topics|search|paths)(?:\/|$)/.test(p))continue;
const url=abs(a.href);if(!url||seen.has(url))continue;seen.add(url);
let card=a;for(let i=0;i<6&&card.parentElement;i++){const q=card.parentElement;if(/^(LI|ARTICLE)$/.test(q.tagName)||q.querySelector('img,[class*="card" i],[data-test*="course" i]')){card=q;break}card=q;}
const title=clean(a.getAttribute('aria-label')||a.querySelector('h2,h3,h4,[class*="title" i]')?.innerText||card.querySelector('h2,h3,h4,[class*="title" i]')?.innerText||a.innerText);if(title&&title.length<=180)courses.push({title,url});
}
const images=[...root.querySelectorAll('img')].map(img=>{try{const u=new URL(img.currentSrc||img.src,location.href);if(!/\.?licdn\.com$/i.test(u.hostname))return null;u.search='';u.hash='';return{src:u.href,width:img.naturalWidth||0,height:img.naturalHeight||0,alt:clean(img.alt).slice(0,120)}}catch{return null}}).filter(Boolean);
const unique=[...new Map(images.map(x=>[x.src,x])).values()],profileImage=unique.filter(x=>x.width>=80&&x.height>=80).sort((a,b)=>b.width*b.height-a.width*a.height)[0]||unique[0]||null;
const body=clean(document.body?.innerText||''),blocked=/uas\/login|authwall|sign in|join linkedin|checkpoint|captcha|security verification/i.test(location.href+' '+body.slice(0,1800))&&!courses.length&&!profileImage;
return{success:!blocked,instructorUrl:location.href.split('?')[0],count:courses.length,courses,profileImage,blocked};
})()Possible Friction Points
- The analytics dashboard has the direct route
https://www.linkedin.com/learning/instructor-portal/analytics; no homepage or search step is needed. - An approved session may expose an account chooser with an exact
Personal accountcontrol. Continue only in the explicitly authorized account context; a renderedMy Coursesmarker is useful portal evidence but is not mandatory when clear analytics/table evidence exists. - Analytics tables can use ordinary tables, ARIA grids, or dynamic markup. Preserve visible headers, cells, and row text; do not assume a fixed column position means revenue, Learners, Shares, or Likes.
- A visible enabled
Nextcontrol may paginate the course table. The evaluator collects pages in order and deduplicates rows; stop when no enabled control remains or no new rows appear. - LinkedIn Learning public profiles use
/learning/instructors/{instructor-slug}. Resolve an unknown slug from Learning search rather than guessing it. - Login redirects, consent gates, checkpoints, CAPTCHA, security verification, missing portal evidence, and failed rendering are access failures, not empty analytics or course results.
- Keep navigation and extraction read-only and same-origin. Do not collect credentials, cookies, tokens, unrelated profile data, or hidden API responses.