Track active users
You can use Lakehouse to track daily, weekly, and monthly active users for a customer domain and monitor adoption over time.
This page provides example queries for active-user metrics in the USAGE_ANALYTICS schema, including DAU, WAU, MAU, stickiness ratio, and a user roster.
Before you begin
Before you run these queries, make sure:
- Your Lakehouse setup is complete and Lakehouse is enabled for your organization. If setup isn't complete, see Get started with Lakehouse.
Query parameters
These queries use the tables in the USAGE_ANALYTICS schema. For table definitions, see USAGE_ANALYTICS namespace.
Replace the placeholders in each query with values for your environment.
{{DATABASE}}: Snowflake database name{{SCHEMA}}: UseUSAGE_ANALYTICS{{DOMAIN}}: Customer domain, for example'acme.atlan.com'{{START_DATE}}: Start date, for example'2025-01-01'
Daily active users
Returns the count of distinct users who performed any tracked action or page view on each calendar day for the specified domain.
- Snowflake
-- dau_by_domain.sql
-- Daily Active Users per customer domain.
-- Uses PAGES.domain for filtering. Counts by user_id.
--
-- Parameters:
-- {{START_DATE}} - e.g., '2025-01-01'
-- {{DOMAIN}} - e.g., 'acme.atlan.com' (or replace filter line with 1=1 for all)
WITH user_domains AS (
SELECT user_id, MAX(domain) AS domain
FROM {{DATABASE}}.{{SCHEMA}}.PAGES
WHERE domain IS NOT NULL
GROUP BY user_id
),
activity_events AS (
SELECT
t.user_id,
ud.domain,
DATE(CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP)) AS event_date
FROM {{DATABASE}}.{{SCHEMA}}.TRACKS t
INNER JOIN user_domains ud ON ud.user_id = t.user_id
WHERE t.event_text NOT IN (
'atlan_analaytics_aggregateinfo_fetch',
'api_error_emit', 'api_evaluator_cancelled', 'api_evaluator_succeeded',
'Experiment Started', '$experiment_started',
'web_vital_metric_inp_track', 'web_vital_metric_ttfb_track',
'performance_metric_user_timing_discovery_search',
'performance_metric_user_timing_app_bootstrap',
'web_vital_metric_fcp_track', 'web_vital_metric_lcp_track'
)
AND t.event_text NOT LIKE 'workflow_%'
AND t.TIMESTAMP >= {{START_DATE}}
UNION ALL
SELECT
p.user_id,
p.domain,
DATE(CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP)) AS event_date
FROM {{DATABASE}}.{{SCHEMA}}.PAGES p
WHERE p.TIMESTAMP >= {{START_DATE}}
)
SELECT
domain,
event_date,
COUNT(DISTINCT user_id) AS dau
FROM activity_events
WHERE domain = {{DOMAIN}}
GROUP BY domain, event_date
ORDER BY domain, event_date DESC;
Sample output
| DOMAIN | EVENT_DATE | DAU |
|---|---|---|
| acme.atlan.com | 2025-03-15 | 47 |
| acme.atlan.com | 2025-03-14 | 52 |
| acme.atlan.com | 2025-03-13 | 38 |
Weekly active users
Returns the count of distinct users who performed any tracked action or page view within each calendar week for the specified domain.
- Snowflake
-- wau_by_domain.sql
-- Weekly Active Users per customer domain.
-- Uses PAGES.domain for filtering. Counts by user_id.
--
-- Parameters:
-- {{START_DATE}} - e.g., '2025-01-01'
-- {{DOMAIN}} - e.g., 'acme.atlan.com' (or replace filter line with 1=1 for all)
WITH user_domains AS (
SELECT user_id, MAX(domain) AS domain
FROM {{DATABASE}}.{{SCHEMA}}.PAGES
WHERE domain IS NOT NULL
GROUP BY user_id
),
activity_events AS (
SELECT
t.user_id,
ud.domain,
DATE_TRUNC('WEEK', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP)) AS event_week
FROM {{DATABASE}}.{{SCHEMA}}.TRACKS t
INNER JOIN user_domains ud ON ud.user_id = t.user_id
WHERE t.event_text NOT IN (
'atlan_analaytics_aggregateinfo_fetch',
'api_error_emit', 'api_evaluator_cancelled', 'api_evaluator_succeeded',
'Experiment Started', '$experiment_started',
'web_vital_metric_inp_track', 'web_vital_metric_ttfb_track',
'performance_metric_user_timing_discovery_search',
'performance_metric_user_timing_app_bootstrap',
'web_vital_metric_fcp_track', 'web_vital_metric_lcp_track'
)
AND t.event_text NOT LIKE 'workflow_%'
AND t.TIMESTAMP >= {{START_DATE}}
UNION ALL
SELECT
p.user_id,
p.domain,
DATE_TRUNC('WEEK', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP)) AS event_week
FROM {{DATABASE}}.{{SCHEMA}}.PAGES p
WHERE p.TIMESTAMP >= {{START_DATE}}
)
SELECT
domain,
event_week,
COUNT(DISTINCT user_id) AS wau
FROM activity_events
WHERE domain = {{DOMAIN}}
GROUP BY domain, event_week
ORDER BY domain, event_week DESC;
Sample output
| DOMAIN | EVENT_WEEK | WAU |
|---|---|---|
| acme.atlan.com | 2025-03-10 | 89 |
| acme.atlan.com | 2025-03-03 | 94 |
| acme.atlan.com | 2025-02-24 | 81 |
Monthly active users
Returns the count of distinct users active each calendar month with month-over-month delta and percentage change.
- Snowflake
-- mau_by_domain.sql
-- Monthly Active Users per customer domain with month-over-month delta.
-- Uses PAGES.domain for domain filtering. Counts by user_id (not email).
-- LEFT JOINs USERS for optional email enrichment.
--
-- Parameters:
-- {{START_DATE}} - e.g., '2025-01-01'
-- {{DOMAIN}} - e.g., 'acme.atlan.com' (or replace filter line with 1=1 for all)
WITH user_domains AS (
SELECT user_id, MAX(domain) AS domain
FROM {{DATABASE}}.{{SCHEMA}}.PAGES
WHERE domain IS NOT NULL
GROUP BY user_id
),
activity_events AS (
SELECT
t.user_id,
ud.domain,
DATE_TRUNC('MONTH', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP)) AS event_month
FROM {{DATABASE}}.{{SCHEMA}}.TRACKS t
INNER JOIN user_domains ud ON ud.user_id = t.user_id
WHERE t.event_text NOT IN (
'atlan_analaytics_aggregateinfo_fetch',
'api_error_emit', 'api_evaluator_cancelled', 'api_evaluator_succeeded',
'Experiment Started', '$experiment_started',
'web_vital_metric_inp_track', 'web_vital_metric_ttfb_track',
'performance_metric_user_timing_discovery_search',
'performance_metric_user_timing_app_bootstrap',
'web_vital_metric_fcp_track', 'web_vital_metric_lcp_track'
)
AND t.event_text NOT LIKE 'workflow_%'
AND t.TIMESTAMP >= {{START_DATE}}
UNION ALL
SELECT
p.user_id,
p.domain,
DATE_TRUNC('MONTH', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP)) AS event_month
FROM {{DATABASE}}.{{SCHEMA}}.PAGES p
WHERE p.TIMESTAMP >= {{START_DATE}}
),
mau_counts AS (
SELECT
domain,
event_month,
COUNT(DISTINCT user_id) AS mau
FROM activity_events
WHERE domain = {{DOMAIN}}
GROUP BY domain, event_month
)
SELECT
domain,
event_month,
mau,
LAG(mau) OVER (PARTITION BY domain ORDER BY event_month) AS prev_month_mau,
mau - LAG(mau) OVER (PARTITION BY domain ORDER BY event_month) AS mau_delta,
ROUND(100.0 * (mau - LAG(mau) OVER (PARTITION BY domain ORDER BY event_month))
/ NULLIF(LAG(mau) OVER (PARTITION BY domain ORDER BY event_month), 0), 1) AS mau_change_pct
FROM mau_counts
ORDER BY domain, event_month DESC;
Sample output
| DOMAIN | EVENT_MONTH | MAU | PREV_MONTH_MAU | MAU_DELTA | MAU_CHANGE_PCT |
|---|---|---|---|---|---|
| acme.atlan.com | 2025-03-01 | 124 | 118 | 6 | 5.1 |
| acme.atlan.com | 2025-02-01 | 118 | 110 | 8 | 7.3 |
| acme.atlan.com | 2025-01-01 | 110 | null | null | null |
Stickiness ratio
Computes the DAU/MAU stickiness ratio per domain per month. A ratio greater than 0.3 indicates strong daily engagement. A ratio less than 0.1 indicates episodic usage.
- Snowflake
-- mau_dau_ratio.sql
-- DAU/MAU stickiness ratio per domain per month.
-- >0.3 = strong daily engagement, <0.1 = episodic usage.
-- Uses PAGES.domain for filtering. Counts by user_id.
--
-- Parameters:
-- {{START_DATE}} - e.g., '2025-01-01'
-- {{DOMAIN}} - e.g., 'acme.atlan.com' (or replace filter line with 1=1 for all)
WITH user_domains AS (
SELECT user_id, MAX(domain) AS domain
FROM {{DATABASE}}.{{SCHEMA}}.PAGES
WHERE domain IS NOT NULL
GROUP BY user_id
),
activity_events AS (
SELECT
t.user_id,
ud.domain,
DATE(CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP)) AS event_date,
DATE_TRUNC('MONTH', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP)) AS event_month
FROM {{DATABASE}}.{{SCHEMA}}.TRACKS t
INNER JOIN user_domains ud ON ud.user_id = t.user_id
WHERE t.event_text NOT IN (
'atlan_analaytics_aggregateinfo_fetch',
'api_error_emit', 'api_evaluator_cancelled', 'api_evaluator_succeeded',
'Experiment Started', '$experiment_started',
'web_vital_metric_inp_track', 'web_vital_metric_ttfb_track',
'performance_metric_user_timing_discovery_search',
'performance_metric_user_timing_app_bootstrap',
'web_vital_metric_fcp_track', 'web_vital_metric_lcp_track'
)
AND t.event_text NOT LIKE 'workflow_%'
AND t.TIMESTAMP >= {{START_DATE}}
UNION ALL
SELECT
p.user_id,
p.domain,
DATE(CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP)) AS event_date,
DATE_TRUNC('MONTH', CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP)) AS event_month
FROM {{DATABASE}}.{{SCHEMA}}.PAGES p
WHERE p.TIMESTAMP >= {{START_DATE}}
),
daily_users AS (
SELECT
domain,
event_month,
event_date,
COUNT(DISTINCT user_id) AS dau
FROM activity_events
WHERE domain = {{DOMAIN}}
GROUP BY domain, event_month, event_date
),
monthly_users AS (
SELECT
domain,
event_month,
COUNT(DISTINCT user_id) AS mau
FROM activity_events
WHERE domain = {{DOMAIN}}
GROUP BY domain, event_month
)
SELECT
m.domain,
m.event_month,
m.mau,
ROUND(AVG(d.dau), 1) AS avg_dau,
ROUND(AVG(d.dau) / NULLIF(m.mau, 0), 3) AS stickiness_ratio,
CASE
WHEN AVG(d.dau) / NULLIF(m.mau, 0) >= 0.3 THEN 'Strong'
WHEN AVG(d.dau) / NULLIF(m.mau, 0) >= 0.1 THEN 'Moderate'
ELSE 'Episodic'
END AS engagement_level
FROM monthly_users m
JOIN daily_users d ON d.domain = m.domain AND d.event_month = m.event_month
GROUP BY m.domain, m.event_month, m.mau
ORDER BY m.domain, m.event_month DESC;
Sample output
| DOMAIN | EVENT_MONTH | MAU | AVG_DAU | STICKINESS_RATIO | ENGAGEMENT_LEVEL |
|---|---|---|---|---|---|
| acme.atlan.com | 2025-03-01 | 124 | 41.2 | 0.332 | Strong |
| acme.atlan.com | 2025-02-01 | 118 | 18.5 | 0.157 | Moderate |
| acme.atlan.com | 2025-01-01 | 110 | 8.9 | 0.081 | Episodic |
User roster
Returns the full list of active users for a domain with activity metadata: total events, active days, first and last activity timestamps, days since last activity, and a status label (Active, Inactive, or Churned).
- Snowflake
-- user_roster_by_domain.sql
-- Full user list for a domain with activity status, last activity, and event counts.
-- Starts from active user_ids in PAGES, LEFT JOINs USERS for metadata.
-- Shows all active users even if they have no USERS record.
--
-- Parameters:
-- {{START_DATE}} - e.g., '2025-01-01'
-- {{DOMAIN}} - e.g., 'acme.atlan.com'
WITH user_domains AS (
SELECT user_id, MAX(domain) AS domain
FROM {{DATABASE}}.{{SCHEMA}}.PAGES
WHERE domain IS NOT NULL
GROUP BY user_id
),
activity_events AS (
SELECT
t.user_id,
CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', t.TIMESTAMP) AS event_ts
FROM {{DATABASE}}.{{SCHEMA}}.TRACKS t
INNER JOIN user_domains ud ON ud.user_id = t.user_id AND ud.domain = {{DOMAIN}}
WHERE t.event_text NOT IN (
'atlan_analaytics_aggregateinfo_fetch',
'api_error_emit', 'api_evaluator_cancelled', 'api_evaluator_succeeded',
'Experiment Started', '$experiment_started',
'web_vital_metric_inp_track', 'web_vital_metric_ttfb_track',
'performance_metric_user_timing_discovery_search',
'performance_metric_user_timing_app_bootstrap',
'web_vital_metric_fcp_track', 'web_vital_metric_lcp_track'
)
AND t.event_text NOT LIKE 'workflow_%'
AND t.TIMESTAMP >= {{START_DATE}}
UNION ALL
SELECT
p.user_id,
CONVERT_TIMEZONE('UTC', 'Asia/Kolkata', p.TIMESTAMP) AS event_ts
FROM {{DATABASE}}.{{SCHEMA}}.PAGES p
WHERE p.domain = {{DOMAIN}}
AND p.TIMESTAMP >= {{START_DATE}}
),
user_activity AS (
SELECT
user_id,
COUNT(*) AS total_events,
COUNT(DISTINCT DATE(event_ts)) AS active_days,
MIN(event_ts) AS first_activity,
MAX(event_ts) AS last_activity
FROM activity_events
GROUP BY user_id
),
user_meta AS (
SELECT id, email, username, role,
MAX(license_type) AS license_type,
MAX(job_role) AS job_role,
MIN(created_at) AS user_created_at
FROM {{DATABASE}}.{{SCHEMA}}.USERS
WHERE email IS NOT NULL
GROUP BY id, email, username, role
)
SELECT
a.user_id,
um.email,
um.username,
um.role,
um.license_type,
um.job_role,
um.user_created_at,
a.total_events,
a.active_days,
a.first_activity,
a.last_activity,
DATEDIFF('day', a.last_activity, CURRENT_TIMESTAMP()) AS days_since_last_activity,
CASE
WHEN a.last_activity >= DATEADD('day', -30, CURRENT_TIMESTAMP()) THEN 'Active'
WHEN a.last_activity >= DATEADD('day', -90, CURRENT_TIMESTAMP()) THEN 'Inactive'
WHEN a.last_activity IS NULL THEN 'Never Active'
ELSE 'Churned'
END AS status
FROM user_activity a
LEFT JOIN user_meta um ON um.id = a.user_id
ORDER BY a.total_events DESC;
Sample output
| USER_ID | USERNAME | ROLE | LICENSE_TYPE | JOB_ROLE | USER_CREATED_AT | TOTAL_EVENTS | ACTIVE_DAYS | FIRST_ACTIVITY | LAST_ACTIVITY | DAYS_SINCE_LAST_ACTIVITY | STATUS | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| usr_001 | [email protected] | alice | admin | guest | Data Engineer | 2024-01-10 | 3420 | 58 | 2025-01-02 | 2025-03-14 | 1 | Active |
| usr_002 | [email protected] | bob | member | member | Data Analyst | 2024-02-15 | 1850 | 41 | 2025-01-05 | 2025-03-10 | 5 | Active |
| usr_003 | [email protected] | carol | member | guest | Business Analyst | 2024-03-01 | 320 | 12 | 2025-01-08 | 2024-12-20 | 85 | Inactive |