Instrukcja użycia→ Ściągnij wtyczkę Tampermonkey do przeglądarki
→ W opcjach wtyczki włącz opcję „Allow User Scripts” (dzięki temu będzie można korzystać z własnych skryptów)
→ Wklej w Tampermonkey poniższy skrypt widgetu
→ Wejdź do Microsoft Clarity, do sekcji nagrań, i pobieraj opisy
Skrypt widgeta
// ==UserScript==
// @name MS Clarity Ściąganie opisów
// @namespace http://tampermonkey.net/
// @version 1.8
// @description Widget do generowania opisów sesji — z dosztukowywaniem (pomija już opisane), przyciskiem STOP i raportem błędów
// @author Paweł Piekarski
// @match https://clarity.microsoft.com/*
// @icon https://clarity.microsoft.com/blog/wp-content/uploads/2025/02/siteIcon.png
// @grant none
// ==/UserScript==
(function() {
'use strict';
const clarityIconSvg = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 4L20 12L4 20V4Z" fill="#1960bf"/>
</svg>`;
const BLAD_AI_TEKST = "I'm sorry, something went wrong";
// Stan procesu — pozwala na dosztukowywanie i przerywanie
const stan = {
aktywny: false,
stop: false,
watcherId: null,
statystyki: { pominiete: 0, klikniete: 0 }
};
// --- LOGIKA WYDOBYWANIA DANYCH ---
function pobierzKarty() {
return Array.from(document.querySelectorAll('div[data-testid="sessionCard"]'));
}
function maOpis(card) {
return !!card.querySelector('div.insightsContent');
}
function czyBladAI(card) {
const content = card.querySelector('div.insightsContent');
return content && content.textContent.includes(BLAD_AI_TEKST);
}
function maPoprawnyOpis(card) {
return maOpis(card) && !czyBladAI(card);
}
function pobierzDostepneNagrania() {
const countEl = document.querySelector('.sessionCount strong');
if (!countEl) return '...';
return countEl.textContent.replace(/\D/g, '');
}
function pobierzUserId(card) {
const badges = Array.from(card.querySelectorAll('div[class*="recordings_cardBadge"]'));
const userBadge = badges.find(el => el.textContent.includes('User ID:'));
if (userBadge) {
const rawText = userBadge.textContent.replace('User ID:', '').trim();
return rawText.split(/\s+/)[0];
}
return '';
}
function pobierzMetadaneSesji(card) {
const meta = {};
// Entry & Exit URLs
const rows = card.querySelectorAll('div[class*="recordings_cardRow"]');
rows.forEach(row => {
const strong = row.querySelector('strong');
const span = row.querySelector('span[class*="oneLineText"]');
if (strong && span) {
const label = strong.textContent.trim().replace(':', '');
if (label === 'Entry') meta.entry = row.getAttribute('title') || span.textContent.trim();
if (label === 'Exit') meta.exit = row.getAttribute('title') || span.textContent.trim();
}
});
// Referrer
const referrerRow = Array.from(rows).find(r => {
const s = r.querySelector('strong');
return s && s.textContent.includes('Referrer');
});
if (referrerRow) {
meta.referrer = referrerRow.getAttribute('title') || referrerRow.querySelector('span')?.textContent.trim() || '';
}
// Duration, Clicks, Pages — z wiersza multiValuedRow
const multiRow = card.querySelector('div[class*="recordings_multiValuedRow"]');
if (multiRow) {
const strongs = multiRow.querySelectorAll('strong');
const spans = multiRow.querySelectorAll('span');
strongs.forEach((s, i) => {
const label = s.textContent.trim().replace(':', '').toLowerCase();
const val = spans[i] ? spans[i].textContent.trim() : '';
if (label === 'duration') meta.duration = val;
if (label === 'clicks') meta.clicks = val;
if (label === 'pages') meta.pages = val;
});
}
// Date & time
const dateSection = card.querySelector('div[class*="recordings_cardDateSection"]');
if (dateSection) {
const dateSpans = dateSection.querySelectorAll('span');
const parts = Array.from(dateSpans).map(s => s.textContent.trim());
meta.date = parts.join(' ');
}
// Badges (User ID, Country, Browser, Device, itd.)
const badges = Array.from(card.querySelectorAll('div[class*="recordings_cardBadge"]'));
const badgeTexts = badges.map(b => b.textContent.trim()).filter(Boolean);
meta.userId = '';
meta.otherBadges = [];
badgeTexts.forEach(txt => {
if (txt.startsWith('User ID:')) {
meta.userId = txt.replace('User ID:', '').trim().split(/\s+/)[0];
} else {
meta.otherBadges.push(txt);
}
});
return meta;
}
// --- PROCES GENEROWANIA (z dosztukowywaniem) ---
function startProcess() {
// Drugi klik podczas pracy = STOP
if (stan.aktywny) {
stan.stop = true;
updateStatus('Zatrzymywanie...');
return;
}
const limit = parseInt(document.getElementById('helper-limit').value) || 0;
const wszystkie = pobierzKarty();
// Bierzemy tylko karty BEZ opisu — dzięki temu kolejne uruchomienie
// "dosztukowuje" następną partię zamiast klikać od początku
const doZrobienia = [];
let pominiete = 0;
for (const card of wszystkie) {
if (limit > 0 && doZrobienia.length >= limit) break;
if (maOpis(card)) continue; // już ma opis (lub błąd AI) — pomijamy
const btn = card.querySelector('button[data-testid="sessionGenerateInsightsButton"]');
if (!btn || btn.disabled) {
pominiete++; // np. zbyt krótka sesja — nie liczy się do limitu
continue;
}
doZrobienia.push({ card, btn });
}
stan.statystyki = { pominiete, klikniete: doZrobienia.length };
if (doZrobienia.length === 0) {
const zOpisem = wszystkie.filter(maPoprawnyOpis).length;
updateStatus(zOpisem > 0
? `Brak nowych sesji do opisania (${pominiete > 0 ? pominiete + ' pominięto' : 'przewiń, by doładować'})`
: 'Brak sesji do opisania');
return;
}
stan.aktywny = true;
stan.stop = false;
ustawPrzyciskGeneruj(true);
let index = 0;
const procesuj = () => {
if (stan.stop || index >= doZrobienia.length) {
stan.statystyki.klikniete = index;
nasluchujOpisy(doZrobienia.slice(0, index).map(d => d.card));
return;
}
doZrobienia[index].btn.click();
index++;
updateStatus(`Klikanie: ${index}/${doZrobienia.length}`);
setTimeout(procesuj, 700 + Math.random() * 500); // losowy odstęp
};
procesuj();
}
function nasluchujOpisy(karty) {
if (karty.length === 0) {
zakonczProces(0, 0);
return;
}
let proba = 0;
// Timeout skaluje się z liczbą sesji (min. 60 s, maks. 5 min)
const maxProb = Math.min(300, Math.max(60, karty.length * 10));
stan.watcherId = setInterval(() => {
proba++;
// Śledzimy KONKRETNE karty (referencje), nie indeksy na liście —
// odporne na doładowanie/przesunięcie listy w trakcie
const gotowe = karty.filter(maOpis);
const sukcesy = gotowe.filter(c => !czyBladAI(c)).length;
updateStatus(`Opisy: ${sukcesy}/${karty.length}`);
if (gotowe.length === karty.length || proba >= maxProb || stan.stop) {
clearInterval(stan.watcherId);
zakonczProces(sukcesy, gotowe.length);
}
}, 1000);
}
function zakonczProces(sukcesy, odpowiedziane) {
stan.aktywny = false;
stan.stop = false;
ustawPrzyciskGeneruj(false);
pokazRaport(sukcesy, odpowiedziane);
refreshLiczniki();
}
// --- UI & RAPORT ---
function ustawPrzyciskGeneruj(pracuje) {
const btn = document.getElementById('btn-generate');
btn.textContent = pracuje ? 'STOP' : 'GENERUJ OPISY';
btn.style.background = pracuje ? '#d93025' : '#1960bf';
}
function updateStatus(msg) {
document.getElementById('helper-status').textContent = `Status: ${msg}`;
document.getElementById('helper-report').style.display = 'none';
}
function pokazRaport(finalneSukcesy, juzOdpowiedziane) {
const reportEl = document.getElementById('helper-report');
const statusEl = document.getElementById('helper-status');
const lacznie = pobierzKarty().filter(maPoprawnyOpis).length;
statusEl.textContent = `Status: Gotowe (+${finalneSukcesy}, łącznie: ${lacznie})`;
const bledyAI = juzOdpowiedziane - finalneSukcesy;
const niedokonczone = stan.statystyki.klikniete - juzOdpowiedziane;
const linie = [];
if (stan.statystyki.pominiete > 0) {
linie.push(`• ${stan.statystyki.pominiete} pominięto — zbyt krótka sesja`);
}
if (bledyAI > 0) {
linie.push(`• ${bledyAI} błąd generowania AI`);
}
if (niedokonczone > 0) {
linie.push(`• ${niedokonczone} bez odpowiedzi (timeout/stop)`);
}
if (linie.length > 0) {
reportEl.innerHTML = linie.join('<br>');
reportEl.style.display = 'block';
}
}
function refreshLiczniki() {
const maxEl = document.getElementById('helper-max-val');
if (maxEl) maxEl.textContent = pobierzDostepneNagrania();
const infoEl = document.getElementById('helper-info');
if (infoEl) {
const karty = pobierzKarty();
const zOpisem = karty.filter(maPoprawnyOpis).length;
infoEl.textContent = `Załadowane: ${karty.length} • z opisem: ${zOpisem}`;
}
}
// --- ANONIMIZACJA URL ---
function wykryjDomeneSerwisu(dane) {
for (const d of dane) {
if (d.entry) {
try {
const url = new URL(d.entry);
return url.hostname;
} catch { /* entry może być skrócone */ }
}
}
return null;
}
function anonimizujTekst(tekst, domena) {
if (!domena || !tekst) return tekst;
const bezWww = domena.replace(/^www\./, '');
const zWww = 'www.' + bezWww;
const esc = s => s.replace(/\./g, '\\.');
const wzorce = [zWww, bezWww, domena]
.filter((v, i, a) => a.indexOf(v) === i)
.sort((a, b) => b.length - a.length);
let wynik = tekst;
for (const wzorzec of wzorce) {
wynik = wynik.replace(new RegExp(esc(wzorzec), 'gi'), 'firma.pl');
}
return wynik;
}
function usunParametryUrl(url) {
if (!url) return url;
try {
const parsed = new URL(url);
return parsed.origin + parsed.pathname;
} catch {
// Fallback dla URLi bez schematu lub skróconych
return url.split('?')[0].split('#')[0];
}
}
// --- POBIERANIE .MD / JSON ---
// Eksport obejmuje WSZYSTKIE załadowane karty (limit dotyczy tylko generowania)
function pobierzDaneSesji() {
return pobierzKarty().map(card => {
const meta = pobierzMetadaneSesji(card);
const opis = maPoprawnyOpis(card)
? card.querySelector('div.insightsContent').textContent.trim()
: null;
return { ...meta, opis };
});
}
function generujMarkdown(dane) {
const dzis = new Date().toISOString().slice(0, 10);
const projekt = document.title || 'MS Clarity';
const zOpisem = dane.filter(d => d.opis);
const bezOpisu = dane.length - zOpisem.length;
const domena = wykryjDomeneSerwisu(dane);
const anon = tekst => anonimizujTekst(tekst, domena);
let md = `# Clarity – opisy sesji\n`;
md += `Projekt: ${anon(projekt)} | Data eksportu: ${dzis}\n`;
md += `Sesji: ${dane.length} (opisów: ${zOpisem.length}`;
if (bezOpisu > 0) md += `, brak opisu: ${bezOpisu}`;
md += `)\n\n`;
dane.forEach((d, i) => {
md += `## Nagranie ${i + 1}\n`;
const pola = [];
if (d.date) pola.push(`Data: ${d.date}`);
if (d.duration) pola.push(`Czas: ${d.duration}`);
if (d.clicks) pola.push(`Kliknięcia: ${d.clicks}`);
if (d.pages) pola.push(`Strony: ${d.pages}`);
if (d.otherBadges && d.otherBadges.length > 0) pola.push(d.otherBadges.join(', '));
if (pola.length > 0) md += pola.join(' | ') + `\n`;
if (d.entry) md += `Entry: ${anon(usunParametryUrl(d.entry))}\n`;
if (d.exit) md += `Exit: ${anon(usunParametryUrl(d.exit))}\n`;
if (d.referrer) md += `Referrer: ${usunParametryUrl(d.referrer)}\n`;
md += `\n`;
if (d.opis) {
md += anon(d.opis) + `\n`;
} else {
md += `*Brak opisu*\n`;
}
md += `\n`;
});
return md;
}
function pobierzMD() {
const dane = pobierzDaneSesji();
if (dane.length === 0) {
const btn = document.getElementById('btn-download');
btn.textContent = 'BRAK SESJI!';
btn.style.background = '#d93025';
setTimeout(() => {
btn.textContent = 'POBIERZ .MD';
btn.style.background = '#2e7d32';
}, 2000);
return;
}
const md = generujMarkdown(dane);
const dzis = new Date().toISOString().slice(0, 10);
const nazwaPliku = `ms-clarity-${dzis}.md`;
const blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = nazwaPliku;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
const btn = document.getElementById('btn-download');
btn.textContent = 'POBRANO!';
setTimeout(() => btn.textContent = 'POBIERZ .MD', 2000);
}
// --- WIDGET UI ---
const container = document.createElement('div');
container.id = 'clarity-helper-widget';
container.innerHTML = `
<div style="background: #ffffff; border: 1px solid #ced4da; border-radius: 10px; padding: 15px; box-shadow: 0 8px 24px rgba(0,0,0,0.2); width: 220px; font-family: sans-serif; color: #333;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; border-bottom: 1px solid #f0f0f0; padding-bottom: 8px;">
<div style="display: flex; align-items: center;">
<div style="margin-right: 8px; display: flex; align-items: center;">${clarityIconSvg}</div>
<h3 style="margin: 0; font-size: 14px; color: #000;">Ściągnij opisy nagrań</h3>
</div>
<div style="display: flex; align-items: center;">
<button id="btn-toggle" title="Zwiń / Rozwiń" style="background: none; border: none; cursor: pointer; font-size: 14px; color: #888; line-height: 1; padding: 0 2px;">▼</button>
</div>
</div>
<div id="helper-body">
<label style="font-size: 11px; color: #888; font-weight: bold;">ILE NOWYCH OPISÓW (MAX: <span id="helper-max-val">...</span>):</label>
<input type="number" id="helper-limit" value="10" style="width: 100%; padding: 6px; margin: 4px 0 4px 0; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; box-sizing: border-box;">
<div id="helper-info" style="font-size: 10px; color: #888; margin-bottom: 8px;">Załadowane: ... • z opisem: ...</div>
<div id="helper-status" style="font-size: 12px; margin-bottom: 4px; color: #1960bf; font-weight: 500; background: #f0f7ff; padding: 4px 8px; border-radius: 4px;">Status: Gotowy</div>
<div id="helper-report" style="font-size: 10px; color: #d93025; margin-bottom: 12px; padding: 0 8px; display: none; line-height: 1.2;"></div>
<button id="btn-generate" style="width: 100%; background: #1960bf; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-bottom: 8px; font-size: 12px;">GENERUJ OPISY</button>
<button id="btn-copy" style="width: 100%; background: #444; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; margin-bottom: 8px; font-size: 12px;">KOPIUJ JSON</button>
<button id="btn-download" style="width: 100%; background: #2e7d32; color: white; border: none; padding: 10px; border-radius: 5px; cursor: pointer; font-weight: bold; font-size: 12px;">POBIERZ .MD</button>
<div style="margin-top: 15px; text-align: center; border-top: 1px solid #f0f0f0; padding-top: 12px;">
<a href="https://pawelpiekarski.pl" target="_blank" title="Odwiedź stronę">
<img src="https://pawelpiekarski.pl/wp-content/uploads/2024/02/Logo-pawelpiekarski.pl-v850.png" alt="Paweł Piekarski" style="max-width: 130px; height: auto; opacity: 0.8; transition: opacity 0.2s ease-in-out;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'">
</a>
</div>
</div>
</div>
`;
Object.assign(container.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: '10000' });
document.body.appendChild(container);
// --- Toggle minimalizacji ---
let zwiniety = false;
document.getElementById('btn-toggle').addEventListener('click', () => {
zwiniety = !zwiniety;
const body = document.getElementById('helper-body');
const toggleBtn = document.getElementById('btn-toggle');
body.style.display = zwiniety ? 'none' : 'block';
toggleBtn.textContent = zwiniety ? '▲' : '▼';
});
document.getElementById('btn-generate').addEventListener('click', startProcess);
document.getElementById('btn-copy').addEventListener('click', () => {
const dane = pobierzKarty().map(card => ({
userId: pobierzUserId(card),
description: maPoprawnyOpis(card)
? card.querySelector('div.insightsContent').textContent.trim()
: 'BRAK OPISU'
}));
navigator.clipboard.writeText(JSON.stringify(dane, null, 2)).then(() => {
const btn = document.getElementById('btn-copy');
btn.textContent = "SKOPIOWANO!";
setTimeout(() => btn.textContent = "KOPIUJ JSON", 2000);
});
});
document.getElementById('btn-download').addEventListener('click', pobierzMD);
setInterval(refreshLiczniki, 3000);
refreshLiczniki();
})();