Export Firebase user data to CSV with one click
Size
16.2 KB
Version
1.1.3
Created
Oct 31, 2025
Updated
25 days ago
1// ==UserScript==
2// @name Firebase Users CSV Exporter
3// @description Export Firebase user data to CSV with one click
4// @version 1.1.3
5// @match https://*.console.firebase.google.com/*
6// @icon https://www.gstatic.com/mobilesdk/240501_mobilesdk/firebase_16dp.png
7// @grant GM.xmlhttpRequest
8// ==/UserScript==
9(function() {
10 'use strict';
11
12 console.log('Firebase Users CSV Exporter loaded');
13
14 // Debounce function to prevent multiple rapid calls
15 function debounce(func, wait) {
16 let timeout;
17 return function executedFunction(...args) {
18 const later = () => {
19 clearTimeout(timeout);
20 func(...args);
21 };
22 clearTimeout(timeout);
23 timeout = setTimeout(later, wait);
24 };
25 }
26
27 // Function to parse date from Firebase format
28 function parseFirebaseDate(dateString) {
29 if (!dateString || dateString.trim() === '') {
30 return null;
31 }
32
33 // Firebase shows dates in various formats, try to parse them
34 const date = new Date(dateString);
35 return isNaN(date.getTime()) ? null : date;
36 }
37
38 // Function to extract user data from the table
39 function extractUserData(dateFrom, dateTo) {
40 console.log('Extracting user data from table...');
41 console.log('Date range:', { from: dateFrom, to: dateTo });
42 const users = [];
43 let shouldStopPagination = false;
44
45 // Find all user rows in the table
46 const rows = document.querySelectorAll('table[role="grid"][aria-label="Users"] tbody tr[role="row"]');
47 console.log(`Found ${rows.length} user rows`);
48
49 rows.forEach((row, index) => {
50 try {
51 // Extract Identifier (email or phone)
52 const identifierCell = row.querySelector('td.cdk-column-identifier');
53 const identifier = identifierCell ? identifierCell.textContent.trim() : '';
54
55 // Extract Created date
56 const createdCell = row.querySelector('td.cdk-column-created-at');
57 const created = createdCell ? createdCell.textContent.trim() : '';
58
59 // Extract Signed In date
60 const signedInCell = row.querySelector('td.cdk-column-last-login');
61 const signedIn = signedInCell ? signedInCell.textContent.trim() : '';
62
63 // Filter by date range if specified
64 if (dateFrom || dateTo) {
65 const createdDate = parseFirebaseDate(created);
66
67 if (createdDate) {
68 // If we have a "from" date and user was created before it, stop pagination
69 // (assuming users are sorted by creation date descending)
70 if (dateFrom && createdDate < dateFrom) {
71 console.log(`User ${index + 1} created before date range - stopping pagination`);
72 shouldStopPagination = true;
73 return;
74 }
75
76 // If user is after "to" date, skip but continue
77 if (dateTo && createdDate > dateTo) {
78 console.log(`Skipping user ${index + 1}: created after date range`);
79 return;
80 }
81 }
82 }
83
84 if (identifier || created || signedIn) {
85 users.push({
86 identifier,
87 created,
88 signedIn
89 });
90 console.log(`User ${index + 1}:`, { identifier, created, signedIn });
91 }
92 } catch (error) {
93 console.error(`Error extracting data from row ${index}:`, error);
94 }
95 });
96
97 return { users, shouldStopPagination };
98 }
99
100 // Function to wait for table to load
101 function waitForTableLoad() {
102 return new Promise((resolve) => {
103 const checkTable = () => {
104 const rows = document.querySelectorAll('table[role="grid"][aria-label="Users"] tbody tr[role="row"]');
105 if (rows.length > 0) {
106 // Wait a bit more to ensure all data is loaded
107 setTimeout(resolve, 1000);
108 } else {
109 setTimeout(checkTable, 500);
110 }
111 };
112 checkTable();
113 });
114 }
115
116 // Function to get pagination info
117 function getPaginationInfo() {
118 const paginatorLabel = document.querySelector('.mat-mdc-paginator-range-label');
119 if (!paginatorLabel) {
120 return null;
121 }
122
123 const text = paginatorLabel.textContent.trim();
124 // Format: "1 – 50 of 8145"
125 const match = text.match(/(\d+)\s*–\s*(\d+)\s*of\s*(\d+)/);
126
127 if (match) {
128 return {
129 start: parseInt(match[1]),
130 end: parseInt(match[2]),
131 total: parseInt(match[3])
132 };
133 }
134
135 return null;
136 }
137
138 // Function to click next page button
139 function clickNextPage() {
140 const nextButton = document.querySelector('button.mat-mdc-paginator-navigation-next:not(.mat-mdc-button-disabled)');
141 if (nextButton) {
142 console.log('Clicking next page button...');
143 nextButton.click();
144 return true;
145 }
146 return false;
147 }
148
149 // Function to collect all users from all pages
150 async function collectAllUsers(dateFrom, dateTo, progressCallback) {
151 const allUsers = [];
152 let currentPage = 1;
153
154 console.log('Starting to collect users from all pages...');
155 console.log('Initial pagination check...');
156
157 while (true) {
158 console.log(`Processing page ${currentPage}...`);
159
160 // Wait for table to load
161 await waitForTableLoad();
162 console.log('Table loaded for page', currentPage);
163
164 // Get pagination info
165 const paginationInfo = getPaginationInfo();
166 if (paginationInfo) {
167 console.log(`Page ${currentPage}: Processing users ${paginationInfo.start} - ${paginationInfo.end} of ${paginationInfo.total}`);
168 if (progressCallback) {
169 progressCallback(paginationInfo.end, paginationInfo.total);
170 }
171 } else {
172 console.warn('Could not get pagination info');
173 }
174
175 // Extract users from current page
176 const result = extractUserData(dateFrom, dateTo);
177 const pageUsers = result.users;
178 const shouldStop = result.shouldStopPagination;
179
180 console.log(`Extracted ${pageUsers.length} users from page ${currentPage}`);
181 allUsers.push(...pageUsers);
182
183 // Stop if we've gone past the date range
184 if (shouldStop) {
185 console.log('Reached end of date range - stopping pagination');
186 break;
187 }
188
189 // Try to go to next page
190 const hasNextPage = clickNextPage();
191 console.log(`Has next page: ${hasNextPage}`);
192 if (!hasNextPage) {
193 console.log('No more pages. Collection complete.');
194 break;
195 }
196
197 currentPage++;
198 console.log(`Moving to page ${currentPage}, waiting for page transition...`);
199
200 // Wait for page transition
201 await new Promise(resolve => setTimeout(resolve, 1500));
202 }
203
204 console.log(`Total users collected: ${allUsers.length}`);
205 return allUsers;
206 }
207
208 // Function to convert data to CSV format
209 function convertToCSV(users) {
210 console.log('Converting data to CSV format...');
211
212 // CSV header
213 const header = ['Identifier', 'Created', 'Signed In'];
214
215 // CSV rows
216 const rows = users.map(user => {
217 return [
218 `"${user.identifier.replace(/"/g, '""')}"`,
219 `"${user.created.replace(/"/g, '""')}"`,
220 `"${user.signedIn.replace(/"/g, '""')}"`
221 ].join(',');
222 });
223
224 // Combine header and rows
225 const csv = [header.join(','), ...rows].join('\n');
226 console.log(`CSV generated with ${users.length} users`);
227
228 return csv;
229 }
230
231 // Function to download CSV file
232 function downloadCSV(csv) {
233 console.log('Initiating CSV download...');
234
235 const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
236 const url = URL.createObjectURL(blob);
237 const link = document.createElement('a');
238
239 // Generate filename with timestamp
240 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
241 const filename = `firebase-users-${timestamp}.csv`;
242
243 link.setAttribute('href', url);
244 link.setAttribute('download', filename);
245 link.style.display = 'none';
246
247 document.body.appendChild(link);
248 link.click();
249 document.body.removeChild(link);
250
251 URL.revokeObjectURL(url);
252 console.log(`CSV file downloaded: ${filename}`);
253 }
254
255 // Function to handle export button click
256 async function handleExportClick() {
257 console.log('Export button clicked');
258
259 const exportButton = document.getElementById('firebase-csv-export-btn');
260 const originalButtonText = exportButton.querySelector('.mdc-button__label').textContent;
261
262 try {
263 // Disable button during export
264 exportButton.disabled = true;
265 exportButton.style.opacity = '0.6';
266 exportButton.style.cursor = 'not-allowed';
267
268 // Get date range values
269 const dateFromInput = document.getElementById('firebase-csv-date-from');
270 const dateToInput = document.getElementById('firebase-csv-date-to');
271
272 const dateFrom = dateFromInput && dateFromInput.value ? new Date(dateFromInput.value) : null;
273 const dateTo = dateToInput && dateToInput.value ? new Date(dateToInput.value + 'T23:59:59') : null;
274
275 // Validate date range
276 if (dateFrom && dateTo && dateFrom > dateTo) {
277 alert('Invalid date range: "From" date must be before "To" date.');
278 return;
279 }
280
281 // Update button text to show progress
282 const updateProgress = (current, total) => {
283 exportButton.querySelector('.mdc-button__label').textContent = `Collecting... ${current}/${total}`;
284 };
285
286 // Collect all users from all pages
287 const users = await collectAllUsers(dateFrom, dateTo, updateProgress);
288
289 if (users.length === 0) {
290 alert('No user data found to export. Please make sure the user table is loaded or adjust your date filters.');
291 console.warn('No users found in the table');
292 return;
293 }
294
295 // Update button text
296 exportButton.querySelector('.mdc-button__label').textContent = 'Generating CSV...';
297
298 const csv = convertToCSV(users);
299 downloadCSV(csv);
300
301 const dateRangeMsg = (dateFrom || dateTo)
302 ? ' (filtered by date range)'
303 : '';
304 alert(`Successfully exported ${users.length} users to CSV${dateRangeMsg}!`);
305 } catch (error) {
306 console.error('Error during export:', error);
307 alert('An error occurred while exporting users. Please check the console for details.');
308 } finally {
309 // Re-enable button
310 exportButton.disabled = false;
311 exportButton.style.opacity = '1';
312 exportButton.style.cursor = 'pointer';
313 exportButton.querySelector('.mdc-button__label').textContent = originalButtonText;
314 }
315 }
316
317 // Function to create and add the export button
318 function addExportButton() {
319 console.log('Attempting to add export button...');
320
321 // Check if button already exists
322 if (document.getElementById('firebase-csv-export-btn')) {
323 console.log('Export button already exists');
324 return;
325 }
326
327 // Find the Add User button
328 const addUserButton = document.querySelector('button[data-test-id="add-user-button"]');
329
330 if (!addUserButton) {
331 console.log('Add User button not found yet');
332 return;
333 }
334
335 console.log('Add User button found, creating export button...');
336
337 // Create a container for the export controls
338 const exportContainer = document.createElement('div');
339 exportContainer.id = 'firebase-csv-export-container';
340 exportContainer.style.cssText = 'display: inline-flex; align-items: center; gap: 8px; margin-left: 12px;';
341
342 // Create date from input
343 const dateFromLabel = document.createElement('label');
344 dateFromLabel.textContent = 'From:';
345 dateFromLabel.style.cssText = 'font-size: 14px; color: #5f6368; margin-left: 8px;';
346
347 const dateFromInput = document.createElement('input');
348 dateFromInput.type = 'date';
349 dateFromInput.id = 'firebase-csv-date-from';
350 dateFromInput.style.cssText = 'padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px; color: #202124;';
351
352 // Create date to input
353 const dateToLabel = document.createElement('label');
354 dateToLabel.textContent = 'To:';
355 dateToLabel.style.cssText = 'font-size: 14px; color: #5f6368; margin-left: 8px;';
356
357 const dateToInput = document.createElement('input');
358 dateToInput.type = 'date';
359 dateToInput.id = 'firebase-csv-date-to';
360 dateToInput.style.cssText = 'padding: 6px 8px; border: 1px solid #dadce0; border-radius: 4px; font-size: 14px; color: #202124;';
361
362 // Create the export button
363 const exportButton = document.createElement('button');
364 exportButton.id = 'firebase-csv-export-btn';
365 exportButton.className = addUserButton.className.replace('mat-mdc-button-disabled', '');
366 exportButton.style.marginLeft = '8px';
367 exportButton.innerHTML = `
368 <span class="mat-mdc-button-persistent-ripple mdc-button__ripple"></span>
369 <span class="mdc-button__label">Export CSV</span>
370 <span class="mat-focus-indicator"></span>
371 <span class="mat-mdc-button-touch-target"></span>
372 `;
373
374 // Add click event listener
375 exportButton.addEventListener('click', handleExportClick);
376
377 // Assemble the container
378 exportContainer.appendChild(dateFromLabel);
379 exportContainer.appendChild(dateFromInput);
380 exportContainer.appendChild(dateToLabel);
381 exportContainer.appendChild(dateToInput);
382 exportContainer.appendChild(exportButton);
383
384 // Insert the container next to the Add User button
385 addUserButton.parentNode.insertBefore(exportContainer, addUserButton.nextSibling);
386
387 console.log('Export button and date filters added successfully');
388 }
389
390 // Initialize the extension
391 function init() {
392 console.log('Initializing Firebase Users CSV Exporter...');
393
394 // Try to add button immediately
395 addExportButton();
396
397 // Use MutationObserver to detect when the page content loads
398 const observer = new MutationObserver(debounce(() => {
399 addExportButton();
400 }, 500));
401
402 // Start observing the document for changes
403 observer.observe(document.body, {
404 childList: true,
405 subtree: true
406 });
407
408 console.log('MutationObserver started');
409 }
410
411 // Wait for the page to be ready
412 if (document.readyState === 'loading') {
413 document.addEventListener('DOMContentLoaded', init);
414 } else {
415 init();
416 }
417})();