Firebase Users CSV Exporter

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})();
Firebase Users CSV Exporter | Robomonkey