What do you do if your boss comes up to you and asks, "So how many times must a person come to our app before they give us \\$500?"
It's an important question. How many touch points does your sales process require? Do you need 2 ad clicks or 5? Two shiny demos or one fat video?
As an engineer, you might not care, but this is the stuff that keeps your CEO and growth lead and head of product up at night. A business owner that can answer that π question reliably is god amongst men.
It's an important question, therefore Google Analytics has the answer, right?
Wrong.
The best Google Analytics can do is this view:
This histogram tells you that out of the 24,187 people that visited swizec.com in the last 30 days, 24,187 of them started their first session. 2,103 started their 2nd, all the way down to the 127 people who started their 201st session.
Those people areβ¦ odd. My blog is not that interesting π€ Even I don't visit the site that much.
Ok, so Google Analytics doesn't want to tell us how many sessions a particular user had before they converted. But what's a session anyway?
This is how a web session is defined in GA. The tl;dr is that a new session starts after every:
- 30 minutes of inactivity
- midnight
utm_campaign
query change
We count something as a new session every 30 minutes (even if the site was left open), on every new day, and following every new ad click. I don't know why Google defined web sessions this way, but if you're building business intelligence, it's best to use standard definitions.
So how do you count those web sessions?
Well, you need to keep track of the 3 parameters above, and you need to keep track of a counter. Then you up the counter on every change. That gives you the count and you can do whatever you want.
Save it to the backend via some sort of API perhaps ;)
Here's the solution I came up with. You can use it as an npm module, or read onwards to learn how it works.
WebSessionCounter
To make your life easier, I made this web-session-counter npm module. You can see the full code on Github, too.
Install the utility with npm install web-session-counter
, then use it like this:
import WebSessionCounter from "web-session-counter";
// Do this on user activity
WebSessionCounter.update();
// To get the total count of sessions
const count = WebSessionCounter.count;
WebSessionCounter
automatically calls .update()
on every initialization. When you import the library, that is. It's a singleton. You can get the current count through the .count
property.
I suggest calling .update()
every time your user performs a significant action. In my day job, we tie this to our internal funnel tracking. Whenever we track an event for business analytics, we update the counter as well.
If your thing is built as a single page app, you have to keep calling .update()
. Otherwise, you might miss the 30-minute inactivity window or make it look too big.
If you often reload the page, don't worry about calling .update()
. The reload will do it for you.
Here's how it works
The gist of WebSessionCounter
is this 71-line class.
class WebSessionCounter {
constructor() {
this.update();
}
get count() {
if (canUseLocalStorage) {
return Number(window.localStorage.getItem("user_web_session_count"));
} else {
return NaN;
}
}
set count(val) {
window.localStorage.setItem("user_web_session_count", val);
}
get lastActive() {
const time = window.localStorage.getItem("user_web_session_last_active");
if (time) {
return moment(time);
} else {
return moment();
}
}
set lastActive(time) {
window.localStorage.setItem(
"user_web_session_last_active",
time.toISOString()
);
}
get lastUtmCampaign() {
return window.localStorage.getItem("user_web_session_utm_campaign");
}
set lastUtmCampaign(val) {
window.localStorage.setItem("user_web_session_utm_campaign", val);
}
get currentUtmCampaign() {
const [path, query = ""] = window.location.href.split("?"),
{ utm_campaign = "" } = querystring.parse(query);
return utm_campaign;
}
update() {
if (canUseLocalStorage) {
let count = this.count,
time = this.lastActive;
if (count === 0 || this.isNewSession()) {
this.count = count + 1;
this.lastActive = moment();
this.lastUtmCampaign = this.currentUtmCampaign;
}
}
}
isNewSession() {
// use definition from https://support.google.com/analytics/answer/2731565?hl=en
const time = this.lastActive,
now = moment();
return [
moment.duration(now.diff(time)).asMinutes() > 30,
now.format("YYYY-MM-DD") !== time.format("YYYY-MM-DD"),
this.lastUtmCampaign !== this.currentUtmCampaign,
].some((b) => b);
}
}
When I say "gist", I mean that's all there is to it. Doesn't look like much, but it did take me an hour or two to write. You can use my web-session-counter
module, and it will take you 5 minutes. :)
We have 3 sets of getters and setters for the count
, the lastActive
timestamp, and lastUtmCampaign
. Getters read values from local storage; setters save them.
The currentUtmCampaign
getter reads the URL and returns the current value of utm_campaign
. Having a pair of getters for current and last utm_campaign
helps us detect changes.
Seems fine for generated code
β Sven Sauleau (@svensauleau) May 10, 2017
Our business logic lies in the update
and isNewSession
methods.
update() {
if (canUseLocalStorage) {
let count = this.count;
if (count === 0 || this.isNewSession()) {
this.count = count + 1;
this.lastActive = moment();
this.lastUtmCampaign = this.currentUtmCampaign;
}
}
}
isNewSession() {
// use definition from https://support.google.com/analytics/answer/2731565?hl=en
const time = this.lastActive,
now = moment();
return [
moment.duration(now.diff(time)).asMinutes() > 30,
now.format('YYYY-MM-DD') !== time.format('YYYY-MM-DD'),
this.lastUtmCampaign !== this.currentUtmCampaign
].some(b => b);
}
update
first checks if local storage is available. Wouldn't wanna throw errors and kill all JavaScript if it isn't.
If we can use local storage, then we get the current count
from local storage. If the count
is zero or isNewSession
returns true
, we have to update info in local storage.
We increase the count
, update the lastActive
timestamp, and store the current utm_campaign
value.
To detect new sessions, we use a helper method β isNewSession
. Some say my code isn't readable, but I think it's not too bad. The function name tells you what it does π
The first condition checks if 30 minutes have passed since the last update, the second checks if the date has changed, and the third check if the utm_campaign
is different. .some(b => b)
returns true
if any of the conditions are truthy.
An alternative that isn't significantly longer, but IMO much clearer pic.twitter.com/iJ2ulJIjEW
β Shampodin (@Bladtman) May 10, 2017
Caveats
Users can clear local storage and you lose track. That's okay; real users don't do that. Only cheeky engineers.
Some browsers don't have local storage, like Safari in incognito mode for example. That's okay; those users don't want to be tracked, so you shouldn't track them.
Happy hacking. π€
Continue reading about Counting web sessions with JavaScript
Semantically similar articles hand-picked by GPT-4
- I stopped using Google Analytics after 15 years
- A Fast Mutex Lamport Lock with JavaScript Promises
- That one time a simple for-loop increased conversions by 19%
- My biggest React App performance boost was a backend change
- A day is not 60*60*24 seconds long
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below π
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails π on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. π"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help π swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers π ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are β€οΈ