Created
April 25, 2026 00:29
-
-
Save compwron/bcb1d21e506151ef956eec062779a8d1 to your computer and use it in GitHub Desktop.
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
| // Paste this into the Firefox console on https://production.tuple.app/profile/usage | |
| // It scrapes all pages and downloads a CSV file. | |
| (async () => { | |
| const allRows = []; | |
| const totalPages = 47; // from pagination: Last » links to page=47 | |
| const TIMEOUT_MS = 10000; | |
| const DELAY_MS = 1000; | |
| let consecutiveEmpty = 0; | |
| for (let page = 1; page <= totalPages; page++) { | |
| console.log(`Fetching page ${page}/${totalPages}...`); | |
| const url = `/profile/usage?page=${page}`; | |
| let resp; | |
| try { | |
| resp = await Promise.race([ | |
| fetch(url, { credentials: 'same-origin' }), | |
| new Promise((_, reject) => | |
| setTimeout(() => reject(new Error('timeout')), TIMEOUT_MS) | |
| ), | |
| ]); | |
| } catch (err) { | |
| console.error(`Page ${page} fetch failed: ${err.message} — skipping`); | |
| continue; | |
| } | |
| if (!resp.ok) { | |
| console.error(`Page ${page} returned HTTP ${resp.status} — skipping`); | |
| continue; | |
| } | |
| const html = await resp.text(); | |
| const doc = new DOMParser().parseFromString(html, 'text/html'); | |
| // Detect login redirect: if page has no call items AND has a login form, bail | |
| if (doc.querySelector('input[type="password"]') || doc.querySelector('form[action*="login"]')) { | |
| console.error('Session expired — got redirected to login. Re-login and retry.'); | |
| break; | |
| } | |
| const calls = doc.querySelectorAll('[data-call-item]'); | |
| console.log(` Page ${page}: found ${calls.length} calls`); | |
| if (calls.length === 0) { | |
| consecutiveEmpty++; | |
| if (consecutiveEmpty >= 3) { | |
| console.warn(`3 consecutive empty pages — stopping early at page ${page}`); | |
| break; | |
| } | |
| } else { | |
| consecutiveEmpty = 0; | |
| } | |
| for (const call of calls) { | |
| const isoDate = call.getAttribute('data-date'); | |
| const dt = new Date(isoDate); | |
| const dateStr = dt.toLocaleDateString('en-CA'); // YYYY-MM-DD | |
| const timeEl = call.querySelector('time'); | |
| const timeStr = timeEl ? timeEl.textContent.trim() : ''; | |
| // Normalize "3:20 pm" → "3:20 PM" | |
| const timeNorm = timeStr.replace(/am/i, 'AM').replace(/pm/i, 'PM'); | |
| // Duration from the visible (non-mobile) duration element | |
| const durationEl = call.querySelector('.hidden.xs\\:block p'); | |
| const duration = durationEl ? durationEl.textContent.trim() : ''; | |
| // Participants from the facepile tooltip | |
| const participantEls = call.querySelectorAll( | |
| '.group\\/facepile .bg-gray-50 .font-medium, .group\\/facepile .dark\\:bg-gray-800 .font-medium' | |
| ); | |
| let participants = []; | |
| if (participantEls.length > 0) { | |
| for (const el of participantEls) { | |
| // Text is duplicated (name shown twice) — take first word-chunk that looks like an email | |
| const text = el.textContent.trim(); | |
| const email = text.split(/\s+/)[0]; | |
| if (email.includes('@')) { | |
| participants.push(email); | |
| } | |
| } | |
| } | |
| const participantStr = participants.join(';'); | |
| allRows.push(`${dateStr},${timeNorm},${duration},"${participantStr}"`); | |
| } | |
| // Delay between fetches | |
| if (page < totalPages) { | |
| await new Promise(r => setTimeout(r, DELAY_MS)); | |
| } | |
| } | |
| if (allRows.length === 0) { | |
| console.error('No calls found — check that you are on https://production.tuple.app/profile/usage and logged in.'); | |
| return; | |
| } | |
| const csv = 'date,time,duration,participants\n' + allRows.join('\n') + '\n'; | |
| const blob = new Blob([csv], { type: 'text/csv' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'tuple-calls.csv'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| console.log(`Done! ${allRows.length} calls exported.`); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment