I am trying to understand whether it is better to report month-over-month on the current Firebase "Active" User metrics report (view graph below), or rather self-calculate and report the average of each of these metrics's values over a specific period.
At first-glance the dashboard shows you 1-day, 7-day, and 28-day active users for the month of December 2018, but it is in fact only the last day of the selected date range's values that is shown (on the right). This is great to know, but a bit misleading to compare only the last date's values for my month-over-month analysis. An alternative approach could be to self-calculate the average over the selected period:
Applied to the Firebase Demo data set, I got the numbers below:
Firebase Dashboard:
My Calculated Average:
The delta difference is small here, but I am seeing some significant differences on our application which has millions of active users per month.
Question:
DAU/MAU is a popular metric for user engagement – it's the ratio of your daily active users over your monthly active users, expressed as a percentage. Usually apps over 20% are said to be good, and 50%+ is world class.
What is Considered a Good DAU/MAU? A good DAU/MAU benchmark varies based on what type of app you have. Generally, you want to focus more on how your DAU/MAU is trending over time rather than the actual number. Generally speaking, apps over 20% are said to be good, and apps that reach 50%+ (like Facebook) are excellent.
It provides a snapshot of user retention. For early stage startups, this is a helpful metric for evaluating traction and potential revenue. Using the ratio - instead of DAU or MAU alone - gives you the necessary context to understand the actual level of engagement.
Three User Engagement Metrics: DAU, WAU, MAU It's very common for organizations to track daily active users (DAU), weekly active users (WAU), and monthly active users (MAU). These metrics measure the number of users who engage with your product or service over the specific time period indicated.
To answer this question, I would like to first revisit Google's applicable definitions, and then run over the calculations (Last Revisited: July 2021).
Google gives us the following definitions:
GA4 - Automatically collected events
session_start
(app, web) - when a user engages the app or websiteuser_engagement
(app, web) - periodically, while the app is in the foreground or the webpage is in focus
With params: engagement_time_msecGA4 - How the number of sessions is calculated
Sessions
: The number of sessions that began on your site or app (the session_start event was triggered).App session timeout duration
: An app session begins to time out when an app is moved to the background, but you have the option to extend that session by including an extend_session parameter (with a value of 1) with events you send while the app is in the background. This is useful if your app is frequently used in the background, (e.g. as with navigation and music apps.) Change the default timeout of 30 minutes for app sessions via the setSessionTimeoutDuration
method.Engaged sessions
: The number of sessions that lasted 10 seconds or longer, or had 1 or more conversion events or 2 or more page views.GA4 Dashboard
Monthly (28-day), Weekly (7-day), and Daily (1-day) Active Users
for the date range, including fluctuation by percentage from the previous date range. An active user has engaged with an app in the device foreground, and has logged a user_engagement event.Daily user engagement
- Average daily engagement per user for the date range, including the fluctuation by percentage from the previous date range.My take on the definitions:
Based on the supporting GA4/Firebase documents, I (re-)summarized the definitions for each of the metrics below. It is very important to state that only the unique users should be counted over each of the metrics (given selected date range). No need to UNNEST
as we are already querying at the event_name
-level, not for example the event_parameter
-level.
user_engagement
event within the last 1-day period (given selected date range).user_engagement
event within the last 7-day period (given selected date range).user_engagement
event within the last 28-day period (given selected date range).In the cells below you can see how the metrics are calculated for December:
Methodology to Calculate Each Metric / Audience:
Average 1-day active user metric
.Average 7-day active user metric
. I calculated this by averaging the snapshots at 7, 14, 21, 28 December.Non-averaged 28-day active user metric
. The main reason for not averaging this metric's value is, because I want to have only one snapshot of the entire month. If I would have used averages here I would also account for users that were active in a previous month.1.a) AVG 1-day Unique Active User Metric
# StandardSQL
SELECT
ROUND(AVG(users),0) AS users
FROM
(
SELECT
event_date,
COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX BETWEEN '20181201' AND '20181231'
AND platform = "ANDROID"
GROUP BY 1
) table
# or you could also use code below, but you will have to add in the remaining days' code to query against the entire month.
-- Set your variables here
WITH timeframe AS (SELECT DATE("2018-12-01") AS start_date, DATE("2018-12-31") AS end_date)
-- Query your variables here
SELECT ROUND(AVG(users),0) AS users
FROM
(
SELECT event_date, COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 1 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL 0 DAY))
AND platform = "ANDROID"
GROUP BY 1
UNION ALL
SELECT event_date, COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 2 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 1 DAY))
AND platform = "ANDROID"
GROUP BY 1
...
...
...
...
) avg_1_day_active_users
1.b) AVG 1-day Unique Active User Metric
A more recent version scheduled daily
to a BQ destination table daus_android_{run_time|"%Y%m%d"}
with write preference WRITE_APPEND
, could look like below. I did a previous deep-dive and determined it could take up to 48 hours for intraday-table events to propagate to permanent BQ tables (hence the - 3 days in the query).
with base AS (
SELECT *
FROM `<id>.analytics_<number>.events_*`
WHERE (_TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY)) AND _TABLE_SUFFIX < FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)))
AND platform = "ANDROID"
AND event_name = 'user_engagement'
), app AS (
SELECT
FORMAT_DATE('%Y%m%d', @run_date) AS _currentdate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY)) AS _begindate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)) AS _enddate,
TIMESTAMP_DIFF(TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 2 DAY)), TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 3 DAY)), HOUR) AS _hoursdiff,
COUNT(DISTINCT user_pseudo_id) AS _uniqusers
FROM base
)
SELECT
app._currentdate,
app._begindate,
app._enddate,
app._hoursdiff,
app._uniqusers
FROM app;
1.c) AVG 1-day Unique Active User Metric
WITH app as (
SELECT
FORMAT_DATE('%Y%m%d', @run_date) AS _currentdate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY)) AS _begindate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)) AS _enddate,
TIMESTAMP_DIFF(TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 2 DAY)), TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 3 DAY)), HOUR) AS _hoursdiff,
COUNT(DISTINCT user_pseudo_id) AS _uniqusers
FROM `<gcp-project>.analytics_<id>.events_*`
WHERE
platform = "ANDROID"
AND event_name = 'user_engagement'
AND _TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY))
AND _TABLE_SUFFIX < FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY))
)
SELECT
app._currentdate,
app._begindate,
app._enddate,
app._hoursdiff,
app._uniqusers
FROM app
2.a) AVG 7-day Unique Active User Metric
-- Set your variables here
WITH timeframe AS (SELECT DATE("2018-12-01") AS start_date, DATE("2018-12-31") AS end_date)
-- Query your variables here
SELECT ROUND(AVG(users),0) AS users
FROM
(
SELECT COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 7 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL 0 DAY))
AND platform = "ANDROID"
UNION ALL
SELECT COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 14 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 7 DAY))
AND platform = "ANDROID"
UNION ALL
SELECT COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 21 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 14 DAY))
AND platform = "ANDROID"
UNION ALL
SELECT COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 28 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 21 DAY))
AND platform = "ANDROID"
) avg_7_day_active_users
2.b) AVG 7-day Unique Active User Metric
A more recent version scheduled daily
to a BQ destination table waus_android_{run_time|"%Y%m%d"}
with write preference WRITE_APPEND
, could look like:
with base AS (
SELECT *
FROM `<id>.analytics_<number>.events_*`
WHERE (_TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 9 DAY)) AND _TABLE_SUFFIX < FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)))
AND platform = "ANDROID"
AND event_name = 'user_engagement'
), app AS (
SELECT
FORMAT_DATE('%Y%m%d', @run_date) AS _currentdate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 9 DAY)) AS _begindate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)) AS _enddate,
TIMESTAMP_DIFF(TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 2 DAY)), TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 9 DAY)), HOUR) AS _hoursdiff,
COUNT(DISTINCT user_pseudo_id) AS _uniqusers
FROM base
)
SELECT
app._currentdate,
app._begindate,
app._enddate,
app._hoursdiff,
app._uniqusers
FROM app;
3.a) Non-averaged 28-day Unique Active User Metric
# StandardSQL
-- Set your variables here
WITH timeframe AS (SELECT DATE("2018-12-01") AS start_date, DATE("2018-12-31") AS end_date)
-- Query your variables here
SELECT COUNT(DISTINCT user_pseudo_id) AS users
FROM `<id>.events_*`AS z, timeframe AS t
WHERE
event_name = 'user_engagement'
AND _TABLE_SUFFIX > FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL - 28 DAY))
AND _TABLE_SUFFIX <= FORMAT_DATE('%Y%m%d', DATE_ADD(t.end_date, INTERVAL 0 DAY))
AND platform = "ANDROID"
3.b) Non-averaged 28-day Unique Active User Metric
A more recent version scheduled daily
to a BQ destination table maus_android_{run_time|"%Y%m%d"}
with write preference WRITE_APPEND
, could look like:
with base AS (
SELECT *
FROM `<id>.analytics_<number>.events_*`
WHERE (_TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 30 DAY)) AND _TABLE_SUFFIX < FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)))
AND platform = "ANDROID"
AND event_name = 'user_engagement'
), app AS (
SELECT
FORMAT_DATE('%Y%m%d', @run_date) AS _currentdate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 30 DAY)) AS _begindate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)) AS _enddate,
TIMESTAMP_DIFF(TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 2 DAY)), TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 30 DAY)), HOUR) AS _hoursdiff,
COUNT(DISTINCT user_pseudo_id) AS _uniqusers
FROM base
)
SELECT
app._currentdate,
app._begindate,
app._enddate,
app._hoursdiff,
app._uniqusers
FROM app;
Side Notes:
DAU-to-MAU
-ratio or WAU-to-MAU
-ratio from above examples to determine your app's stickiness
SELECT COUNT(DISTINCT user_id) FROM /* PLEASE REPLACE WITH YOUR TABLE NAME */ `YOUR_TABLE.events_*` WHERE event_name = 'user_engagement' /* Pick events in the last N = 20 days */ AND event_timestamp > UNIX_MICROS(TIMESTAMP_SUB(CURRENT_TIMESTAMP, INTERVAL 20 DAY)) /* PLEASE REPLACE WITH YOUR DESIRED DATE RANGE */ AND _TABLE_SUFFIX BETWEEN '20180521' AND '20240131';
🥷 Ninja Tip 🥷
To transition your team's/company's/product's focus from Vanity Metrics
to Actionable Metrics
, consider adding one of your main conversion-events as part of the queries above (e.g. in_app_purchase
for e-commerce companies):
with base AS (
SELECT *
FROM `<id>.analytics_<number>.events_*`
WHERE (_TABLE_SUFFIX >= FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY)) AND _TABLE_SUFFIX < FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)))
AND platform = "ANDROID"
# AND event_name = 'user_engagement'
AND event_name = 'in_app_purchase'
), app AS (
SELECT
FORMAT_DATE('%Y%m%d', @run_date) AS _currentdate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 3 DAY)) AS _begindate,
FORMAT_DATE('%Y%m%d', DATE_ADD(@run_date, INTERVAL - 2 DAY)) AS _enddate,
TIMESTAMP_DIFF(TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 2 DAY)), TIMESTAMP(DATE_ADD(@run_date, INTERVAL - 3 DAY)), HOUR) AS _hoursdiff,
COUNT(DISTINCT user_pseudo_id) AS _uniqusers
FROM base
)
SELECT
app._currentdate,
app._begindate,
app._enddate,
app._hoursdiff,
app._uniqusers
FROM app;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With