Last active
January 3, 2026 16:00
-
-
Save louim/def1bf8f76392bb32018c84c25e94afa to your computer and use it in GitHub Desktop.
Are you curious how much time you spend tupleing with colleagues? Go to https://production.tuple.app/profile/usage. Paste script in console. Enjoy!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (async function () { | |
| // ========== CONFIGURE DATE RANGE HERE ========== | |
| const startDate = new Date("2025-01-01"); | |
| const endDate = new Date("2025-12-31T23:59:59"); | |
| // =============================================== | |
| const timePerPerson = {}; | |
| const logs = []; | |
| // Parse time string like "46 min", "1 hr 30 min", "2 hr" to minutes | |
| function parseTimeToMinutes(timeStr) { | |
| let totalMinutes = 0; | |
| const hrMatch = timeStr.match(/(\d+)\s*hr/); | |
| const minMatch = timeStr.match(/(\d+)\s*min/); | |
| if (hrMatch) totalMinutes += parseInt(hrMatch[1]) * 60; | |
| if (minMatch) totalMinutes += parseInt(minMatch[1]); | |
| return totalMinutes; | |
| } | |
| // Format minutes to readable string | |
| function formatMinutes(minutes) { | |
| const hrs = Math.floor(minutes / 60); | |
| const mins = minutes % 60; | |
| if (hrs > 0 && mins > 0) return `${hrs} hr ${mins} min`; | |
| if (hrs > 0) return `${hrs} hr`; | |
| return `${mins} min`; | |
| } | |
| // Check if date is within range | |
| function isInDateRange(dateStr) { | |
| const date = new Date(dateStr); | |
| return date >= startDate && date <= endDate; | |
| } | |
| // Extract data from a page's HTML | |
| function extractFromPage(doc) { | |
| const callItems = doc.querySelectorAll("[data-call-item]"); | |
| let callsInRange = 0; | |
| let callsOutOfRange = 0; | |
| callItems.forEach((call) => { | |
| const callDate = call.getAttribute("data-date"); | |
| if (!isInDateRange(callDate)) { | |
| callsOutOfRange++; | |
| return; | |
| } | |
| callsInRange++; | |
| const popup = call.querySelector("div.space-y-2"); | |
| if (!popup) return; | |
| const participantRows = popup.querySelectorAll(":scope > div"); | |
| participantRows.forEach((row) => { | |
| const nameEl = row.querySelector("p.font-medium"); | |
| const timeEl = row.querySelector("p.ml-auto"); | |
| if (nameEl && timeEl) { | |
| const name = nameEl.textContent.trim(); | |
| const time = timeEl.textContent.trim(); | |
| const minutes = parseTimeToMinutes(time); | |
| if (name && minutes > 0) { | |
| timePerPerson[name] = (timePerPerson[name] || 0) + minutes; | |
| } | |
| } | |
| }); | |
| }); | |
| return { callsInRange, callsOutOfRange }; | |
| } | |
| // Get total number of pages from pagination | |
| function getLastPage(doc) { | |
| const lastLink = doc.querySelector(".pagination .last a"); | |
| if (lastLink) { | |
| const match = lastLink.href.match(/page=(\d+)/); | |
| return match ? parseInt(match[1]) : 1; | |
| } | |
| const pageLinks = doc.querySelectorAll(".pagination .page a"); | |
| let maxPage = 1; | |
| pageLinks.forEach((link) => { | |
| const match = link.href.match(/page=(\d+)/); | |
| if (match) maxPage = Math.max(maxPage, parseInt(match[1])); | |
| }); | |
| return maxPage; | |
| } | |
| // Fetch and parse a page | |
| async function fetchPage(pageNum) { | |
| const url = `/profile/usage?page=${pageNum}`; | |
| logs.push(`Fetching page ${pageNum}...`); | |
| const response = await fetch(url); | |
| const html = await response.text(); | |
| const parser = new DOMParser(); | |
| return parser.parseFromString(html, "text/html"); | |
| } | |
| try { | |
| logs.push( | |
| `Date range: ${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}` | |
| ); | |
| let totalCallsInRange = 0; | |
| let totalCallsOutOfRange = 0; | |
| let stats = extractFromPage(document); | |
| totalCallsInRange += stats.callsInRange; | |
| totalCallsOutOfRange += stats.callsOutOfRange; | |
| const totalPages = getLastPage(document); | |
| logs.push(`Found ${totalPages} total pages`); | |
| for (let page = 2; page <= totalPages; page++) { | |
| const doc = await fetchPage(page); | |
| stats = extractFromPage(doc); | |
| totalCallsInRange += stats.callsInRange; | |
| totalCallsOutOfRange += stats.callsOutOfRange; | |
| } | |
| const sorted = Object.entries(timePerPerson).sort((a, b) => b[1] - a[1]); | |
| logs.push(""); | |
| logs.push("========================================"); | |
| logs.push("TOTAL TUPLE TIME PER PERSON"); | |
| logs.push( | |
| `${startDate.toLocaleDateString()} - ${endDate.toLocaleDateString()}` | |
| ); | |
| logs.push( | |
| `(${totalCallsInRange} calls in range, ${totalCallsOutOfRange} excluded)` | |
| ); | |
| logs.push("========================================"); | |
| logs.push(""); | |
| sorted.forEach(([name, minutes]) => { | |
| logs.push(`${name}: ${formatMinutes(minutes)}`); | |
| }); | |
| logs.push(""); | |
| logs.push("========================================"); | |
| console.log(logs.join("\n")); | |
| return Object.fromEntries( | |
| sorted.map(([name, mins]) => [name, formatMinutes(mins)]) | |
| ); | |
| } catch (error) { | |
| console.error("Error:", error); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment