The traditional approach of simply chaining .orderBy()
methods won't create a true multi-factor ranking system for our potential users. We need a more sophisticated solution that truly balances all three factors.
Here's my approach using a composite scoring method:
// Calculate a unified potential customer score
function calculateUserPotentialIndex(userData) {
// Scale each metric to a comparable range
const ratingComponent = userData.totalAverageWeightRatings / 5; // Assuming 5-star max
// Apply diminishing returns for very high rental counts
const rentalsComponent = 1 - (1 / (1 + (userData.numberOfRents / 20)));
// Convert recency to a 0-1 scale (favoring activity within last 2 weeks)
const currentTime = Date.now() / 1000;
const twoWeeksSeconds = 14 * 24 * 60 * 60;
const recencyComponent = Math.max(0, Math.min(1, 1 - ((currentTime - userData.recentlyActive) / twoWeeksSeconds)));
// Weight the components according to business priorities
return (ratingComponent * 0.65) + (rentalsComponent * 0.25) + (recencyComponent * 0.10);
}
This resembles how food delivery apps rank restaurants, they don't just show the highest-rated places first. They balance rating with popularity and recency of reviews.
To implement in a real production environment:
- I'd create a background processor that updates this score whenever a user document changes:
updateUserPotentialScore = db
.docs('USERS/{userId}')
.onWrite((change, context) => {
const user = change.after.exists ? change.after.data() : null;
if (!user) return null;
const potentialScore = calculateUserPotentialIndex(user);
// Store the computed score
return change.after.ref.update({
potentialUserScore: potentialScore,
// Store a timestamp so we know when this was calculated
scoreUpdatedAt: FieldValue.serverTimestamp()
});
});
- Then for client queries, we simply use:
// First page query
const topUsersQuery = db.collection("USERS")
.orderBy("potentialUserScore", "desc")
.limit(20);
// For subsequent pages
function loadNextPage(lastUser) {
return db.collection("USERS")
.orderBy("potentialUserScore", "desc")
.startAfter(lastUser)
.limit(20);
}
Real-world example: This is similar to how LinkedIn ranks job candidates for recruiters. They don't just look at years of experience (our rental count), but also combine it with profile completeness (our rating) and recent activity (our recency). A holistic score helps surface the most promising candidates who might otherwise be missed by simpler filtering.
For tracking user online status and activity timestamps effectively, I'd implement a resilient multi-tier system:
class UserPresenceTracker {
constructor(userId) {
this.userId = userId;
this.firestoreRef = db.collection("USERS").doc(userId);
this.rtdbRef = firebase.database().ref(`/presence/${userId}`);
this.connectionRef = firebase.database().ref('.info/connected');
this.activityTimeout = null;
this.inactivityThreshold = 3 * 60 * 1000; // 3 minutes
this.initializePresenceMonitoring();
this.initializeActivityTracking();
}
initializePresenceMonitoring() {
this.connectionRef.on('value', snapshot => {
// Connection established or re-established
if (snapshot.val() === true) {
// Set up what happens when client disconnects
const offlineData = {
status: 'offline',
lastChanged: firebase.database.ServerValue.TIMESTAMP
};
this.rtdbRef.onDisconnect().set(offlineData).then(() => {
// Now update current status to online
this.rtdbRef.set({
status: 'online',
lastChanged: firebase.database.ServerValue.TIMESTAMP
});
// Update Firestore user document
this.updateFirestoreActivity(true);
});
}
});
// Listen for RTDB status changes as a backup
this.rtdbRef.on('value', snapshot => {
const data = snapshot.val();
if (data && data.status === 'offline') {
this.updateFirestoreActivity(false);
}
});
}
updateFirestoreActivity(isOnline) {
const now = Math.floor(Date.now() / 1000);
this.firestoreRef.update({
isOnline: isOnline,
recentlyActive: now,
// Also update timezone information for global applications
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
});
}
initializeActivityTracking() {
// Reset inactivity timer whenever user does something
const resetTimer = () => {
if (this.activityTimeout) {
clearTimeout(this.activityTimeout);
}
this.activityTimeout = setTimeout(() => {
this.updateFirestoreActivity(true);
}, this.inactivityThreshold);
};
// Track common user interactions
['mousedown', 'keydown', 'touchstart', 'scroll', 'focus'].forEach(eventType => {
window.addEventListener(eventType, resetTimer, { passive: true });
});
// Initial timer
resetTimer();
}
}
// Usage
const presenceTracker = new UserPresenceTracker(currentUser.uid);
I'd also implement a server-side safeguard:
checkStaleUserStatus = functions.pubsub
.schedule('every 10 minutes')
.onRun(async () => {
const usersRef = admin.firestore().collection('USERS');
// Define stale threshold (10 minutes ago)
const staleThreshold = Math.floor(Date.now() / 1000) - (10 * 60);
const staleUsers = await usersRef
.where('isOnline', '==', true)
.where('recentlyActive', '<', staleThreshold)
.get();
// Batch update stale users
const batch = admin.firestore().batch();
staleUsers.docs.forEach(doc => {
batch.update(doc.ref, {
isOnline: false,
// We don't update recentlyActive as that should remain when they were truly last active
});
});
return batch.commit();
});
Real-world example: This also shows how messaging apps like WhatsApp handle presence. They show when you're "online" but also indicate "last seen at" timestamps. The system needs to be responsive enough to show accurate status but not so aggressive that it drains mobile batteries or consumes excessive bandwidth.
For a booking app I worked on, we used a similar system to track when freelancers were actually using their reserved booking versus just having a reservation. This allowed the space to offer "hot booking" availability for walk-ins when someone with a reservation hadn't shown up within 30 minutes of their booking start time.
The multi-layered approach ensures reliability even when users lose connectivity unexpectedly or their device enters sleep mode, making the user experience smooth while maintaining accurate data for business analytics.