Tracking User Login Streaks with MongoDB
Tags: node, mongodb, typescript
Published: 2023-02-01 18:23:56

## Motivation

While working on one of my professional projects, I wanted to add some kind of streak tracking to the platform. It was more of an exercise for me, than an actual feature but it did seem interesting to work on. So let me take you on a short journey where I show you how I came up with this feature build from scratch. 

> _This guide is meant for beginner to intermediate developers, but experienced folks can also take a thing or two from this. Everything is welcomed from all minds_

## Idea

So, before we jump into the fancy stuff, let's map out the idea itself and what it is supposed to achieve, which will be our success metric for the same.

The idea is to track the user login streaks on the platform. For ease of operations, let's stick to just the number of consecutive days the user logged in onto the platform. This can later be extended to show various streaks as streak histories to the user.

## Flow

The flow of operations is where we define how the things will work and connect. We will use two flows, **an external flow** and **an internal flow**. Our external flow will help us **_define the user interactions_** and what the user can see. Our internal flow will help us **_define what the system will do under the hood._**

The external flow will be as such.

- The user logs in.
- We track the streak (internal)
- The user sees their streak on the navigation bar or profile.

The internal flow will be similar to

- Login request received.
- check the last login date of the user and 
  - if the last login is today, do not update the streak
  - if the last login was yesterday, increment the streak by 1
  - if the last login was before yesterday, reset the streak to 1

_Now that we have the flows defined, let's jump into some code and see it in action._

## The **Good** Stuff

This project uses Typescript with MongoDB, but the concept itself can be used in any suitable language and database combination.

### Prerequisites

- MongoDB instance ([docker](https://hub.docker.com/_/mongo) or [cloud](https://atlas.mongodb.com))
- Your Editor of Choice (I will use [Visual Studio Code](https://code.visualstudio.com) for the purpose of this guide, but you are welcome to use neovim, lunarvim, spacevim, astrovim, or any other ...vim thing you like)

### Database Model

We will work with a single model or entity called User. It can have any number of field you might like, but for the purpose of this guide, we will restrict it to only a few fields.

```ts
export class User {
  // generic fields
  username: string;
  email: string;
  
  // to be used in the feature
  lastLogin: string;
  loginStreak: number;
}
```

With this set up, we can now use this to build our streak feature.

_Note that writing a class like this is just a personal preference. If you are comfortable in writing native objects for mongodb using simple objects as schemas, you are more than welcome to use your preferred style of development._

### The Streak Service

We are not implementing the login feature itself but we will write a function that will augment the login service and that can be plugged into any other service to calculate the streak. This helps with development as this feature can be tested in isolation without touching the existing services and methods.

Let us look at the code first and the wrap our heads around it.

```ts
/**
 * @param userId String format of ObjectId of the user 
 * @returns the current user streak for the day
 */
export async function updateStreak(userId: string): Promise<number> {
  // get the user document, this is mutable and can be saved to reflect the changes in the database
  const user = await userModel.findById(userId);
  
  // calculate the current day, in the Indian locale
  const today = (new Date()).toLocaleDateString('en-IN', { dateStyle: 'medium' });
  // calculate the previous day, in the Indian locale
  const yesterdayDate = new Date()
  yestardayDate.setDate(yesterdayDate.getDate() - 1);
  const yesterday = yesterdayDate.toLocaleDateString('en-IN', { dateStyle: 'medium' });
  
  if(user.lastLogin === today) {
    // do nothing
  } else if(user.lastLogin == yesterday) {
    user.loginStreak += 1;
  } else {
    user.loginStreak = 1;
  }

  await user.save();
  return user.loginStreak;   // because we return a primitive type, it is passed by value.
}
```

Now let's break it down.

We first fetch the user document for the provided _userId_. This allows us to attach this service function wherever we like and let is update the streak. 

We then calculate the string values of current date and previous date to ensure that the dates are the only things being matched. If the exact time of last login is also required, there might be additional processing required to bring down the checks to just dates. 

We then check the last login date against the current date and previous date to update the streaks accordingly. _In this code snippet, We reset the streak to 1 to indicate that the current day is the start of a new streak.
_
### Bonus

So far, we have calculated and updated the current streak. If we wanted to store the longest streak so far of the user, we could do that easily by adding an additional field in the user schema like so -

```ts
export class User {
   .... all previous fields
   longestLoginStreak: number;
```

We can then use this field in the function to update it to the longest so far, like so -

```ts
export async function updateStreak(userId: string): Promise<number> {
  ...... all previous code

  // set the fallback to 0, so that the documents that do not have this field will also be handled
  user.longestLoginStreak = Math.max(user.longestLoginStreak || 0, user.loginStreak);
  user.save();
  return user.loginStreak;
}
```

### Passing Remarks

There you go. Congratulations on bearing with me so far. You have successfully created a feature from scratch with all the code necessary to make it functional (kind of). So go ahead and use this newly gained knowledge to build out this feature in your own applications. 

Thank me later. Hang around for the next one.